前言
metro是一种支持ReactNative的打包工具,我们现在也是基于他来进行拆包的。为了对bundle进行进一步深入的分析,我们就需要深入源码理解一下RN应用metro打包的流程
概念
Metro的捆绑过程分为三个单独的阶段:
Resolution
Metro需要从入口点构建所需的所有模块的图,要从另一个文件中找到所需的文件,需要使用Metro解析器。在现实开发中,这个阶段与Transformation阶段是并行的。
Transformation
所有模块都要经过Transformation阶段,Transformation负责将模块转换成目标平台可以理解的格式(如React Naitve)。模块的转换是基于拥有的核心数量来进行的。
Serialization
所有模块一经转换就会被序列化,Serialization会组合这些模块来生成一个或多个包,包就是将模块组合成一个JavaScript文件的包。
打包方式
Moudles
Metro被划分为多个模块,每个模块对应于流程中的每个步骤,每个模块都有自己的职责。这意味着我们每个模块可以根据您的需要进行交换。
Plain bundle
这是一种标准的打包方式,在这种方式中,所有文件都用函数调用包装,然后添加到全局文件中,这对于只需要JS包(例如浏览器)的环境非常有用。只需要具有.bundle扩展名的入口点就可以完成它的构建。
Indexed RAM bundle
这种打包方式会将包打包成二进制文件,其格式包括以下部分:
一组数字:用于验证文件。uint32必须位于文件的开头,值为0xFB0BD1E5。
偏移表:该表是一个由32对uint32对组成的序列,带有一个表头。
其他子模块,由一个空字节(\0)完成。
Indexed RAM bundle通常被用于iOS分包。
File RAM bundle
每个模块都会被存储为一个文件,例如,名称为js-modules/${id},创建了一个名为UNBUNDLE的额外文件,它唯一的内容是一个数字0xFB0BD1E5。注意,解包文件是在根目录下创建的。
Android通常使用这种方式分包,因为包内容是压缩的,而且访问压缩文件要快得多。如果使用索引方式(Indexed RAM bundle),则应立即解压缩所有绑定,以获取对应模块的代码。
流程
前置流程
RN-CLI中首先在'react-native/local-cli/util/Config.js'中配置了metro
--->
执行react-native bundle指令 路径在'react-native/local-cli/bundle/bundle'
--->
路径 react-native/local-cli/cliENtry.js
--->
路径 react-native/local-cli/bundle/bundle.js
方法 bundleWithOutput
--->
路径 react-native/local-cli/bundle/buildBundle.js
引用 const outputBundle = require('metro/src/shared/output/bundle');
方法 buildBundle
const server = new Server({...config, resetCache: args.resetCache});
//在这里构建 构建实际上是调用Server.build
const bundle = await output.build(server, requestOpts);
//存储 在这里输出
await output.save(bundle, args, log);
resolve流程
堆栈信息
可以看到最后调用到resolve
路径 metro-resolver/src/resolve.js
方法 resolve
最终输出是一个resolution,里面包含两个属性filePath和type
Transformer流程
基于babel做的三件事
- Parse(解析):将源代码转换成更加抽象的表示方法(例如抽象语法树)
- Transform(转换):对(抽象语法树)做一些特殊处理,让它符合编译器的期望
- Generate(代码生成):将第二步经过转换过的(抽象语法树)生成新的代码
路径 metro/src/DeltaBundler/Worker.js
属性 transform
调用 transformer.transform
---->
路径 metro/src/JSTransformer/worker.js
class JsTransformer {
constructor(projectRoot, config) {
this._projectRoot = projectRoot;
this._config = config;
}
transform(filename, data, options) {
var _this = this;
return _asyncToGenerator(function*() {
const sourceCode = data.toString("utf8");
let type = "js/module";
...
// 判断类型
...
// $FlowFixMe TODO t26372934 Plugin system
const transformer = require(_this._config.babelTransformerPath);
// 解析
let ast =
transformResult.ast ||
babylon.parse(sourceCode, { sourceType: "module" });
var _generateImportNames = generateImportNames(ast);
const importDefault = _generateImportNames.importDefault,
importAll = _generateImportNames.importAll;
...
// plugins push
...
// 转换
if (type === "js/script") {
dependencies = [];
wrappedAst = JsFileWrapping.wrapPolyfill(ast);
} else {
try {
...
var _JsFileWrapping$wrapM = JsFileWrapping.wrapModule(
ast,
importDefault,
importAll,
dependencyMapName
);
wrappedAst = _JsFileWrapping$wrapM.ast;
}
const reserved =
options.minify && data.length <= _this._config.optimizationSizeLimit
? normalizePseudoglobals(wrappedAst)
: [];
// 生成代码
const result = generate(
wrappedAst,
{
comments: false,
compact: false,
filename,
retainLines: false,
sourceFileName: filename,
sourceMaps: true
},
sourceCode
);
let map = result.rawMappings
? result.rawMappings.map(toSegmentTuple)
: [];
let code = result.code;
...
return { dependencies, output: [{ data: { code, map }, type }] };
})();
}
...
}
这里转换的时候调用到了
路径 metro/src/ModuleGraph/worker/JsFileWrapping.js
方法 wrapModule
function wrapModule(
fileAst,
importDefaultName,
importAllName,
dependencyMapName
) {
const params = buildParameters(
importDefaultName,
importAllName,
dependencyMapName
);
const factory = functionFromProgram(fileAst.program, params);
const def = t.callExpression(t.identifier("__d"), [factory]);
const ast = t.file(t.program([t.expressionStatement(def)]));
const requireName = renameRequires(ast);
return { ast, requireName };
}
序列化流程
--->
metro/Server.js
function build
--->
metro/src/DeltaBundler/Serializers/plainJSBundle.js
function plainJSBundle 四个参数 entryPoint, pre, graph, options
entryPoint
"/Users/haojie/WorkSpace/rnSpace/HouseSeedProject/index.js"
graph
{"dependencies":{},"entryPoints":["/Users/haojie/WorkSpace/rnSpace/HouseSeedProject/index.js"]}
options
{"dev":false,"projectRoot":"/Users/haojie/WorkSpace/rnSpace/HouseSeedProject","runBeforeMainModule":["/Users/haojie/WorkSpace/rnSpace/HouseSeedProject/node_modules/react-native/Libraries/Core/InitializeCore.js"],"runModule":true,"inlineSourceMap":false}
执行server的build方法
//Server.js
build(options) {
var _this2 = this;
return _asyncToGenerator(function*() {
const graphInfo = yield _this2._buildGraph(options);
const entryPoint = getEntryAbsolutePath(
_this2._config,
options.entryFile
);
return {
code: plainJSBundle(entryPoint, graphInfo.prepend, graphInfo.graph, {
processModuleFilter: _this2._config.serializer.processModuleFilter,
createModuleId: _this2._createModuleId,
getRunModuleStatement:
_this2._config.serializer.getRunModuleStatement,
dev: options.dev,
projectRoot: _this2._config.projectRoot,
runBeforeMainModule: _this2._config.serializer.getModulesRunBeforeMainModule(
path.relative(_this2._config.projectRoot, entryPoint)
),
runModule: options.runModule,
sourceMapUrl: options.sourceMapUrl,
inlineSourceMap: options.inlineSourceMap
}),
map: sourceMapString(graphInfo.prepend, graphInfo.graph, {
excludeSource: options.excludeSource,
processModuleFilter: _this2._config.serializer.processModuleFilter
})
};
})();
}
在plainJSBundle中根据createModuleId设置module的id,根据processModuleFilter筛选module然后生成代码。
function plainJSBundle(entryPoint, pre, graph, options) {
for (const module of graph.dependencies.values()) {
options.createModuleId(module.path);
}
return []
.concat(
_toConsumableArray(pre),
_toConsumableArray(graph.dependencies.values()),
_toConsumableArray(getAppendScripts(entryPoint, pre, graph, options))
)
.filter(isJsModule)
.filter(options.processModuleFilter)
.map(module => wrapModule(module, options))
.join("\n");
}
缓存
Metro具有多层缓存,您可以设置多个缓存供Metro使用,而不是一个缓存。下面来看看Motro的多层缓存是如何工作的。
为什么要缓存
缓存提供了很大的性能优势,它们可以将打包的速度提高十倍以上。然而,许多系统使用的是非持久缓存。对于Metro来说,我们有一种更复杂的层系统缓存方式。例如,我们可以在服务器上存储缓存,这样,连接到同一服务器的所有打包都可以使用共享缓存。因此,CI服务器和本地开发的初始构建时间显著降低。
我们希望将缓存存储在多个位置,以便缓存可以执行回退操作。这就是为什么有一个多层缓存系统。
缓存的请求与缓存
在Metro中,系统使用了一个排序机制来决定使用哪个缓存。为了检索缓存,我们从上到下遍历缓存,直到找到结果;为了保存缓存,我们同样遍历缓存,直到找到具有缓存的存储。
假设您有两个缓存存储:一个在服务器上,另一个在本地文件系统上。那么,你可以这样指定:
const config = {
cacheStores: [
new FileStore({/*opts*/}),
new NetworkStore({/*opts*/})
]
}
当我们检索缓存时,Metro将首先查看本地文件存储,如果不能找到缓存,它将检查NetworkStore。最后,如果没有缓存,它将生成一个新的缓存。一旦缓存生成,Metro将再次从上到下在所有存储中存储缓存。如果找到缓存,也会进行存储。例如,如果Metro在NetworkStore中找到缓存,它也会将其存储在FileStore中。
Metro配置
Metro配置可以通过以下三种方式创建:
metro.config.js
metro.config.json
The metro field in package.json
结构
每个模块都有一个单独的配置选项,Metro中常见的配置结构如下:
module.exports = {
resolver: {
/* resolver options */
},
transformer: {
/* transformer options */
},
serializer: {
/* serializer options */
},
server: {
/* server options */
}
/* general options */
};
可用的选项参数可以参考下面的链接:
General Options