使用umi開發react-native應用

動機

很早之前看過這樣一個漫畫,如何用8種編程語言去救公主:

JavaScript: 你是一個喝著咖啡的有品味的騎士。你花了好多時間去選擇支持庫,設置節點和為城堡建造一個框架。當你完成了框架的時候,你發現公主所在的要塞已經被廢棄,公主也已經搬到了另外一個城堡。

記得似乎是從 nextjs 起,前端框架就進入了帶編譯時的時代。

自此,開發者可以迅速投入到業務代碼的開發,而不用去搭建腳手架,寫一堆配置和膠水代碼去整合各種框架等等。

筆者在Web端習慣使用 umi 后,就變得越來越“懶”,什么問題都用這一錘子解決。

當工作中涉及到 react-native(后文簡稱:RN)應用的內容時,發現 umi 暫時沒有支持RN的打算。

筆者從Github clone了 umi 的代碼研究學習后發現整個 umi 引擎設計的非常科學。

基于 umi 插件化的思想,很容易就能擴展一些額外的能力用于支持 RN 的開發。

于是就產生了這個項目:umi-react-native

umi 在 RN 中僅用來生成中間代碼(臨時文件),介于編碼構建的之間,旨在引入 umi 的開發姿勢來提升 RN 編程體驗。

下游可以使用:

  • React Native CLI:RN 官方開發/打包工具;
  • expo:不需要搭建 iOS 和 Android 開發環境,工程目錄干凈清爽,添加 RN 依賴方便快捷;
  • haul:第三方 RN 打包器,使用 webpack。缺點是不支持:Fast Refresh、Live Reloading、Hot Replacement。

目前的版本已經支持:

  • 零配置,添加dva@ant-design/react-native... 等依賴后開箱即用;
  • 只需要專注頁面 UI 和業務領域模型的實現,所有編譯配置,框架運行所需 HOC 和 Context Provider 全部由 umi 搞定;
  • 路由方案默認使用 umi 內置的react-router可選react-navigation
  • 啟用dynamicImport配置后,支持拆包,運行時從本地按需加載 JS bundle 文件。

實施

下面將詳細介紹umi-react-native的使用方式。

你也可以略過本文直接查看示例工程

當 RN 工程滿足下列條件時,會進行拆包:

必備

  • RN 工程;
  • umi 3.0 及以上版本。

概覽

NPM 包 簡介
umi-plugin-antd-react-native @ant-design/react-native提供按需加載主題定制、預設、切換,國際化支持,在expo鏈接字體圖標
umi-preset-react-native 基礎包,讓umi具備開發 RN 的能力。需要 react-native 0.44.0 及以上版本(>=0.44.0)
umi-preset-react-navigation 使用react-navigation替換react-router開發地道的原生應用。需要 react-native 0.60.0 及以上版本(>=0.60.0)
umi-renderer-react-navigation 支持以react-navigation的方式來渲染react-router所定義的路由模型。無須單獨安裝該依賴
umi-react-native-multibundle RN Bridge API,為 JS 層提供按需加載 Bundle 文件的能力。需要 react-native 0.62.2 及以上版本(>=0.62.2)

安裝

如果沒有 RN 工程,則使用react-native init得到初始工程:

npx react-native init UMIRNExample

在 RN 工程根目錄下使用 yarn 添加umiumi-preset-react-native依賴:

yarn add umi umi-preset-react-native --dev

集成 dva

在 RN 工程根目錄下使用 yarn 添加@umijs/plugin-dva依賴:

yarn add @umijs/plugin-dva --dev

集成 @ant-design/react-native

在 RN 工程目錄下,使用 yarn 安裝@ant-design/react-native:

yarn add @ant-design/react-native && yarn add umi-plugin-antd-react-native --dev

@ant-design/react-native 當前(2020/05/14)版本:3.x。如需使用4.x請按照:安裝 & 使用操作。

集成 react-navigation(可選)

react-navigation可作為 umi 默認react-router替代方案

需要 react-native 0.60.0 及以上版本(>=0.60.x)

安裝所有react-navigation的依賴到 RN 工程本地:

yarn add react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view

RN0.60.0 及以上版本有自動鏈接功能,Android 會自動搞定這些react-navigation的原生依賴,但對于iOS,待 yarn 安裝完成后,還需要進到 ios 目錄,使用 pod 安裝:

cd ios && pod install
image

最后,使用 yarn 安裝umi-preset-react-navigation

yarn add umi-preset-react-navigation --dev

查看詳情:umi-preset-react-navigation

配置

All dependencies start with @umijs/preset-、@umijs/plugin-、umi-preset-、umi-plugin- will be registered as plugin/plugin-preset.

umi 3.x 后會自動探測、裝配插件。所以不需要在.umirc.js中配置pluginspresets

在 RN 中集成其他umi插件需要開發者自行斟酌。

umi插件包括:

與 DOM 無關的umi插件都是可以使用的,或者說支持服務端渲染的插件基本也是可以在 RN 運行環境中使用的。

umi-preset-react-native 擴展配置

umi-preset-react-native會探測用戶工程內的依賴,自動為下列工具生成所需的配置文件入口文件

推薦在.gitignore文件末尾,追加以下內容:

# umi-react-native
tmp
index.js
metro.config.js
babel.config.js
haul.config.js

如果你的 RN 工程只使用一種開發工具則無需任何配置

如果你的 RN 工程安裝了多種開發工具,則必須通過 umi 配置指定當前使用哪一個:

使用expo

// .umirc.js
export default {
  expo: true,
  haul: false,
};

使用haul

// .umirc.js
export default {
  expo: false,
  haul: true,
};

使用React Native CLI:

// .umirc.js
export default {
  expo: false,
  haul: false,
};

Babel 配置

使用extraBabelPluginsextraBabelPresets添加額外的 Babel 配置。

Metro 配置

添加額外的Metro 配置需要使用環境變量:UMI_ENV指定要加載的配置文件:metro.${UMI_ENV}.config.js

比如,執行UMI_ENV=dev umi g rn時,會加載metro.dev.config.js文件中的配置,使用mergeConfigmetro.config.js中的配置進行合并。

使用

開發

修改package.json文件:

{
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
+   "watch": "umi g rn --dev",
    "test": "jest",
    "lint": "eslint ."
  }
}

啟動 watch 進程,監聽文件變動,重新生成中間代碼:

yarn watch

接下來,另啟一個終端,編譯并啟動 Android 應用:

yarn android

編譯并啟動 iOS 應用:

yarn ios

打包

先使用 umi 生成臨時代碼:

umi g rn

再使用react-native bundle構建離線包(offline bundle)。

路由

umi-preset-react-native提供了 2 種可相互替代的路由方案:

使用 umi 內置的 react-router

umi內置了react-router-domumi-preset-react-native使用alias在編譯時將其替換為:react-router-native

二者都基于 react-router,但存在一些差異。

Link組件在 RN 和 DOM 中存在差異

以下是react-router-native Link組件的屬性:

Link.propTypes= {
  onPress: PropTypes.func,
  component: PropTypes.elementType,
  replace: PropTypes.bool,
  to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
};

在 RN 中,只能這樣使用Link

import React from 'react';
import { Link } from 'umi';
import { List } from '@ant-design/react-native';

const Item = List.Item;

function Index() {
  return (
    <List>
      <Link to="/home" component={Item} arrow="horizontal">
        主頁
      </Link>
      <Link to="/login" component={Item} arrow="horizontal">
        登錄頁
      </Link>
    </List>
  );
}

沒有NavLink組件

react-router-native沒有NavLink組件,當你嘗試引入時會得到undefined

import { NavLink } from 'umi';

typeof NavLink=== 'undefined'; // true;

新增BackButtonAndroidBackButton組件

對于 RN 應用,需要在全局 layout中使用BackButton作為根容器:

// layouts/index.js
import React from 'react';
import { SafeAreaView, StatusBar } from 'react-native';
import { BackButton } from 'umi';
const Layout = ({ children }) => {
  return (
    <BackButton>
      {children}
    </BackButton>
  );
};

export default Layout;

這樣做,當用戶使用Android 系統返回鍵時會返回應用的上一個路由,而不是退出應用。

使用 react-navigation

擴展配置

以下是安裝umi-preset-react-navigation后,擴展的 umi 配置

reactNavigation

theme字段選填,下面示例中填入的是默認值,等價于不填:

// .umirc.js
export default {
  reactNavigation: {
    // 使用 ant-design 默認配色作為導航條的默認主題
    theme: {
      dark: false,
      colors: {
        primary: '#108ee9',
        background: '#ffffff',
        card: '#ffffff',
        text: '#000000',
        border: '#dddddd',
      },
    },
  },
};

擴展運行時配置

查看 umi 文檔,了解什么是:運行時配置

以下是安裝umi-preset-react-navigation后,擴展的運行時配置

getReactNavigationInitialState

異步(async)函數,返回的 promise resolve 后的結果會傳給 react-navigation 作為初始狀態。

返回類型:Promise<object | void | undefined>

getReactNavigationInitialIndicator

自定義初始化 react-navigation 狀態過程中的指示器/Loading。通常在實現了上面的getReactNavigationInitialState后才會生效。

缺省情況下:

  1. 如果未啟用dynamicImport配置,則會使用一個內置的簡陋 Loading;
  2. 如果啟用dynamicImport配置,則會使用dynamicImport.loading
    1. 如果未實現自定義的dynamicImport.loadingdynamicImport默認的 Loading 同樣也很簡陋。
onReactNavigationStateChange

異步(async)函數,用于訂閱 react-navigation 狀態變更通知,在每次路由變動時,接收最新狀態。

案例:持久化導航狀態

RN 工程根目錄下app.js文件:

// app.js
import { Linking, Platform, Text } from 'react-native';
/**
 * AsyncStorage 將來會從 react-native 庫中移除。
 * 按照 RN 官方文檔引用:https://github.com/react-native-community/async-storage
 */
import AsyncStorage from '@react-native-community/async-storage';

const PERSISTENCE_KEY = 'MY_NAVIGATION_STATE';

// 返回之前本地持久化保存的狀態,通常用于需要復蘇應用、狀態恢復的場景。
export async function getReactNavigationInitialState() {
  try {
    const initialUrl = await Linking.getInitialURL();
    if (Platform.OS !== 'web' && initialUrl == null) {
      const savedStateString = await AsyncStorage.getItem(PERSISTENCE_KEY);
      if (savedStateString) {
        return JSON.parse(savedStateString);
      }
    }
  } catch (ignored) {}
}

// 自定義返回初始狀態過程中顯示的Loading,只有實現了 getReactNavigationInitialState 才會生效。
export function getReactNavigationInitialIndicator() {
  // 下面這個就是內置的簡陋Loading:
  return ({ error, isLoading }) => {
    if (__DEV__) {
      if (isLoading) {
        return React.createElement(Text, null, 'Loading...');
      }
      if (error) {
        return React.createElement(
          View,
          null,
          React.createElement(Text, null, error.message),
          React.createElement(Text, null, error.stack),
        );
      }
    }
    return React.createElement(Text, null, 'Loading...');
  };
}

// 訂閱 react-navigation 狀態變化通知,每次路由變化時,將導航狀態持久化保存到手機本地。
export async function onReactNavigationStateChange(state) {
  if (state) {
    await AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state));
  }
}
  • 如果你需要用到@react-native-community/async-storage請按照https://github.com/react-native-community/async-storage安裝;
  • 安裝完成后,記得進到 ios 目錄使用 pod 安裝原生依賴:cd ios && pod install && cd -,之后記得使用yarn iosyarn android重新編譯,啟動原生 App。

擴展路由屬性

查看 umi 文檔,了解什么是:擴展路由屬性

案例:單獨為某個頁面設置導航條

使用擴展路由屬性定制頂部導航條:

import React from 'react';
import { Text } from 'react-native';
import { Button } from '@ant-design/react-native';

function HomePage({ navigation }) {
  // 處理導航條右側按鈕點擊事件
  function onHeaderRightPress() {
    // do something...
  }

  // 設置導航條右側按鈕
  useLayoutEffect(() => {
    navigation.setOptions({
      headerRight: () => (
        <Button type="primary" size="small" onPress={onHeaderRightPress}>
          彈窗
        </Button>
      ),
    });
  }, [navigation]);

  return <Text>Home Page</Text>;
}

// 擴展路由屬性:
HomePage.title = 'Home Page';
HomePage.headerTintColor = '#000000';
HomePage.headerTitleStyle = {
  fontWeight: 'bold',
};
HomePage.headerStyle = {
  backgroundColor: '#ffffff',
};
// headerRight 也可以寫在這里:
// HomePage.headerRight = () => (
//  <Button type="primary" size="small">
//    彈窗
//  </Button>
// );

export default HomePage;

如果頁面的title屬性未設置,則使用.umirc.js中的全局title

頁面間跳轉

查看 umi 文檔:頁面間跳轉,姿勢保持不變。

使用聲明式Link組件時需要注意,在 RN 中 與 DOM 存在較大差異:

import React from 'react';
import { Link } from 'umi';
import { List } from '@ant-design/react-native';

const Item = List.Item;

function Index() {
  return (
    <List>
      <Link to="/home" component={Item} arrow="horizontal">
        主頁
      </Link>
      <Link to="/login" component={Item} arrow="horizontal">
        登錄頁
      </Link>
    </List>
  );
}

使用命令式跳轉頁面時,只能使用history的 API,umi-preset-react-navigation目前還不支持使用react-navigation提供的navigation來跳轉,只能做導航條設置之類的操作。

頁面間傳遞/接收參數

IndexPage點擊Link,攜帶query參數路由到HomePage:

import React from 'react';
import { Link } from 'umi';
import { List } from '@ant-design/react-native';

const Item = List.Item;

export default function IndexPage() {
  return (
    <List>
      <Link to="/home?name=bar" component={Item} arrow="horizontal">
        主頁
      </Link>
    </List>
  );
}
export default function HomePage({ route }) {
  console.log(route); // route 屬性字段查看下面

  // ...
}

route屬性示例:

{ "key": "/home-WnnfQomYXFls0kS0v0lxo", "name": "/home", "params": { "name": "bar" } }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374