動機
很早之前看過這樣一個漫畫三圆,如何用8種編程語言去救公主:
JavaScript: 你是一個喝著咖啡的有品味的騎士。你花了好多時間去選擇支持庫扔茅,設(shè)置節(jié)點和為城堡建造一個框架。當(dāng)你完成了框架的時候,你發(fā)現(xiàn)公主所在的要塞已經(jīng)被廢棄矩距,公主也已經(jīng)搬到了另外一個城堡。
記得似乎是從 nextjs 起怖竭,前端框架就進入了帶編譯時的時代锥债。
自此,開發(fā)者可以迅速投入到業(yè)務(wù)代碼的開發(fā)痊臭,而不用去搭建腳手架哮肚,寫一堆配置和膠水代碼去整合各種框架等等。
筆者在Web端習(xí)慣使用 umi 后广匙,就變得越來越“懶”允趟,什么問題都用這一錘子解決。
當(dāng)工作中涉及到 react-native(后文簡稱:RN)應(yīng)用的內(nèi)容時鸦致,發(fā)現(xiàn) umi 暫時沒有支持RN的打算潮剪。
筆者從Github clone了 umi 的代碼研究學(xué)習(xí)后發(fā)現(xiàn)整個 umi 引擎設(shè)計的非常科學(xué)分唾。
基于 umi 插件化的思想抗碰,很容易就能擴展一些額外的能力用于支持 RN 的開發(fā)。
于是就產(chǎn)生了這個項目:umi-react-native绽乔。
umi 在 RN 中僅用來生成中間代碼(臨時文件)弧蝇,介于編碼和構(gòu)建的之間,旨在引入 umi 的開發(fā)姿勢來提升 RN 編程體驗折砸。
下游可以使用:
- React Native CLI:RN 官方開發(fā)/打包工具看疗;
- expo:不需要搭建 iOS 和 Android 開發(fā)環(huán)境,工程目錄干凈清爽鞍爱,添加 RN 依賴方便快捷鹃觉;
- haul:第三方 RN 打包器,使用 webpack睹逃。缺點是不支持:Fast Refresh盗扇、Live Reloading祷肯、Hot Replacement。
目前的版本已經(jīng)支持:
- 零配置疗隶,添加dva佑笋,@ant-design/react-native... 等依賴后開箱即用;
- 只需要專注頁面 UI 和業(yè)務(wù)領(lǐng)域模型的實現(xiàn)斑鼻,所有編譯配置蒋纬,框架運行所需 HOC 和 Context Provider 全部由 umi 搞定;
- 路由方案默認使用 umi 內(nèi)置的react-router坚弱,可選react-navigation蜀备;
- 啟用dynamicImport配置后,支持拆包荒叶,運行時從本地按需加載 JS bundle 文件碾阁。
實施
下面將詳細介紹umi-react-native的使用方式。
你也可以略過本文直接查看示例工程:
- 使用 React Native CLI:UMIRNExample
- 使用 expo:UMIExpoExample
- 使用 haul 拆包:UMIHaulExample
當(dāng) RN 工程滿足下列條件時些楣,會進行拆包:
- 安裝并啟用了haul打包器脂凶;
- 開啟了dynamicImport配置。
必備
- RN 工程愁茁;
- umi 3.0 及以上版本蚕钦。
概覽
NPM 包 | 簡介 |
---|---|
umi-plugin-antd-react-native | 為@ant-design/react-native提供按需加載,主題定制鹅很、預(yù)設(shè)嘶居、切換,國際化支持道宅,在expo中鏈接字體圖標(biāo)食听。 |
umi-preset-react-native | 基礎(chǔ)包,讓umi具備開發(fā) RN 的能力污茵。需要 react-native 0.44.0 及以上版本(>=0.44.0) |
umi-preset-react-navigation | 使用react-navigation替換react-router開發(fā)地道的原生應(yīng)用樱报。需要 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 添加umi
和umi-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 當(dāng)前(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
最后,使用 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
中配置plugins和presets帮掉。
在 RN 中集成其他umi插件需要開發(fā)者自行斟酌弦悉。
umi插件包括:
- 內(nèi)建插件:@umijs/preset-built-in,這一部分是無法拆除的蟆炊。
- 額外擴展插件:@umijs/plugins
與 DOM 無關(guān)的umi插件都是可以使用的警绩,或者說支持服務(wù)端渲染的插件基本也是可以在 RN 運行環(huán)境中使用的。
umi-preset-react-native 擴展配置
umi-preset-react-native會探測用戶工程內(nèi)的依賴盅称,自動為下列工具生成所需的配置文件和入口文件。
推薦在.gitignore
文件末尾后室,追加以下內(nèi)容:
# umi-react-native
tmp
index.js
metro.config.js
babel.config.js
haul.config.js
如果你的 RN 工程只使用一種開發(fā)工具則無需任何配置缩膝。
如果你的 RN 工程安裝了多種開發(fā)工具,則必須通過 umi 配置指定當(dāng)前使用哪一個:
使用expo:
// .umirc.js
export default {
expo: true,
haul: false,
};
使用haul:
// .umirc.js
export default {
expo: false,
haul: true,
};
// .umirc.js
export default {
expo: false,
haul: false,
};
Babel 配置
使用extraBabelPlugins和extraBabelPresets添加額外的 Babel 配置岸霹。
Metro 配置
添加額外的Metro 配置需要使用環(huán)境變量:UMI_ENV指定要加載的配置文件:metro.${UMI_ENV}.config.js
疾层。
比如,執(zhí)行UMI_ENV=dev umi g rn
時贡避,會加載metro.dev.config.js
文件中的配置痛黎,使用mergeConfig同metro.config.js
中的配置進行合并。
使用
開發(fā)
修改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 進程刮吧,監(jiān)聽文件變動湖饱,重新生成中間代碼:
yarn watch
接下來,另啟一個終端杀捻,編譯并啟動 Android 應(yīng)用:
yarn android
編譯并啟動 iOS 應(yīng)用:
yarn ios
打包
先使用 umi 生成臨時代碼:
umi g rn
再使用react-native bundle構(gòu)建離線包(offline bundle)井厌。
路由
umi-preset-react-native提供了 2 種可相互替代的路由方案:
使用 umi 內(nèi)置的 react-router
umi內(nèi)置了react-router-dom
,umi-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
組件墓拜,當(dāng)你嘗試引入時會得到undefined
:
import { NavLink } from 'umi';
typeof NavLink=== 'undefined'; // true;
新增BackButton
和AndroidBackButton
組件
對于 RN 應(yīng)用,需要在全局 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;
這樣做请契,當(dāng)用戶使用Android 系統(tǒng)返回鍵時會返回應(yīng)用的上一個路由咳榜,而不是退出應(yīng)用夏醉。
使用 react-navigation
擴展配置
以下是安裝umi-preset-react-navigation后,擴展的 umi 配置:
reactNavigation
theme
字段選填贿衍,下面示例中填入的是默認值授舟,等價于不填:
// .umirc.js
export default {
reactNavigation: {
// 使用 ant-design 默認配色作為導(dǎo)航條的默認主題
theme: {
dark: false,
colors: {
primary: '#108ee9',
background: '#ffffff',
card: '#ffffff',
text: '#000000',
border: '#dddddd',
},
},
},
};
擴展運行時配置
查看 umi 文檔,了解什么是:運行時配置贸辈。
以下是安裝umi-preset-react-navigation后释树,擴展的運行時配置:
getReactNavigationInitialState
異步(async)函數(shù),返回的 promise resolve 后的結(jié)果會傳給 react-navigation 作為初始狀態(tài)擎淤。
返回類型:Promise<object | void | undefined>
奢啥。
getReactNavigationInitialIndicator
自定義初始化 react-navigation 狀態(tài)過程中的指示器/Loading。通常在實現(xiàn)了上面的getReactNavigationInitialState
后才會生效嘴拢。
缺省情況下:
- 如果未啟用dynamicImport配置桩盲,則會使用一個內(nèi)置的簡陋 Loading;
- 如果啟用dynamicImport配置席吴,則會使用
dynamicImport.loading
赌结;- 如果未實現(xiàn)自定義的
dynamicImport.loading
,dynamicImport默認的 Loading 同樣也很簡陋孝冒。
- 如果未實現(xiàn)自定義的
onReactNavigationStateChange
異步(async)函數(shù)柬姚,用于訂閱 react-navigation 狀態(tài)變更通知,在每次路由變動時庄涡,接收最新狀態(tài)量承。
案例:持久化導(dǎo)航狀態(tài)
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';
// 返回之前本地持久化保存的狀態(tài)穴店,通常用于需要復(fù)蘇應(yīng)用撕捍、狀態(tài)恢復(fù)的場景。
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) {}
}
// 自定義返回初始狀態(tài)過程中顯示的Loading泣洞,只有實現(xiàn)了 getReactNavigationInitialState 才會生效忧风。
export function getReactNavigationInitialIndicator() {
// 下面這個就是內(nèi)置的簡陋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 狀態(tài)變化通知,每次路由變化時斜棚,將導(dǎo)航狀態(tài)持久化保存到手機本地阀蒂。
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 ios
和yarn android
重新編譯,啟動原生 App义钉。
擴展路由屬性
查看 umi 文檔昧绣,了解什么是:擴展路由屬性。
案例:單獨為某個頁面設(shè)置導(dǎo)航條
使用擴展路由屬性定制頂部導(dǎo)航條:
import React from 'react';
import { Text } from 'react-native';
import { Button } from '@ant-design/react-native';
function HomePage({ navigation }) {
// 處理導(dǎo)航條右側(cè)按鈕點擊事件
function onHeaderRightPress() {
// do something...
}
// 設(shè)置導(dǎo)航條右側(cè)按鈕
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
屬性未設(shè)置捶闸,則使用.umirc.js
中的全局title夜畴。
頁面間跳轉(zhuǎn)
查看 umi 文檔:頁面間跳轉(zhuǎn)拖刃,姿勢保持不變。
使用聲明式的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>
);
}
使用命令式跳轉(zhuǎn)頁面時兑牡,只能使用history
的 API,umi-preset-react-navigation目前還不支持使用react-navigation提供的navigation
來跳轉(zhuǎn)税灌,只能做導(dǎo)航條設(shè)置之類的操作均函。
頁面間傳遞/接收參數(shù)
在IndexPage
點擊Link
,攜帶query
參數(shù)路由到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" } }