使用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阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350