前言
本文是基于react-native 0.55, react 16.3.1版本展开
目的
- 减少业务包体积(app瘦身)
- 节省热更新流量
- 提升模块加载速度
实现方式
- 打通用包,包括react-native框架、code-push框架、常用第三方框架、rnlib代码等
- 打全量bundle包
- 全量包基于通用包打出差异包(业务包)
- 通用包和业务包同时引入工程
- 原生端预先加载通用包,当进入rn页面只加载业务包
- code-push分别更新通用包和业务包
jsbundle解析(全量包)
var
__DEV__ = false,
__BUNDLE_START_TIME__ = this.nativePerformanceNow ? nativePerformanceNow() : Date.now(),
process = this.process || {};
process.env=process.env || {};
process.env.NODE_ENV = "production";
!(function(r) {
"use strict";
r.__r = o,
....
})();
__d(function(r,o,t,i,n){t.exports=r.ErrorUtils},18,[]);
...
...
__d(function(c,e,t,a,n){var r=e(n[0]);r(r.S,'Object',{create:e(n[1])})},506,[431,460]);
require(46);
require(11);
大致分为四个部分
- var 声明的变量,对当前运行环境的定义,bundle 的启动时间、Process进程环境相关信息;
- (function() { })() 闭包中定义的代码块,其中定义了对 define(__d)、 require(__r)、clear(__c) 的支持,以及 module(react-native及第三方dependences依赖的module) 的加载逻辑;
- __d 定义的代码块,包括RN框架源码 js 部分、自定义js代码部分、图片资源信息,供 require 引入使用;
- require定义的代码块,找到 __d 定义的代码块并执行,其中require中的数字即为 __d定义行中最后出现的那个数字。
如果每个业务都单独打全量包,那第一、二部分和大量第三部分代码将会重复,因此我们需要提取这部分代码做为通用部分,common.jsbundle,业务包将会复用这些代码
使用metro拆包
在上文的jsbundle解析中,__d中定义的各个module后都有一个数字表示,并在最后的require方法中进行调用(如require(41)),这其中的数字就是metro项目中createModuleIdFactory方法生成的(node_modules/metro/src/lib/createModuleIdFactory.js);如果添加了module,那么ID会重新生成,如果要做一个基础包,那么公共module的ID必须是固定的,因此0.56+版本的RN可以通过此方法的接口来将module的ID固定,0.52~0.55的RN依赖的metro也用到了这个方法,只是没暴露出来,可以通过修改源码的方式来实现0.56+版本相同的效果
源码改动如下
function createModuleIdFactory() {
if (process.env.NODE_ENV != 'production') { // debug模式
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;
};
} else { // 生产打包使用具体包路径代替id,方便拆包处理
// 定义项目根目录路径
const projectRootPath = `${process.cwd()}`;
// path 为模块路径名称
return path => {
let moduleName = '';
if (path.indexOf('node_modules\\react-native\\Libraries\\') > 0) {
moduleName = path.substr(path.lastIndexOf('\\') + 1);
} else if (path.indexOf(projectRootPath) == 0) {
moduleName = path.substr(projectRootPath.length + 1);
}
moduleName = moduleName.replace('.js', '');
moduleName = moduleName.replace('.png', '');
moduleName = moduleName.replace('.jpg', '');
moduleName = moduleName.replace(/\\/g, '_'); // 适配Windows平台路径问题
moduleName = moduleName.replace(/\//g, '_'); // 适配macos平台路径问题
return moduleName;
};
}
} module.exports = createModuleIdFactory;
从上述源码也可以看出,系统使用整数型的方式,从0开始遍历所有模块,并依次使 Id 增加 1。所以我们可以修改此处逻辑,以模块路径名称的方式作为Id即可。
注意:这里我加了一个process.env.NODE_ENV的判断,即在开发模式下依旧使用原模式(因为我发现全局改的话开发模式会运行失败)
基础包和业务包打包
打包之前,需要我们分别定义好基础模块与业务模块文件,核心代码如下:
// base.js
require('react-native');
require('react');
require('@bwt/bwt-navigation');
require('./rnlib/RNKit');
require('./rnlib/UIKit');
import CodePush from "@bwt/bwt-code-push";
...//这里可以引入更多的第三方模块及自己的公共模块
// index_{{BundleName}}.js
import App_{{ModuleName}} from './App_{{ModuleName}}';
AppRegistry.registerComponent("{{ModuleName}}", () => App_{{ModuleName}});
注:base.js为基础模块入口,index_{{BundleName}}.js为业务业务模块入口
接下来就是通过react-native bundle命令来进行打包了,需要两个不同的命令,区别在于打包入口文件参数(–entry-file)不一样:
base.js入口打包
react-native bundle --platform ios --dev false --entry-file base.js --bundle-output $projectPath/ios/bundle/common/common.jsbundle --assets-dest $projectPath/ios/bundle/common/ --dev false
输出common.jsbundle
index_{{BundleName}}.js入口打包
react-native bundle --platform ios --dev false --entry-file index_{{BundleName}}.js --bundle-output $projectPath/ios/bundle/business.jsbundle --assets-dest $projectPath/ios/bundle/$jsbundleName/ --dev false
输出business.jsbundle
差异包打包
business.jsbundle基于common.jsbundle打差异包,实现思路:
- business.jsbundle逐行扫描
- 扫描内容如在common.jsbundle中没找到,用数组存放
- 将数组转换为数据保存到差异包patch.jsbundle
结论:求出两个文件的差集,且只包含business.jsbundle的代码
//$1:common.jsbundle $2:business.jsbundle $3:patch.jsbundle
sort $2 $1 $1 | uniq -u > $3 #回写
输出patch.jsbundle作为最终业务包,引入到工程中
总结
我们利用bundle的结构拆分出common.jsbundle和patch.jsbundle,common.jsbundle只需要引入到工程一次,就可以被复用;当原生运行时加载模块的时候有几种方案:
- 进入页面时动态合并成全量包后显示
- 预加载common.jsbundle,进入页面时只加载patch.jsbundle后显示,RN加载代码容器使用单例
- 预加载common.jsbundle,进入页面时加载patch.jsbundle,同时用另一个代码容器预加载common.jsbundle,当下次进入页面时使用;此方法使代码容器生命周期各自独立
本文讲述的是bundle文件拆包方案,那么原生端应该如何管理呢?且看下篇,原生多bundle加载方案,RN拆包解决方案(二) bundle加载