前言
最开始思考拆包时,总是认为操作新项目(目录结构更灵活)和操作旧项目(目录结构不能轻易动,否则影响其他人的开发)是两回事,但后续做着做着发现,其实差别并不太大,都要设置入口文件,都要做文件的筛选。下面就记录一下碰到的一些问题。
指定入口文件
入口文件的硬性要求有两个:
- 基础模块仅仅包含所有的基础代码
- 业务模块仅仅包含对应的业务代码
咋一看,这不说的是废话么,且听我娓娓道来
基础模块仅仅包含所有的基础代码
我们要打基础模块,首先要想办法让基础模块的入口文件,引用到所有的基础模块文件,但由于基础模块一般都是在业务模块里面引用的,这就造成脱离业务模块,基础模块无法打包。因此我们首先要考虑怎么让所有基础模块的入口文件引用到所有基础模块文件。对此有两种方式:
- 在基础模块里通过脚本引入所有的基础模块代码
如果你能确保所有的基础模块代码都是有规律的放在某些目录里,这种方案很棒,但如果你操作的是老项目,这个方案就行不通了,毕竟老项目的目录不总会那么规律。 - 打包之前通过脚本搜集业务模块所引用的所有基础文件的绝对路径,然后在基础模块中引用
这个方式是我们目前使用的,等拆完包我们会统一整理目录,那时或许会考虑第一种方案。
方案确定了,接下来就要组织代码实现了,最开始的想法比较单纯,想着找到RN或webpack的解析(resolver)模块直接使用,但单独使用时发现会出现各种问题(文档也不全),后续又想基于AST自己搞也失败了,最后实在没办法了,就老老实实的回归正则。下面贴上代码:有更好的方案麻烦指教一二。
/*
* 解析出某个文件的所有引用
* 1. 使用babel将代码转化成ES5,所有的引入都会变成require,这样正则就不用兼容的匹配import
* 2. 拿到第一步的结果通过正则匹配到所有的require内容
* 3. 第三方库直接返回库名,否则转化成绝对路径
* 4. 最后返回数组,包含着所有引用文件的绝对路径
*/
const path = require('path');
const fs = require('fs');
const _require = require('@babel/core');
const transformSync = _require.transformSync;
function parse(inputPath) {
let inputPathArray = inputPath.split('/');
inputPathArray.pop();
const cdPath = inputPathArray.join('/');
const source = fs.readFileSync(inputPath).toString();
let babel = transformSync(source).code;
let array = babel.match(/require\(["'](\S*?)["']\)/gi);
array = array
.map(res => {
return res.match(/require\(["'](?<href>\S*?)["']\)/i).groups.href;
})
.filter(res => {
// 打包时用到的工具,release不要
return res.indexOf('@babel') === -1;
})
.map(res => {
if (res.indexOf('./') === -1) {
return res;
}
return path.resolve(cdPath, res);
});
return array;
}
module.exports = parse;
这是上面所提到方案的核心,其他的细节在此便不多说了,如有兴趣可以私信我。
业务模块仅仅包含对应的业务代码
其实思路跟基础模块一致,就是将该模块涉及到的文件在入口文件引入一下即可,这可不仅仅包括路由页面哦,还有所有只属于该模块的子组件或工具类,这种就没法通过脚本操作只能自己一个一个添加,毕竟情况还是比较复杂的。
通过拆包可以减少初次加载bundle的时间,减少用户等待增加体验,因此对于要放在基础包的内容要有选择性的
打包配置
RN打包配置核心就两项,processModuleFilter
和createModuleIdFactory
,更多配置请移步metro-Configuration
processModuleFilter
这个方法有两种理解:
- 本模块里添加指定文件:只关心相应的业务文件,就是对应业务模块
再打业务包时,只需要关心在设计业务模块的入口文件时统计的业务模块的引用,其他都过滤掉 - 过滤掉跟本模块无关的文件:过滤掉所有业务文件,就是基础文件
我们定义基础包的入口文件时已经获取了所有业务模块的引用,但这些引用中有些是某些业务模块所特有的,不需要放在基础包中,因此打基础包时要过滤掉这些
如果你的项目是新项目,并且目录结构设计完美,就可以直接通过文件夹过滤了,会更方便些。
createModuleIdFactory
用于为require
语句生成模块id,同bridge环境下id不可重复。在打整个包时该方法可以不指定,默认是从0累加。但当拆包时由于默认都是从0开始,id会造成重复。所以在拆包时该方法必须指定,在此提供一个思路:根据路径生成。
另外建议该模块的id不可过长,因为该id也会被打到bundle里面,过长的话会造成bundle体积增大。
拆包如何调试
各种操作拆完包后,突然有个问题,怎么调试呢?起初还想着怎么让Native在初始化时直接加载全部bundle。但后来突然想明白,拆包的本质就是通过设置多个入口文件将代码给分割,那调试的时候我们直接将入口文件都在放在index.js里不就行了么。这样就实现了跟RN单包一样的调试
路由表
拆包跟路由有关的也就是路由表了,Native收到要跳转的路由时首先在路由表中查询,查到所在的bundle后加载bundle在做后续跳转。至于我们路由表的内容就补贴出了,按照自己习惯的风格来吧。