携程CRN源码详解之拆包(三)——增量编译
1、增量编译
增量编译在这里指的是:打过一次js包后,后面再打包就会基于上次的打包基础打包。这里的基础指的是moduleId的map。crn-cli使用的是数字来代表module,rn_common包生成的文件都是0.js 1.js 2.js 3.js.... 业务包生成的文件都是666666.js 666667.js ..... ,而这些数字在rn中并不是固定的,比如在一次打包中rn_common包的288.js文件代表的是Button.js,这个过程中我们在RN框架代码中新增或者删除一个module,下次打包的时候rn_common包时288.js代表的是Alert.js。在业务包中是以id来require 模块的,这就会导致本来引用的是Button,却变成了Alert,这个时候就需要把所有的业务包全部重新打包,这对于携程有上百个业务包的情况是非常不友好的。增量编译能让之前已经打包的模块id保持不变,因此crn-cli必须支持增量编译。
这里要吐槽以下,这个理论上将不能算增量编译,因为js代码本来就没有编译一说,应该叫做增量打包。我话讲完,
2、原理很简单
如果需要增量打包,那就把每次打包新增的module和id对应关系保存到一个文件,下次打包的时候再根据这个文件取出id复用。
//baseMapping.json
[
{
"id": 0,
"path": "/crn_common_entry.js"
},
{
"id": 1,
"path": "/node_modules/@babel/runtime/helpers/interopRequireWildcard.js"
},
{
"id": 2,
"path": "/node_modules/@babel/runtime/helpers/interopRequireDefault.js"
},
{
"id": 3,
"path": "/node_modules/@babel/runtime/helpers/classCallCheck.js"
},
... ... ...
这里的文件就是rn-common包下的map文件,对应着模块路径和id
看看处理moduleId的源码
//crn-createModuleIdFactory.js
function createModuleIdFactory() {
if(mappingContainer.length == 0){
var baseMappingPath = path.join(process.cwd(), "bundle_output","baseMapping.json")
if(fs.existsSync(baseMappingPath)){//1<--
mappingContainer = require(baseMappingPath);
nextModuleId.rn = mappingContainer[mappingContainer.length-1].id;
}
}
if(mappingContainerBU.length == 0 && !global.CRN_BUILD_COMMON){
var buMappingPath = path.join(process.cwd(), "bundle_output","buMapping.json")
if(fs.existsSync(buMappingPath)){
mappingContainerBU = require(buMappingPath);
nextModuleId.business = mappingContainerBU[mappingContainerBU.length-1].id;
}
}
return path => {
var oldModule = [];
path = path.replace(process.cwd(), "");
if (mappingContainer && mappingContainer.length > 0) {
oldModule = mappingContainer.filter(element => {//2<--
return path === element.path;
});
}
var oldBUModule = [];
if (mappingContainerBU && mappingContainerBU.length > 0) {
oldBUModule = mappingContainerBU.filter(element => {
return path === element.path;
});
}
if (oldModule && oldModule.length > 0) {
return oldModule[0].id;//3<--
} else if (oldBUModule && oldBUModule.length > 0) {
return oldBUModule[0].id;
} else {
var nID = generateId();//4<--
var enterModule = {
id: nID,
path: path
};
if (!global.CRN_BUILD_COMMON) {
if (... ...) {
mappingContainerBU.push(enterModule);
}
} else {
mappingContainer.push(enterModule);//5<--
}
return nID;
}
};
}
这里个函数中一起处理common包和buz包的moduleId,两者逻辑一摸一样,就挑一个讲即可,我把步骤分成了5步:
1、读取map文件,并取出这个文件里最后的id(也是最大的ID),如果有新的模块那这个模块的id=maxId+1
2、根据传入的模块路径找id
3、如果在map文件中找到了id,就使用这个旧id
4、如果没找到就最大id+1
5、把这个模块id信息保存下来,打包完后会保存mappingContainer到map文件
3、携程的做法
查看携程app打包后的业务包,许多业务包的ID都是666666开头的,而携程官方也说了90%共用一个js运行环境,业务包id重合也就意味着一个业务的模块在其他业务下无法被使用,id覆盖后之前的业务状态也可能会被覆盖,因此如果crn的业务包之间需要交互的情况下会变得有一点别扭。而且,如果在页面退出后业务代码还在后台运行的话,这种情况会非常危险,很可能爆出找不到module或找错module的情况。如果只是使用这个开源工程的话是没有业务包间协作的功能的,所以我猜测携程自己应该写了业务包交互的中间件。而如果使用react-native-multibundler开源项目就不会有这个问题,这个还是要看业务,如果业务之间不耦合交互也是可有无,去哪儿的QRN使用的是和react-native-multibundler一样的文件名当moduleId的做法,期待QRN的开源。
4、后续
到现在已经把crn-cli了解了差不多一半了
-
打包支持框架和业务代码拆分 支持框架代码后台预加载打包支持增量编译(同一模块,两次打包模块ID不变)- iOS&Android统一一套打包产物
- 首屏加载性能统计
- LazyRequire