拆包原理
由于metro向开发者提供了Serialization的相关函数,我们便可以改写其默认实现,从而实现过滤我们不需要的模块的目的。
拆包重点1--模块ID
拆包第1个问题是确定基础包有哪些模块。我们是把React-native框架本身和一些必要的第三方库打到基础包里面,在此之外的模块都属于业务包,因此打业务包时需要参考基础包包含了哪些模块。
我们在序列化生成bundle的过程的过程中会调用到baseJSBundle这个函数,他主要是遍历依赖图的所有模块,根据每一个模块的路径生成ID,然后根据ID的大小排序,再通过processModules过滤掉不用的模块并包装module,最终生成bundle.我们来看下baseJSBundle的代码:
function baseJSBundle(entryPoint, preModules, graph, options) {
for (const module of graph.dependencies.values()) {
options.createModuleId(module.path);
}
const processModulesOptions = {
filter: options.processModuleFilter,
createModuleId: options.createModuleId,
dev: options.dev,
projectRoot: options.projectRoot,
}; // Do not prepend polyfills or the require runtime when only modules are requested
if (options.modulesOnly) {
preModules = [];
}
//这里传入的processModuleOptions.filter就是processModuleFilter
const preCode = processModules(preModules, processModulesOptions)
.map(([_, code]) => code)
.join("\n");
//这里创建moduleId并根据ID大小来对module排序
const modules = [...graph.dependencies.values()].sort(
(a, b) => options.createModuleId(a.path) - options.createModuleId(b.path)
);
const postCode = processModules(
getAppendScripts(
entryPoint,
[...preModules, ...modules],
graph.importBundleNames,
{
asyncRequireModulePath: options.asyncRequireModulePath,
createModuleId: options.createModuleId,
getRunModuleStatement: options.getRunModuleStatement,
inlineSourceMap: options.inlineSourceMap,
projectRoot: options.projectRoot,
runBeforeMainModule: options.runBeforeMainModule,
runModule: options.runModule,
serverRoot: options.serverRoot,
sourceMapUrl: options.sourceMapUrl,
sourceUrl: options.sourceUrl,
}
),
processModulesOptions
)
.map(([_, code]) => code)
.join("\n");
return {
pre: preCode,
post: postCode,
modules: processModules(
[...graph.dependencies.values()],
processModulesOptions
).map(([module, code]) => [options.createModuleId(module.path), code]),
};
}
这里面用到了createModuleId来确定moduleId,这个createModuleId是options的一个属性,对应的是Server.js的_createModuleId属性,而_createModuleId又是通过createModuleIdFactory方法执行后返回的函数。metroServer在初始化时,会读入开发者传入的配置,其中就包括了我们前面提到的createModuleIdFactory.
class Server {
constructor(config, options) {
this._config = config;
this._serverOptions = options;
if (this._config.resetCache) {
this._config.cacheStores.forEach((store) => store.clear());
this._config.reporter.update({
type: "transform_cache_reset",
});
}
this._reporter = config.reporter;
this._logger = Logger;
this._platforms = new Set(this._config.resolver.platforms);
this._isEnded = false; // TODO(T34760917): These two properties should eventually be instantiated
// elsewhere and passed as parameters, since they are also needed by
// the HmrServer.
// The whole bundling/serializing logic should follow as well.
this._createModuleId = config.serializer.createModuleIdFactory();//这里使用我们提供的createModuleIdFactory得到_createmoduleId函数
this._bundler = new IncrementalBundler(config, {
hasReducedPerformance: options && options.hasReducedPerformance,
watch: options ? options.watch : undefined,
});
this._nextBundleBuildID = 1;
}
//...省略无关代码
}
createModuleIdFactory的默认实现在defaultCreateModuleIdFactory.js中
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是每次调用createModuleId时去fileToIdMap获取,如果拿到的id不是number类型(比如说undefined类型),则会在上一次生成的ID基础上+1,这样可以保证每个模块的id是唯一的。但是对于拆包来说,基础包和业务包是分开打的,每次moduleId都是从0开始自增,这样基础包和业务包里面就有很多模块的ID是一样的,导致无法根据moduleId过滤,所以我们需要把moduleId固定下来。
这是我们重写的代码:
function createModuleIdFactory() {
console.log("-----------node dir", projectRootPath);
return path => {
const name = getModuleId(projectRootPath, path, entry, false, moduleMapDir);
platformNameArray.push(name);
const platformMapDir = projectRootPath + pathSep + moduleMapDir;
if (!fs.existsSync(platformMapDir)) {
fs.mkdirSync(platformMapDir);
}
const platformMapPath = platformMapDir + pathSep + platformMapName;
fs.writeFileSync(platformMapPath, JSON.stringify(platformNameArray));
return name;
};
}
这里的getModuleId会根据包的类型,分别在不同的路径下区查找map文件(map文件是用来记录moduleId和路径、版本的映射关系的)来得到moduleId,这样保证每次打包moduleId都是固定的,以基础包loader为例,获取moduleId的方法如下:
// 获取loader的ModuleId
function getLoaderModuleIdByIndex(projectRootPath, path) {
const pathRelative = getRelativePath(projectRootPath, path, false);
const findPlatformItem = baseModuleIdMap.find(value => {
return value.path === pathRelative;
});
if (findPlatformItem) {
return findPlatformItem.id;
} else {
//基础包
curModuleId = ++curModuleId;
const version = getLibVersion(projectRootPath, pathRelative);
baseModuleIdMap.push({ id: curModuleId, path: pathRelative, v: version });
fs.writeFileSync(baseMappingPath, JSON.stringify(baseModuleIdMap));
return curModuleId;
}
}
这里提一下为什么要把版本号考虑进来,看过网上很多方案,都是只考虑了文件路径,但是没有考虑版本,其实同一个模块不同版本是很有可能不兼容的,像RN从0.59升级到0.60以上版本,就是一个很大的变化,当然这种情况我们肯定是要根据APP版本下发不同bundle的,但对于其他的第三方库我们升级时就有可能没考虑到这一点了。
拆包重点2--过滤基础包的模块
打业务包的时候,需要过滤掉基础包所包含的模块,关键就在于processModulesFilter这个函数,根据传入的module返回true来保留模块,或者返回false来过滤模块。前面提到processModules过滤了一些模块,我们来看下代码
function processModules(
modules,
{ filter = () => true, createModuleId, dev, projectRoot }
) {
return [...modules]
.filter(isJsModule)
.filter(filter)//过滤模块
.map((module) => [
module,
wrapModule(module, {
createModuleId,
dev,
projectRoot,
}),
]);
}
这里的第二个filter函数的参数就是processModulesFilter,我们实际项目拆包定义的processModulesFilter函数如下
function postProcessModulesFilter(module) {
if (platformModules === null || platformModules.length === 0) {
console.log("请先打基础包");
process.exit(1);
return false;
}
const path = module["path"];
if (
path.indexOf("__prelude__") >= 0 ||
path.indexOf("/node_modules/react-native/Libraries/polyfills") >= 0 ||
path.indexOf("source-map") >= 0 ||
path.indexOf("/node_modules/metro/src/lib/polyfills/") >= 0
) {
return false;
}
if (module["path"].indexOf(pathSep + "node_modules" + pathSep) > 0) {
if ("js" + pathSep + "script" + pathSep + "virtual" === module["output"][0]["type"]) {
return true;
}
const name = getModuleId(projectRootPath, path, entry, true, moduleMapDir);
if (platformModules.indexOf(name) >= 0) {
//这个模块在基础包已打好,过滤
return false;
}
}
return true;
}
到这里,RN的拆包基本上就完成了,剩下的主要是划分各个业务包的起始moduleId,避免业务包的moduleId冲突,逻辑比较简单,就不详细介绍了。