update: image source

This commit is contained in:
杭城小刘
2024-02-23 15:58:55 +08:00
parent fb51939f76
commit 6e47061735
22 changed files with 783 additions and 61 deletions

View File

@@ -1,5 +1,604 @@
# React Native 总结
## 一、为什么React/React Native 采用组件式的设计?
除了组件式之外,常见的构建方式还有类似 HTML + CSS + JS 分层的架构、基于 MVC 的架构,那为什么会采用组件的架构设计?
- 随着前端/客户端等展示层 UI 交互承载的业务逻辑越来越复杂,单纯的 MVC 会面临「**胖 Model**」、「**巨大 Controller**」等情况
- 业务千变万化,昨日的设计无法支撑下个版本的产品调性,但写过的代码还是有用的,直接 copy 过去?
- 设计部门也在千变万化的版本迭代中沉淀公司自己的设计规范、交互规范、Android、iOS 各个平台的人机交互指南,基于此也沉淀产出了 UI 组件库。那端上同学必然基于组件式去写一套 UI 组件代码。
- 组件化的风慢慢的吹到了 RN 诞生之初2010年开源的 MVC 模式的 AngularJS 也被最新的 Angular>= V2这种基于组件的架构模式所取代。组件式影响到新诞生的一批框架
React/React Native 选择基于组件的架构模式好处有3:
- 第一,组件是内聚的,组件内既有逻辑,也有状态,又有视图。一个组件可以独立完成一个事情,这也使得 UI 模块复用变得简单;
- 第二,组件之间是可以组合的,多个组件可以组合成一个更大的页面或者一个更大的组件。当一个组件很大很臃肿、难以维护的时候也可以拆分优化成粒度更合理的组件
- 第三,组件和组件之间的数据流动永远是确定的,从上到下单向流动
组件可组合、可复用的特性,和组件之间单向数据流的模式,是现代应用重交互重展示的情况下,更方便,这也是 React/React Native 采用组件式的核心原因。
## 二、热更新平台
热更新能力是大家选择 RN 的一个重要原因之一,有了热更新能力,就相当于在用户手机和公司业务之间铺设了一道直达的高速公路,公司新业务开发后不再受限于 App 发版审核下载更新这个流程了。那么如何设计一个热更新平台呢?
业界主流的有:
- Code Push是微软 App Center 的服务之一。底层是微软自家的 Azure 云服务。由于国内网络环境的原因,访问国外云服务较慢,不推荐使用
- Expo是亚马逊的 AWS 和 Google Cloud 云服务。由于国内网络环境的原因,访问国外云服务较慢,不推荐使用
- Pushy是 React Native 中文网提供的热更新方案使用的是国内的阿里云服务且比前2者有更省流量的增量更新方案。也是国内可直接使用的开源热更新方案之一了。
- 自研:灵活自由度高,可控
热更新方案主要包括2部分打包服务 + 静态资源服务。
打包服务核心就是将 React Native 项目中的 JS 代码打包成一个 Bundle 文件。静态资源服务就是将 Bundle 文件分发给客户端的服务。客户端拿到 Bundle 文件后,就可以渲染展示了。
1. 通过 react-native bundle 命令,提前把 JS 代码打包成一个 Bundle 文件,本质上是一个可执行的 JavaScript 文件。
```shell
npx react-native bundle --entry-file index.js --dev false --minify false --bundle-output ./build/index.bundle --assets-dest ./build
```
2. 如果使用的是 Hermes还需要把 Javascript 文件转成相应的字节码文件。Hermes 提供了方案
```
hermes -emit-binary -out ./build/index.hbc ./build/index.bundle
```
转换后会得到一个 `.hbc` 字节码包hbc 也就是 hermes bytecode。
得到 Bundle 文件后就需要上传到 CDN 上了。但 CDN 存在一个问题,就是 CDN 资源地址是固定不变的,所以不够灵活,**存在几分钟的更新延迟问题**,假设线上出现一个重大故障,需要等几分钟才可以完全回滚,这对于公司形象、用户损失都会产生很大影响。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ReactNativeBundleCDNIssue.png" style="zoom:40%">
上图中旧版本的 JS Bundle 包是绿色的,新版本 JS Bundle 是蓝色的。在旧版本覆盖新版本的过程中:删除 CDN 缓存的旧版本资源,当 CDN 没有缓存了,这时候用户新的请求才不会命中缓存,而是到 OSS 拉取最新资源。
然而 CDN 不是单点计算机,而是分布在不同位置的网络节点,当使用 CDN 刷新能力时,实际上就是删除上千个节点的缓存。搞过 CDN 的人说要把这成千个节点的缓存删除干净最长需要5分钟同时无法保证这5分钟的时效。
也就是说这5分钟内请求 JS Bundle 存在3种情况
- 命中老版缓存
- 未命中缓存,从 OSS 拉取新的资源
- 命中新版缓存
也就是说在享受 CDN 的同时也要接受这5分钟渐进式更新的延迟也就是说要有5分钟内用户可能全部访问有问题的业务 JS Bundle 的预见性。
有没有改进措施?
在端上设备和 CDN 之间再架设一层版本服务。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ReactNativeBundleServer.png" style="zoom:40%">
具体步骤:
1. 本地打包好的 JS Bundle 根据文件内容生成 MD5然后命名对应的 JS Bundle 文件,格式为 `{MD5}.bundle` ,保证唯一性,然后上传 Bundle 到 OSS。
2. 发布上线,比如一个可视化平台点击上线按钮,要做的事情就是告诉版本服务,当前业务 JS Bundle 版本为 0.01JS Bundle 名为 `s2d8...j07.bundle`
3. 端上发起请求,请求版本服务,版本服务根据信息返回对应的 Bundle 名。`{uri: s2d8...j07.bundle}`
4. 端上再次发起真正的 CDN 资源请求。资源请求会先询问某个 CDN 边缘节点,如果边缘节点没有缓存,则去源站拉取资源;如果边缘节点有缓存,则直接返回
## 三、RN 启动速度优化
### 3.1 Hermes
Hermes 是 FaceBook 2019 年中旬开源的一款 JS 引擎,从 **release[1]** 记录可以看出,这个是专为 React Native 打造的 JS 引擎,可以说从设计之初就是为 Hybrid UI 系统打造。
Hermes **支持直接加载字节码**,也就是说,`Babel`、`Minify`、`Parse` 和 `Compile` 这些流程全部都在开发者电脑上完成,直接下发字节码让 Hermes 运行就行,这样做可以**省去 JSEngine 解析编译 JavaScript 的流程**JS 代码的加载速度将会大大加快,启动速度也会有非常大的提升。
### 3.2 减小 JS Bundle 体积
前面的优化其实都是 Native 层的优化,从这里开始就进入 Web 前端最熟悉的领域了。
其实谈到 JS Bundle 的优化,来来回回就是那么几条路:
- **缩**:缩小 Bundle 的总体积,减少 JS 加载和解析的时间
- **延**动态导入dynamic import懒加载按需加载延迟执行
- **拆**:拆分公共模块和业务模块,避免公共模块重复引入
如果有 webpack 打包优化经验的小伙伴,看到上面的优化方式,是不是脑海中已经浮现出 webpack 的一些配置项了?不过 React Native 的打包工具不是 webpack 而是 Facebook 自研的 Metro虽然配置细节不一样但道理是相通的下面我就这几个点讲讲 React Native 如何优化 JS Bundle。
Metro 打包 JS 时,会把 ESM 模块转为 CommonJS 模块,这就导致现在比较火的依赖于 ESM 的 Tree Shaking 完全不起作用而且根据官方回复Metro 未来也不会支持 Tree Shaking
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RNWillNotSupportTreeShaking.jpeg" style="zoom:40%">
说人话就是 Tree-Shaking 现在不搞了,现在在搞更有前途的方式来保证 bundle 体积的减少:
#### 1. 使用 react-native-bundle-visualizer 查看包体积
使用 react-native-bundle-visualizer 查看包体积。优化 bundle 文件前,一定要知道 bundle 里有些什么最好的方式就是用可视化的方式把所有的依赖包列出来。web 开发中,可以借助 Webpack 的 `webpack-bundle-analyzer` 插件查看 bundle 的依赖大小分布React Native 也有类似的工具,可以借助 RN`react-native-bundle-visualizer` 查看依赖关系:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RNDemoMoudleDependcencyVisualizer.jpeg" style="zoom:40%">
#### 2. 对于同样的功能,优先选择体积更小的第三方库
这是一个非常经典的例子。同样是时间格式化的第三方库, moment.js 体积 200 KBday.js 体积只有 2KB而且 API 与 moment.js 保持一致。如果项目里用了 moment.js替换为 day.js 后可以立马减少 JSBundle 的体积。
**利用 babel 插件,避免全量引用**
lodash 基本上属于 Web 前端的工程标配了,但是对于大多数人来说,对于 lodash 封装的近 300 个函数,只会用常用的几个,例如 `get`、 `chunk`,为了这几个函数全量引用还是有些浪费的。
社区上面对这种场景,当然也有优化方案,比如说 `lodash-es`,以 ESM 的形式导出函数,再借助 Webpack 等工具的 Tree Sharking 优化就可以只保留引用的文件。但是就如前面所说React Native 的打包工具 Metro 不支持 Tree Shaking所以对于 `lodash-es` 文件,其实还会全量引入,而且 `lodash-es` 的全量文件比 `lodash` 要大得多。
做了个简单的测试,对于一个刚刚初始化的 React Native 应用,全量引入 lodash 后,包体积增大了 71.23KB,全量引入 `lodash-es` 后,包体积会扩大 173.85KB。
`lodash-es ` 太大了,能不能在 lodash 上做文章?
```js
// 全量
import { join } from 'lodash'
// 局部
import join from 'lodash/join'
```
这样打包就只会打 `lodash/join` 这一个文件,但这严格依赖于团队小伙伴的共识,不能严格保证。且使用 lodash 的七八个方法,就需要 import 七八次,这很低效。有个 `babel-plugin-lodash` 插件,可以在 JS 编译时操作 AST 做如下转换:
```js
import { join, chunk } from 'lodash'
// 转换为
import join from 'lodash/join'
import chunk from 'lodash/chunk'
```
终极大杀器:**`babel-plugin-import`** 基本可以解决所有按需引用的问题
如何使用?
有个 ahooks 开源库,封装了很多常用的 React hooks但问题是这个库是针对 Web 平台封装的,比如说 `useTitle` 这个 hook是用来设置网页标题的但是 React Native 平台是没有相关的 BOM API 的,所以这个 hooks 完全没有必要引入RN 也永远用不到这个 API。
这时候我们就可以用 `babel-plugin-import` 实现按需引用了,假设我们只要用到 `useInterval` 这个 Hooks我们现在业务代码中引入
```javascript
import { useInterval } from 'ahooks'
```
然后运行 `yarn add babel-plugin-import -D` 安装插件,在 `babel.config.js` 文件里启用插件:
```
// babel.config.js
module.exports = {
plugins: [
[
'import',
{
libraryName: 'ahooks',
camel2DashComponentName: false, // 是否需要驼峰转短线
camel2UnderlineComponentName: false, // 是否需要驼峰转下划线
},
],
],
presets: ['module:metro-react-native-babel-preset'],
};
```
启用后就可以实现 ahooks 的按需引入
```js
import { useInterval } from 'ahooks'
// 等价于
import useInterval from 'ahooks/lib/useInterval'
```
| 全量 ahooks | ahooks/lib/useInterval 单文件引用 | ahooks + babel-plugin-import |
| :---------- | :-------------------------------- | :--------------------------- |
| 111.41 KiB | 443 Bytes | 443 Bytes |
#### 3. 制定编码规范,减少重复代码
##### 移除 console
`babel-plugin-transform-remove-console` 插件,我们可以配置它在打包发布的时候移除 `console` 语句,减小包体积的同时还会加快 JS 运行速度,我们只要安装后再简单的配置一下就好了:
```js
// babel.config.js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
env: {
production: {
plugins: ['transform-remove-console'],
},
},
};
```
##### 制定良好的编码规范
- 代码的抽象和复用:代码中重复的逻辑根据可复用程度,尽量抽象为一个方法,不要用一次复制一次
- 删除无效的逻辑:这个也很常见,随着业务的迭代,很多代码都不会用了,如果某个功能下线了,就直接删掉,哪天要用到再从 git 记录里找
- 删除冗余的样式:例如引入 ESLint plugin for React Native开启 `"react-native/no-unused-styles"` 选项,借助 ESLint 提示无效的样式文件
### 3.3 Inline Requires
懒执行。一般情况下 RN 容器初始化之后就会加载全量的 JS Bundle 文件,而 Inline Requires 延迟运行,只有需要使用的时候才会执行 JS 代码而不是启动的时候就执行RN 0.64 版本,默认开启。需要在 `metro.config.js` 中进行修改
```js
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const fs = require('fs');
const path = require('path');
const config = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
```
比如说我们写了个工具函数 `join` 放在 `utils.js` 文件里:
```js
// utils.js
export function join(list, j) {
return list.join(j);
}
```
```js
// App.js
import { join } from 'my-module';
const App = (props) => {
const result = join(['a', 'b', 'c'], '~');
return <Text>{result}</Text>;
};
```
被 Metro 编译后
```js
const App = (props) => {
const result = require('./utils').join(['a', 'b', 'c'], '~');
return <Text>{result}</Text>;
};
```
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RNInlineRequireDemo1.png" style="zoom:40%">
`r()` 代表 `require()` 函数,可以看到在顶部的 import 实际上被替换成在实际使用的位置 import 了。
然而 Metro 的 import 是不支持 export default 的。需要注意,可以具体查看 [RN 官方文档](https://reactnative.dev/docs/ram-bundles-inline-requires)
### 3.4 RN 拆包
假设业务 A 和 B 的业务代码是通过 JS Bundle 动态下发的:
- Business A JS Bundle共300KB其中 200KB 基础包React、RN、100KB 业务代码
- Business B JS Bundle共400KB其中 200KB 基础包React、RN、200KB 业务代码
访问完业务 A 之后再去访问业务 B下载好300KB 代码后需要继续下载400KB 代码。存在冗余,完全没有必要多次下载和加载,这时候一个想法自然而然就出来了:
> 能否把一些共有库打包到一个 `common.bundle` 文件里,我们每次只要动态下发业务包 `businessA.bundle` 和 `businessB.bundle`,然后在客户端实现先加载 `common.bundle` 文件,再加载 `business.bundle` 文件就可以了
这样做的好处有几个:
- `common.bundle` 可以直接放在本地,省去多业务线的多次下载,**节省流量和带宽**
- 可以在 RN 容器预初始化的时候就加载 `common.bundle` **二次加载的业务包体积更小,初始化速度更快**
顺着上面的思路,上面问题就会转换为两个小问题:
- 如何实现 JSBundle 的拆包?
- iOS/Android 的 RN 容器如何实现多 bundle 加载?
RN 本地调试还是构建,底层都是用到了 Metro 打包工具的能力。然而 Facebook 的 Metro 本身没有拆包能力,只能将 JS 代码打包成一个 Bundle 文件,且 Metro 不支持三方插件。在查看 Metro 源代码的时候发现了一个令人眼前一亮的方法 `customSerializer` ,可以实现不侵入修改 Metro 源码,通过配置的方式给 Metro 写第三方插件的能力
#### 3.4.1 拆包原理
为什么选择基于模块拆包而不是基于文本?因为基于模块拆包,加载速度会更快。
为什么基于模块的拆包方式能够独立运行,而基于文本的拆包方式不能独立运行?
来做一些说明,架设采用的是多 Bundle 基于文本的拆包方式,多个 Bundle 之间的公共代码是 React、React Native 库,用 `console.log('react')`、`console.log('react native')` 代替。多个 Bundle 不同的部分是业务代码,用 `console.log('foo')` 代表业务代码。
基于文本拆包一般采用 Google 开源的 [diff-match-patch](https://github.com/google/diff-match-patch?tab=readme-ov-file) 算法。repo 主页也提供了 Demo 入口,可以在线体验效果
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ReactNativeBundlePatchDemo.png" style="zoom:40%">
实际上,在基于文本计算热更新包的场景下,我们会内置 Old version这部分代码除了升级 RN 版本外不会改动,而 New Version 的字符串是本次热更新的目标代码,但为了传输效率不需要下载完成的 Bundle 文件,因为 Old Version 已经内置了,基于 diff-match-patch 计算出需要热更新的部分即可。
客户端拉取到需要 Patch 热更新包后,会和 Old Version 代表的内置包进行合并,合并的结果就是 New Version 所代表的完整的 Bundle
很显然Patch 热更新是一段记录修改位置、修改内容的文本,而不是一段可单独运行的代码。会导致内置包没法提前执行,只能等下在完成再合并,生成完整的 Bundle 文件后,作为整体才执行。这就是为什么基于文本的拆包方式不能独立运行的原因。
引入正题,基于模块的拆包方式,内置包和热更新包就可以分别独立运行了。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ReactNativeBundlePatch.png" style="zoom:40%">
可以看到基于模块的拆包方案,拆出来热更新包是一个业务代码,单独可运行。所以可以在客户端先运行内置包,然后下载热更新包,等热更新包下载完成,再运行热更新包。
#### 3.4.2 Bundle 文件结构及内容说明
React Native打包形成的Bundle文件的内容从上到下依次是
- Polyfills定义基本的JS环境`__d()`函数、`__r()`函数、`__DEV__` 变量等)
- Module定义使用`__d()`函数定义所有用到的模块该函数为每个模块赋予了一个模块ID模块之间的依赖关系都是通过这个ID进行关联的。
- Require调用使用`__r()`函数引用根模块。
业务不同的2个 Bundle 但是会在 Polyfills 部分和 Module 定义的部分有大量重复,因为都包含 React、React Native 2个模块代码重复部分大约500K 左右
`-d` 函数实际上就是 `define()` 函数3个参数分别为factory 方法、moudleID、dependencyMap
```js
function define(factory, moduleId, dependencyMap) {
if (moduleId in modules) {
// that are already loaded
return;
}
modules[moduleId] = { dependencyMap};
// other code ....
};
```
`_r` 函数实际上就是 `require()` 函数,这个方法首先判断所有要加载的模块是否已经存在并完成了初始化。如果是,则直接返回模块的 exports如果不是则调用 `guardedLoadModule` 方法来完成模块的初始化
```js
function require(moduleId) {
const module = modules[moduleId];
return module && module.isInitialized
? module.exports
: guardedLoadModule(moduleIdReallyIsNumber, module);
}
function guardedLoadModule(moduleId, module) {
return loadModuleImplementation(moduleId, module);
}
function loadModuleImplementation(moduleId, module) {
module.isInitialized = true;
const exports = (module.exports = {});
var _module = module;
const factory = _module.factory,
dependencyMap = _module.dependencyMap;
const moduleObject = { exports };
factory(global, require, moduleObject, exports, dependencyMap);
return (module.exports = moduleObject.exports);
}
```
#### 3.4.3 公共资源包
随着 RN 版本迭代官方已经逐步将bundle文件生成流程规范化并为此设计了独立的打包模块 Metro。Metro 通过输入一个需要打包的JS文件及几个配置参数返回一个包含了所有依赖内容的JS文件。
Metro将打包的过程分为了3个依次执行的阶段
1. **解析Resolution**:计算得到所有的依赖模块,形成依赖树,该过程是多线程并行执行。
2. **转义Transformation**:代码的编译转换,该过程是多线程并行执行。
3. **序列化Serialization**:所有代码转换完毕后,打印转换后的代码,生成一个或者多个 bundle 文件
Metro工具提供了配置功能开发人员可以通过配置RN项目中的**metro.config.js**文件修改bundle文件的生成流程。
可以看到,我们需要关注 Metro Serialization 阶段,只要借助 `Serialization` 暴露的各个方法就可以实现 bundle 分包了。主要是 `createModuleIdFactory(path)` 方法和 `processModuleFilter(module)` 。
`createModuleIdFactory(path)`是传入的模块绝对路径`path`,并为该模块返回一个唯一的`Id`。`processModuleFilter(module)`则可以实现对模块进行过滤使其不被写入到最后的bundle文件中。
官方的 `createModuleIdFactory` 内部实现是返回一个数字,该数字在 require 方法中被调用,以此来实现模块的导入和初始化
```js
"use strict";
function createModuleIdFactory() {
const fileToIdMap = new Map();
let nextId = 0;
return path => {
let id = fileToIdMap.get(path);
if (typeof id !== "number") {
id = nextId++;
fileToIdMap.set(path, id);
}
return id;
};
}
```
官方默认的实现存在一个问题,就是业务代码改动后,重新打包,由于 moduleID 是从0开始自增分配可能会存在前后2次构建中 moduleID 发生改变。
针对官方实现,可以重新自定义 `createModuleIdFactory(path)` 方法,该方法根据当前模块文件的路径哈希值作为分配 moduleID 的依据,并建立哈希值和模块 ID 的对应关系保存到本地文件缓存中,每次编译 Bundle 先读取本地缓存文件来初始化内存缓存,当需要分配 ID 的时候,先从缓存内部查找,找不到则重新分配 ID 并存储变化。
```js
// metro.common.config.js
/**
* Metro configuration
* https://facebook.github.io/metro/docs/configuration
*
* @type {import('metro-config').MetroConfig}
*/
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const fs = require('fs');
const path = require('path');
const config = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
serializer: {
createModuleIdFactory: function () {
//获取命令行执行的目录__dirname是nodejs提供的变量
const projectRootPath = __dirname;
return (path) => {
let name = '';
// 如果需要去除react-native/Libraries路径去除可以放开下面代码
// if (path.indexOf('node_modules' + pathSep + 'react-native' + pathSep + 'Libraries' + pathSep) > 0) {
// //这里是react native 自带的库,因其一般不会改变路径,所以可直接截取最后的文件名称
// name = path.substr(path.lastIndexOf(pathSep) + 1);
// }
if (path.indexOf(projectRootPath) == 0) {
/*
这里是react native 自带库以外的其他库,因是绝对路径,带有设备信息,
为了避免重复名称,可以保留node_modules直至结尾
如/{User}/{username}/{userdir}/node_modules/xxx.js 需要将设备信息截掉
*/
name = path.substr(projectRootPath.length + 1);
}
//js png字符串 文件的后缀名可以去掉
// name = name.replace('.js', '');
// name = name.replace('.png', '');
//最后在将斜杠替换为下划线
let regExp = pathSep == '\\' ? new RegExp('\\\\', "gm") : new RegExp(pathSep, "gm");
name = name.replace(regExp, '_');
//名称加密
if (isEncrypt) {
name = md5(name);
}
fs.appendFileSync('./idList.txt', `${name}\n`);
return name;
};
},
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
```
同时,为了能够在`processModuleFilter(module)`方法中对模块进行过滤需要在构建Common文件时标记某个模块是否已包含在Common文件中。
打 common 包:`npx react-native bundle --platform ios --config metro.common.config.js --dev false --entry-file common.js --bundle-output='./ios/common.ios.bundle'`
#### 3.4.4 业务资源包
```js
// metro.business.config.js
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
/**
* Metro configuration
* https://facebook.github.io/metro/docs/configuration
*
* @type {import('metro-config').MetroConfig}
*/
const fs = require('fs');
const path = require('path');
const idList = fs.readFileSync('./idList.txt', 'utf8').toString().split('\n');
function createModuleId(path) {
// 和上面生成 moduleID 方法一样
return moduleId;
}
const config = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
serializer: {
createModuleIdFactory: function () {
return function (path) {
return createModuleId(path);
};
},
processModuleFilter: function (modules) {
const mouduleId = createModuleId(modules.path);
// 通过 mouduleId 过滤在 common.bundle 里的数据
if (idList.indexOf(mouduleId) < 0) {
console.log('createModuleIdFactory path', mouduleId);
return true;
}
return false;
},
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
```
此时打业务包 `npx react-native bundle --platform ios --config metro.business.config.js --dev false --entry-file index.js --bundle-output='./ios/businessA.ios.bundle'`
其中几个业务,就需要几个业务入口,也就需要打几次包。当然可以再次基础上包装一层,比如读取配置文件。
#### 3.4.5 客户端加载
以 iOS 为例,加载基础包
```objective-c
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"rnCodeSplitDemo"
initialProperties:nil];
if (@available(iOS 13.0, *)) {
rootView.backgroundColor = [UIColor systemBackgroundColor];
} else {
rootView.backgroundColor = [UIColor whiteColor];
}
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [[NSBundle mainBundle] URLForResource:@"common.ios"" withExtension:@"bundle"];
}
```
加载业务包
为 `RCTCxxBridge` 添加分类 `RCTCxxBridge+RunBundleJS`
```objective-c
#import <React/RCTBridge+Private.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTCxxBridge (RunBundleJS)
- (void)executeSourceCode:(NSData *)sourceCode withSourceURL:(NSURL *)url sync:(BOOL)sync;
@end
NS_ASSUME_NONNULL_END
```
```objective-c
NSString *businessBundle = [[NSBundle mainBundle] pathForResource:@"business.ios.jsbundle" ofType:nil];
NSData *businessData = [NSData dataWithContentsOfFile:businessBundle options:NSDataReadingMappedIfSafe error:nil];
[(RCTCxxBridge *)bridge.batchedBridge executeSourceCode:businessData sync:YES];
```
## 学习方面
学过 React.js 之后你再去学习 React Native 会很简单,一些核心的东西理解之后会很简单。比如 React 中的单向数据流、虚拟 Dom、diff 算法、数据变动的批量更新机制、diff 之后的 UI 渲染。