前言
metro是一種支持ReactNative的打包工具粪般,我們現(xiàn)在也是基于他來進(jìn)行拆包的。為了對bundle進(jìn)行進(jìn)一步深入的分析瘟仿,我們就需要深入源碼理解一下RN應(yīng)用metro打包的流程
概念
Metro的捆綁過程分為三個(gè)單獨(dú)的階段:
Resolution
Metro需要從入口點(diǎn)構(gòu)建所需的所有模塊的圖葬燎,要從另一個(gè)文件中找到所需的文件窜醉,需要使用Metro解析器荤胁。在現(xiàn)實(shí)開發(fā)中瞧预,這個(gè)階段與Transformation階段是并行的。
Transformation
所有模塊都要經(jīng)過Transformation階段仅政,Transformation負(fù)責(zé)將模塊轉(zhuǎn)換成目標(biāo)平臺可以理解的格式(如React Naitve)垢油。模塊的轉(zhuǎn)換是基于擁有的核心數(shù)量來進(jìn)行的。
Serialization
所有模塊一經(jīng)轉(zhuǎn)換就會(huì)被序列化圆丹,Serialization會(huì)組合這些模塊來生成一個(gè)或多個(gè)包滩愁,包就是將模塊組合成一個(gè)JavaScript文件的包。
打包方式
Moudles
Metro被劃分為多個(gè)模塊辫封,每個(gè)模塊對應(yīng)于流程中的每個(gè)步驟硝枉,每個(gè)模塊都有自己的職責(zé)廉丽。這意味著我們每個(gè)模塊可以根據(jù)您的需要進(jìn)行交換。
Plain bundle
這是一種標(biāo)準(zhǔn)的打包方式妻味,在這種方式中正压,所有文件都用函數(shù)調(diào)用包裝,然后添加到全局文件中弧可,這對于只需要JS包(例如瀏覽器)的環(huán)境非常有用蔑匣。只需要具有.bundle擴(kuò)展名的入口點(diǎn)就可以完成它的構(gòu)建劣欢。
Indexed RAM bundle
這種打包方式會(huì)將包打包成二進(jìn)制文件棕诵,其格式包括以下部分:
一組數(shù)字:用于驗(yàn)證文件。uint32必須位于文件的開頭凿将,值為0xFB0BD1E5校套。
偏移表:該表是一個(gè)由32對uint32對組成的序列,帶有一個(gè)表頭牧抵。
其他子模塊笛匙,由一個(gè)空字節(jié)(\0)完成。
Indexed RAM bundle通常被用于iOS分包犀变。
File RAM bundle
每個(gè)模塊都會(huì)被存儲(chǔ)為一個(gè)文件妹孙,例如,名稱為js-modules/${id}获枝,創(chuàng)建了一個(gè)名為UNBUNDLE的額外文件蠢正,它唯一的內(nèi)容是一個(gè)數(shù)字0xFB0BD1E5。注意省店,解包文件是在根目錄下創(chuàng)建的嚣崭。
Android通常使用這種方式分包,因?yàn)榘鼉?nèi)容是壓縮的懦傍,而且訪問壓縮文件要快得多雹舀。如果使用索引方式(Indexed RAM bundle),則應(yīng)立即解壓縮所有綁定粗俱,以獲取對應(yīng)模塊的代碼说榆。
流程
前置流程
RN-CLI中首先在'react-native/local-cli/util/Config.js'中配置了metro
--->
執(zhí)行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});
//在這里構(gòu)建 構(gòu)建實(shí)際上是調(diào)用Server.build
const bundle = await output.build(server, requestOpts);
//存儲(chǔ) 在這里輸出
await output.save(bundle, args, log);
resolve流程
堆棧信息
可以看到最后調(diào)用到resolve
路徑 metro-resolver/src/resolve.js
方法 resolve
最終輸出是一個(gè)resolution,里面包含兩個(gè)屬性filePath和type
Transformer流程
基于babel做的三件事
- Parse(解析):將源代碼轉(zhuǎn)換成更加抽象的表示方法(例如抽象語法樹)
- Transform(轉(zhuǎn)換):對(抽象語法樹)做一些特殊處理寸认,讓它符合編譯器的期望
- Generate(代碼生成):將第二步經(jīng)過轉(zhuǎn)換過的(抽象語法樹)生成新的代碼
路徑 metro/src/DeltaBundler/Worker.js
屬性 transform
調(diào)用 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
...
// 轉(zhuǎn)換
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 }] };
})();
}
...
}
這里轉(zhuǎn)換的時(shí)候調(diào)用到了
路徑 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 四個(gè)參數(shù) 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}
執(zhí)行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中根據(jù)createModuleId設(shè)置module的id娱俺,根據(jù)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具有多層緩存废麻,您可以設(shè)置多個(gè)緩存供Metro使用荠卷,而不是一個(gè)緩存。下面來看看Motro的多層緩存是如何工作的烛愧。
為什么要緩存
緩存提供了很大的性能優(yōu)勢油宜,它們可以將打包的速度提高十倍以上掂碱。然而,許多系統(tǒng)使用的是非持久緩存慎冤。對于Metro來說疼燥,我們有一種更復(fù)雜的層系統(tǒng)緩存方式。例如蚁堤,我們可以在服務(wù)器上存儲(chǔ)緩存醉者,這樣,連接到同一服務(wù)器的所有打包都可以使用共享緩存披诗。因此撬即,CI服務(wù)器和本地開發(fā)的初始構(gòu)建時(shí)間顯著降低。
我們希望將緩存存儲(chǔ)在多個(gè)位置呈队,以便緩存可以執(zhí)行回退操作剥槐。這就是為什么有一個(gè)多層緩存系統(tǒng)。
緩存的請求與緩存
在Metro中宪摧,系統(tǒng)使用了一個(gè)排序機(jī)制來決定使用哪個(gè)緩存粒竖。為了檢索緩存,我們從上到下遍歷緩存几于,直到找到結(jié)果蕊苗;為了保存緩存,我們同樣遍歷緩存沿彭,直到找到具有緩存的存儲(chǔ)朽砰。
假設(shè)您有兩個(gè)緩存存儲(chǔ):一個(gè)在服務(wù)器上,另一個(gè)在本地文件系統(tǒng)上膝蜈。那么锅移,你可以這樣指定:
const config = {
cacheStores: [
new FileStore({/*opts*/}),
new NetworkStore({/*opts*/})
]
}
當(dāng)我們檢索緩存時(shí),Metro將首先查看本地文件存儲(chǔ)饱搏,如果不能找到緩存非剃,它將檢查NetworkStore。最后推沸,如果沒有緩存备绽,它將生成一個(gè)新的緩存。一旦緩存生成鬓催,Metro將再次從上到下在所有存儲(chǔ)中存儲(chǔ)緩存肺素。如果找到緩存,也會(huì)進(jìn)行存儲(chǔ)宇驾。例如倍靡,如果Metro在NetworkStore中找到緩存,它也會(huì)將其存儲(chǔ)在FileStore中课舍。
Metro配置
Metro配置可以通過以下三種方式創(chuàng)建:
metro.config.js
metro.config.json
The metro field in package.json
結(jié)構(gòu)
每個(gè)模塊都有一個(gè)單獨(dú)的配置選項(xiàng)塌西,Metro中常見的配置結(jié)構(gòu)如下:
module.exports = {
resolver: {
/* resolver options */
},
transformer: {
/* transformer options */
},
serializer: {
/* serializer options */
},
server: {
/* server options */
}
/* general options */
};
可用的選項(xiàng)參數(shù)可以參考下面的鏈接:
General Options