# 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 资源地址是固定不变的,所以不够灵活,**存在几分钟的更新延迟问题**,假设线上出现一个重大故障,需要等几分钟才可以完全回滚,这对于公司形象、用户损失都会产生很大影响。 上图中旧版本的 JS Bundle 包是绿色的,新版本 JS Bundle 是蓝色的。在旧版本覆盖新版本的过程中:删除 CDN 缓存的旧版本资源,当 CDN 没有缓存了,这时候用户新的请求才不会命中缓存,而是到 OSS 拉取最新资源。 然而 CDN 不是单点计算机,而是分布在不同位置的网络节点,当使用 CDN 刷新能力时,实际上就是删除上千个节点的缓存。搞过 CDN 的人说,要把这成千个节点的缓存删除干净,最长需要5分钟,同时无法保证这5分钟的时效。 也就是说这5分钟内请求 JS Bundle 存在3种情况: - 命中老版缓存 - 未命中缓存,从 OSS 拉取新的资源 - 命中新版缓存 也就是说在享受 CDN 的同时,也要接受这5分钟渐进式更新的延迟,也就是说要有5分钟内用户可能全部访问有问题的业务 JS Bundle 的预见性。 有没有改进措施? 在端上设备和 CDN 之间再架设一层版本服务。 具体步骤: 1. 本地打包好的 JS Bundle 根据文件内容生成 MD5,然后命名对应的 JS Bundle 文件,格式为 `{MD5}.bundle` ,保证唯一性,然后上传 Bundle 到 OSS。 2. 发布上线,比如一个可视化平台点击上线按钮,要做的事情就是告诉版本服务,当前业务 JS Bundle 版本为 0.01,JS 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 说人话就是 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` 查看依赖关系: #### 2. 对于同样的功能,优先选择体积更小的第三方库 这是一个非常经典的例子。同样是时间格式化的第三方库, moment.js 体积 200 KB,day.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 {result}; }; ``` 被 Metro 编译后 ```js const App = (props) => { const result = require('./utils').join(['a', 'b', 'c'], '~'); return {result}; }; ``` `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 入口,可以在线体验效果 实际上,在基于文本计算热更新包的场景下,我们会内置 Old version,这部分代码除了升级 RN 版本外不会改动,而 New Version 的字符串是本次热更新的目标代码,但为了传输效率不需要下载完成的 Bundle 文件,因为 Old Version 已经内置了,基于 diff-match-patch 计算出需要热更新的部分即可。 客户端拉取到需要 Patch 热更新包后,会和 Old Version 代表的内置包进行合并,合并的结果就是 New Version 所代表的完整的 Bundle 很显然,Patch 热更新是一段记录修改位置、修改内容的文本,而不是一段可单独运行的代码。会导致内置包没法提前执行,只能等下在完成再合并,生成完整的 Bundle 文件后,作为整体才执行。这就是为什么基于文本的拆包方式不能独立运行的原因。 引入正题,基于模块的拆包方式,内置包和热更新包就可以分别独立运行了。 可以看到基于模块的拆包方案,拆出来热更新包是一个业务代码,单独可运行。所以可以在客户端先运行内置包,然后下载热更新包,等热更新包下载完成,再运行热更新包。 #### 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 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 渲染。 样式布局方面增加了 flexbox,这样子布局在移动端会非常方便,非常简单, ## 经验小结 最近在做企业内部工具,在提 MR 进行 Code review 的时候,reviewer 提了这样一个问题。将 `{this.renderOverview()}` 改为 `` 这种写法,界面上更加直观些。我觉得公用组件或者页面这样做是可以的,但是页面上的某个部分 UI,其他页面不需要用到,所以不需要抽取出来。所以这个 code review 我没接受,跟她讲了一番,我维持了现状。 背景是这样的,React + Redux 实现界面编写,Redux 负责状态的管理,页面一般是需要负责渲染和交互的,所以在代码编写上通过 Redux 的 `@connect` 将 state 绑定到当前页面的 `props` 上,因此界面的展示全部由 props 完成。页面的交互逻辑由各个组件内部 `dispatch` `action` 到 `reducer` 中进行运算。运算后的结果继续以 props 绑定到当前的页面上。代码如下 ```Javascript @connect(({ skynet }) => ({ ...skynet, }), dispatch => ({ getXXXLogDetail (***) { return dispatch({ type: ListPre('***'), payload: *** }) } })) export default class Detail extends Component { state = { showDispatchPanel: false } get id() { const { match } = this.props return match.params.id } componentDidMount() { const { getSkyLogDetail } = this.props getXXXLogDetail({ groupHash: this.id}) } s() { const { skyLogDetail: { groupDTO = {} } } = this.props // ... return (
//***
) } render() { return (
{this.renderOverview()} {/* ... */}
) } } ``` 和 Reviewer 聊过后,她好像自己对这些东西不是很熟悉,比如 `` 这种形式需要改造为纯函数,且如果纯函数不能通过这种小写的方式去写,不然 React 在内部渲染会认为是一个**html 标签**。效果如下 ![渲染为标签](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/20200205-ReactPureComponent) 最佳实践就是:自定义组件大写开头,小写开头会被认为是 html 标签。createElement(ComponentVariable),createElement('renderOverview')。只是渲染,不需要内部改变 state 的话,纯函数非常适合做 UI 渲染,状态的事情由 redux 解决,最后通过 props 处理。