开发过 React-native
的同学们应该都知道,客户端运行RN代码,主要是运行RN打包后生成的 bundle
文件,以及一些静态图片资源。本文将从这两点展开优化。
我们先来了解下React-native
的打包工具
Metro:
Metro 是官方推荐的 React-native 的打包工具,借助 Metro, 我们能够在编译过程中,通过一些方式来优化编译输出的bundle
文件
大概的打包流程如图:
图中我们能看到,有三个阶段我们能干预:
1、Resolution
文件解析:从指定入口文件分析依赖,生成一个有依赖关系的图谱
2、Transformation
文件转换:通过babel
将React-native
代码编译成客户端可以执行的代码片段
3、Serialization
序列化输出:这一阶段主要是根据前两个阶段的执行结果,生成一个或者多个 bundle 文件
Metor
默认会将所有依赖打包成一个bundle
,全量内置在客户端。
这样做的好处是:全量内置,页面启动速度快
但是缺点也很明显:前端资源更新,重度依赖客户端,如果线上出现严重bug,只有发布新版本才能解决
以上我们得出结论
1、将一些稳定的、不常更新的第三方包(react, react-native, redux等等)单独打包,内置到客户端
2、将不同的业务分开打包,动态下发不同的业务包,达到热更新的效果
而我们的分包操作主要是依赖 Serialization
提供的两个方法createModuleIdFactory
processModuleFilter
来实现
公共包实现
我们将公共模块单独打包,将模块打上标签
公共包的createModuleIdFactory
代码如下
const commonModulesMap = {};
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);
// 记录模块的路径跟Id
const modulePath = path.relative(process.cwd(), path);
commonModulesMap[modulePath] = id;
fs.writeFileSync('./commonModulesMap.json', JSON.stringify(commonModulesMap))
}
return id;
};
}
我们新建common.js
,引入第三方依赖。
import "react";
import "react-native";
新建metro.base.config.js
,引入createModuleIdFactory
module.exports = {
serializer: {
createModuleIdFactory
}
};
新建bundles
目录
mkdir bundles
执行打包命令
react-native bundle --platform android --dev false --entry-file common.js --bundle-output bundles/common.android.bundle --assets-dest bundles --config ./metro.base.config.js --reset-cache
打包后我们看到commonModulesMap.json
文件,每个依赖都打上了数字标签。
{
"common-entry.js": 1,
"node_modules/react/index.js": 2,
"node_modules/react/cjs/react.production.min.js": 3,
"node_modules/object-assign/index.js": 4,
"node_modules/@babel/runtime/helpers/extends.js": 5,
"node_modules/react-native/index.js": 6
}
业务包实现
业务包如下,公共模块复用公共包当中的Id, 业务包随机数Id自增
const commonModulesMap = require('./commonModulesMap.json');
function createModuleIdFactory() {
let nextId = +`${+new Date()}`.slice(-9);
const fileToIdMap = new Map();
return (path) => {
// 公共包的Id
const modulePath = path.relative(process.cwd(), path);
if (commonModulesMap[modulePath]) {
return commonModulesMap[modulePath];
}
// 业务包的Id
let id = fileToIdMap.get(path);
if (typeof id !== 'number') {
id = nextId + 1;
nextId = nextId + 1;
fileToIdMap.set(path, id);
}
return id;
};
}
打业务包的时候,如果模块在公共包已经存在,就返回false
,不将其打包在内。
processModuleFilter
代码如下
function processModuleFilter() {
return (module) => {
const { path } = module;
const modulePath = path.relative(process.cwd(), path);
if (
path.indexOf('__prelude__') !== -1 ||
path.indexOf('/node_modules/react-native/Libraries/polyfills') !== -1 ||
path.indexOf('source-map') !== -1 ||
path.indexOf('/node_modules/metro-runtime/src/polyfills/require.js') !== -1
) {
return false;
}
if (commonModulesMap[modulePath]) {
return false;
}
return true;
};
}
新建metro.business.config.js
,引入createModuleIdFactory
以及processModuleFilter
module.exports = {
serializer: {
createModuleIdFactory,
processModuleFilter
}
};
编译业务包,这里以一个业务包为例,多个业务包循环编译即可
react-native bundle --platform android --dev false --entry-file index.js --bundle-output bundles/business.android.bundle --assets-dest bundles --config ./metro.business.config.js --reset-cache
通过以上策略,业务包代码压缩后的体积大幅减少了,如何更细粒度减少下发包体积呢
如何减小下发包体积
补丁式更新 diff_match_patch
1、diff 补丁
const diffMatchPatch = require('diff-match-patch')
const dmp = new diffMatchPatch();
const pre = "线上运行的资源包"
const next = "新资源包"
const diff = dmp.diff_main(pre, next)
const patches = dmp.patch_make(diff)
patches
就是补丁内容,压缩之后只有几kb大小
2、客户端下载补丁并更新
const diffMatchPatch = require('diff-match-patch')
const dmp = new diffMatchPatch()
const pre = "线上运行的资源包"
const patches = "下发的补丁文件"
const result = dmp.patch_apply(patches, pre)
result
就是我们打好补丁的最新资源包。
静态图片上CDN
默认做法是静态图片全量内置在客户端
优点:加载速度快
缺点:下发包体积增大 我们可以选择将图片上传到CDN,在减少下发包体积的同时,CDN也帮助我们更快的加载图片资源
有两种方式可以做
1、React-native
Image 组件提供了动态更改图片路径的方法 setCustomSourceTransformer
我们在访问时动态改变图片路径,达到访问CDN的目的
Image.resolveAssetSource.setCustomSourceTransformer((resolver) => {
// CDN 地址
const cdnPath = 'https://******';
resolver.jsbundleUrl = cdnPath;
let resolveAsset;
if (Platform.OS === "android") {
resolveAsset = resolver.drawableFolderInBundle();
} else {
resolveAsset = resolver.scaledAssetURLNearBundle();
}
return resolveAsset;
});
2、将本地地址替换为远程地址
React-native
加载图片的两种方式:
<Image source="require(本地图片地址)" />
<Image source="{uri: cdn地址}" />
我们只需要在打包前将图片上传CDN,生成本地路径对应的CDN地址的Map文件,动态的将 require(本地图片地址)
替换为 {uri: cdn地址}
即可
总结
1、我们通过将第三方包抽离成单独的公共包,又将不同的业务拆分成独立的业务包
2、为了更细粒度的实现热更新,我们又增加了打补丁的方式去更新业务包
3、通过图片上CDN,减小了下发包体积