react-native metro 分析

前言

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流程

堆棧信息


image.png

可以看到最后調(diào)用到resolve

路徑 metro-resolver/src/resolve.js
方法 resolve

最終輸出是一個(gè)resolution,里面包含兩個(gè)屬性filePath和type


image.png

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末他挎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子捡需,更是在濱河造成了極大的恐慌办桨,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件站辉,死亡現(xiàn)場離奇詭異呢撞,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)饰剥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門殊霞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人捐川,你說我怎么就攤上這事脓鹃∫菁猓” “怎么了古沥?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長娇跟。 經(jīng)常有香客問我岩齿,道長,這世上最難降的妖魔是什么苞俘? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任盹沈,我火速辦了婚禮,結(jié)果婚禮上吃谣,老公的妹妹穿的比我還像新娘乞封。我一直安慰自己,他們只是感情好岗憋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布肃晚。 她就那樣靜靜地躺著,像睡著了一般仔戈。 火紅的嫁衣襯著肌膚如雪关串。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天监徘,我揣著相機(jī)與錄音晋修,去河邊找鬼。 笑死凰盔,一個(gè)胖子當(dāng)著我的面吹牛墓卦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播户敬,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼落剪,長吁一口氣:“原來是場噩夢啊……” “哼溅漾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起著榴,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤添履,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后脑又,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體暮胧,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年问麸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了往衷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡严卖,死狀恐怖席舍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情哮笆,我是刑警寧澤来颤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站稠肘,受9級特大地震影響福铅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜项阴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一滑黔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧环揽,春花似錦略荡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至跨扮,卻和暖如春序无,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背衡创。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工帝嗡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人璃氢。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓哟玷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子巢寡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容