动机
很早之前看过这样一个漫画,如何用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的使用方式。
你也可以略过本文直接查看示例工程:
- 使用 React Native CLI:UMIRNExample
- 使用 expo:UMIExpoExample
- 使用 haul 拆包:UMIHaulExample
当 RN 工程满足下列条件时,会进行拆包:
- 安装并启用了haul打包器;
- 开启了dynamicImport配置。
必备
- 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 添加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 当前(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插件需要开发者自行斟酌。
umi插件包括:
- 内建插件:@umijs/preset-built-in,这一部分是无法拆除的。
- 额外扩展插件:@umijs/plugins
与 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,
};
// .umirc.js
export default {
expo: false,
haul: false,
};
Babel 配置
使用extraBabelPlugins和extraBabelPresets添加额外的 Babel 配置。
Metro 配置
添加额外的Metro 配置需要使用环境变量:UMI_ENV指定要加载的配置文件:metro.${UMI_ENV}.config.js
。
比如,执行UMI_ENV=dev umi g rn
时,会加载metro.dev.config.js
文件中的配置,使用mergeConfig同metro.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-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
组件,当你尝试引入时会得到undefined
:
import { NavLink } from 'umi';
typeof NavLink=== 'undefined'; // true;
新增BackButton
和AndroidBackButton
组件
对于 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
后才会生效。
缺省情况下:
- 如果未启用dynamicImport配置,则会使用一个内置的简陋 Loading;
- 如果启用dynamicImport配置,则会使用
dynamicImport.loading
;- 如果未实现自定义的
dynamicImport.loading
,dynamicImport默认的 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 ios
和yarn 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" } }