React Native 拆包實(shí)踐3 - react-native bundle

下面我們來看看react-native bundle的實(shí)現(xiàn)结执,bundle也是react-native的一個(gè)子命令荚虚,和start(ps: 實(shí)際在代碼里是server)同級(jí):

// cli/packages/cli/src/commands/index.ts
export const projectCommands = [
  server,
  bundle,
  ...
] as Command[];

跳入bundle的實(shí)現(xiàn)件豌,在下面的代碼中給出了注釋,主要的流程可以總結(jié)為:解析參數(shù) -> 啟動(dòng)metro server -> 打包js和資源文件,并保存于磁盤指定路徑 -> 停止metro server

// cli/packages/cli/src/commands/bundle/buildBundle.ts
async function buildBundle(
  args: CommandLineArgs,
  ctx: Config,
  output: typeof outputBundle = outputBundle,
) {
  const config = await loadMetroConfig(ctx, {
    maxWorkers: args.maxWorkers,
    resetCache: args.resetCache,
    config: args.config,
  });
  // 判斷打包的平臺(tái)是ios,android或是web患久,如果都不是,則拋出異常浑槽,結(jié)束打包
  if (config.resolver.platforms.indexOf(args.platform) === -1) {
    ...
    throw new Error('Bundling failed');
  }

  // This is used by a bazillion of npm modules we don't control so we don't
  // have other choice than defining it as an env variable here.
  process.env.NODE_ENV = args.dev ? 'development' : 'production';
  // 根據(jù)命令行的入?yún)?--sourcemap-output 構(gòu)建 sourceMapUrl
  // --sourcemap-output: File name where to store the sourcemap file for resulting bundle
  let sourceMapUrl = args.sourcemapOutput;
  if (sourceMapUrl && !args.sourcemapUseAbsolutePath) {
    sourceMapUrl = path.basename(sourceMapUrl);
  }
  // 根據(jù)解析得到參數(shù)蒋失,構(gòu)建RequestOptions,傳遞給打包函數(shù)
  const requestOpts: RequestOptions = {
    entryFile: args.entryFile,  // 入口文件桐玻,也就是react-native生成模板工程的index.js
    sourceMapUrl,
    dev: args.dev, // 生產(chǎn)環(huán)境還是開發(fā)環(huán)境
    minify: args.minify !== undefined ? args.minify : !args.dev, // 是否壓縮生成的jsbundle
    platform: args.platform,
  };

  const server = new Server(config);

  try {
    // 開始打包
    const bundle = await output.build(server, requestOpts);
    // 將打包生成的bundle存儲(chǔ)在--bundle-output指定的位置
    await output.save(bundle, args, logger.info);
    // 處理資源文件篙挽,解析,并在下一步保存在--assets-dest指定的位置
    const outputAssets: AssetData[] = await server.getAssets({
      ...Server.DEFAULT_BUNDLE_OPTIONS,
      ...requestOpts,
      bundleType: 'todo',
    });
    return await saveAssets(outputAssets, args.platform, args.assetsDest);
  } finally {
    server.end(); // 所有工作處理后镊靴,即可停止server
  }
}

sourceMapUrl?
從上述代碼可以看到具體的打包實(shí)現(xiàn)都在output.build(server, requestOpts)中铣卡,output是outputBundle類型,這部分代碼在Metro JS中偏竟,具體的路徑為:metro/packages/metro/src/shared/output/bundle.js

...
function buildBundle(
  packagerClient: Server,
  requestOptions: RequestOptions,
): Promise<{
  code: string,
  map: string,
  ...
}> {
  return packagerClient.build({
    ...Server.DEFAULT_BUNDLE_OPTIONS,
    ...requestOptions,
    bundleType: 'bundle',
  });
}
...
exports.build = buildBundle;

可以看到這里的packagerClient就是從外面?zhèn)魅氲膕erver煮落,就是Metro Server,加了這一層封裝的目的是為了在requestOptions中添加一個(gè)bundleType參數(shù)踊谋,值為'bundle'蝉仇。Server.build的源碼則在metro/packages/metro/src/Server.js中定義:

  async build(options: BundleOptions): Promise<{code: string, map: string, ...}> {
    const {
      entryFile,
      graphOptions,
      onProgress,
      serializerOptions,
      transformOptions,
    } = splitBundleOptions(options);
    // Resolution和Transformation
    const {prepend, graph} = await this._bundler.buildGraph(
      entryFile,
      transformOptions,
      {
        onProgress,
        shallow: graphOptions.shallow,
      },
    );
    // 開始 Serialization
    const entryPoint = path.resolve(this._config.projectRoot, entryFile);
    const bundleOptions = {
      asyncRequireModulePath: this._config.transformer.asyncRequireModulePath,
      processModuleFilter: this._config.serializer.processModuleFilter,
      createModuleId: this._createModuleId,
      getRunModuleStatement: this._config.serializer.getRunModuleStatement,
      dev: transformOptions.dev,
      projectRoot: this._config.projectRoot,
      modulesOnly: serializerOptions.modulesOnly,
      runBeforeMainModule: this._config.serializer.getModulesRunBeforeMainModule(
        path.relative(this._config.projectRoot, entryPoint),
      ),
      runModule: serializerOptions.runModule,
      sourceMapUrl: serializerOptions.sourceMapUrl,
      sourceUrl: serializerOptions.sourceUrl,
      inlineSourceMap: serializerOptions.inlineSourceMap,
    };
    let bundleCode = null;
    let bundleMap = null;
    if (this._config.serializer.customSerializer) {
      const bundle = this._config.serializer.customSerializer(
        entryPoint,
        prepend,
        graph,
        bundleOptions,
      );
      if (typeof bundle === 'string') {
        bundleCode = bundle;
      } else {
        bundleCode = bundle.code;
        bundleMap = bundle.map;
      }
    } else {
      bundleCode = bundleToString(
        baseJSBundle(entryPoint, prepend, graph, bundleOptions),
      ).code;
    }
    if (!bundleMap) {
      bundleMap = sourceMapString(
        [...prepend, ...this._getSortedModules(graph)],
        {
          excludeSource: serializerOptions.excludeSource,
          processModuleFilter: this._config.serializer.processModuleFilter,
        },
      );
    }
    return {
      code: bundleCode,
      map: bundleMap,
    };
  }

在這個(gè)build函數(shù)中,首先執(zhí)行了buildGraph,而this._bundler的初始化發(fā)生在Server的constructor中量淌。

  constructor(config: ConfigT, options?: ServerOptions) {
    ...
    this._createModuleId = config.serializer.createModuleIdFactory();
    this._bundler = new IncrementalBundler(config, {
      watch: options ? options.watch : undefined,
    });
    this._nextBundleBuildID = 1;
  }

此處的_bundlerIncrementalBundler的實(shí)例骗村,它的buildGraph函數(shù)完成了打包過程中前兩步Resolution和Transformation⊙绞啵可以跳入定義metro/packages/metro/src/IncrementalBundler.js查看它的完整實(shí)現(xiàn)胚股。

constructor中除了配置了一些參數(shù)外,初始化了_bundler裙秋,還有我們之后再拆包中一個(gè)核心的函數(shù)createModuleIdFactory琅拌,它是從config.serializer中獲取的。

createModuleIdFactory負(fù)責(zé)固定 module 的 ID摘刑。在打包好的jsbundle中进宝,__d中定義的各個(gè) module 后都有一個(gè)數(shù)字表示,并在jsbundle文件最后的 require 方法中進(jìn)行調(diào)用(如 require(41))枷恕,這其中的數(shù)字就是createModuleIdFactory方法生成的党晋。

完成了Resolution和Transformation后,便是Serialization了徐块。首先創(chuàng)建了一個(gè)bundleOptions未玻,包含了所有Serialization中需要用到的參數(shù),其中這兩個(gè)是我們的重點(diǎn):

processModuleFilter: this._config.serializer.processModuleFilter,
createModuleId: this._createModuleId,

this._createModuleId在初始化時(shí)已經(jīng)創(chuàng)建好了胡控,另一個(gè)processModuleFilter扳剿,它是用于過濾Transformation的結(jié)果,決定哪些module可以被序列化到最終輸出的jsbundle里昼激。它是我們拆包的另一個(gè)核心函數(shù)庇绽。

接下來,通過判斷有沒有自定義的Serializer橙困,決定由誰來序列化瞧掺。當(dāng)沒有自定一個(gè)serializer時(shí),將才有自帶的bundleToString函數(shù)進(jìn)行序列化凡傅,序列化前夸盟,根據(jù)上一步結(jié)果通過baseJSBundle構(gòu)建一個(gè)Bundle對(duì)象。
bundleToString的入?yún)⑹荁undle像捶,根據(jù)baseJSBundle的返回值我們可以知道Bundle的內(nèi)部結(jié)構(gòu)是這樣的:

{
  pre: string,
  post: string,
  modules: [[number, string]],
}

其中最主要的是這個(gè)modules,看似是一個(gè)二維數(shù)組桩砰,其實(shí)可以理解為Tuple的數(shù)組拓春,一個(gè)Tuple代表了一個(gè)js module,number是它的id亚隅,這個(gè)id是由createModuleId產(chǎn)生的硼莽,string就是這個(gè)module的js代碼的序列化結(jié)果。

有了這個(gè)認(rèn)識(shí)后,bundleToString的工作就是很明顯很多懂鸵,sortedModules是將js module干id進(jìn)行了一個(gè)排序偏螺。再遍歷這個(gè)數(shù)組,將module code進(jìn)行了一個(gè)字符串拼接匆光,拼接到code變量上套像。code上還同時(shí)拼接了pre和post。

function bundleToString(
  bundle: Bundle,
): {|+code: string, +metadata: BundleMetadata|} {
  let code = bundle.pre.length > 0 ? bundle.pre + '\n' : '';
  const modules = [];
  const sortedModules = bundle.modules
    .slice()
    .sort((a: [number, string], b: [number, string]) => a[0] - b[0]);

  for (const [id, moduleCode] of sortedModules) {
    if (moduleCode.length > 0) {
      code += moduleCode + '\n';
    }
    modules.push([id, moduleCode.length]);
  }

  if (bundle.post.length > 0) {
    code += bundle.post;
  } else {
    code = code.slice(0, -1);
  }

  return {
    code,
    metadata: {pre: bundle.pre.length, post: bundle.post.length, modules},
  };
}

接下來终息,我們可以看看baseJSBundle的實(shí)現(xiàn)夺巩。有了對(duì)bundleToString的理解,這部分代碼就很好懂了周崭,其中調(diào)用了三次processModules函數(shù)柳譬,分別用于生產(chǎn)preCode,postCode续镇,以及modules美澳。

function baseJSBundle(
  entryPoint: string,
  preModules: $ReadOnlyArray<Module<>>,
  graph: Graph<>,
  options: SerializerOptions,
): Bundle {
  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 = [];
  }

  const preCode = processModules(preModules, processModulesOptions)
    .map(([_, code]) => code)
    .join('\n');

  const modules = [...graph.dependencies.values()].sort(
    (a: Module<MixedOutput>, b: Module<MixedOutput>) =>
      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,
        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]),
  };
}

processModules中,主要的作用就是一個(gè)filter摸航,進(jìn)行了兩次filter制跟,第一次過濾出所有的js module;第二次filter是有外面?zhèn)魅氲膄ilter決定的忙厌。

function processModules(
  modules: $ReadOnlyArray<Module<>>,
  {
    filter = () => true,
    createModuleId,
    dev,
    projectRoot,
  }: {|
    +filter?: (module: Module<>) => boolean,
    +createModuleId: string => number,
    +dev: boolean,
    +projectRoot: string,
  |},
): $ReadOnlyArray<[Module<>, string]> {
  return [...modules]
    .filter(isJsModule)
    .filter(filter)
    .map((module: Module<>) => [
      module,
      wrapModule(module, {
        createModuleId,
        dev,
        projectRoot,
      }),
    ]);
}

總結(jié)

到此我們已經(jīng)完整了梳理了一遍bundle的執(zhí)行過程凫岖,也從中了解到我們最關(guān)注的兩個(gè)函數(shù)createModuleIdFactoryprocessModuleFilter是何時(shí)被觸發(fā)的。下一節(jié)中逢净,我們就一起來實(shí)現(xiàn)自己的createModuleIdFactoryprocessModuleFilter哥放。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市爹土,隨后出現(xiàn)的幾起案子甥雕,更是在濱河造成了極大的恐慌,老刑警劉巖胀茵,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件社露,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡琼娘,警方通過查閱死者的電腦和手機(jī)峭弟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脱拼,“玉大人瞒瘸,你說我怎么就攤上這事∠ㄅǎ” “怎么了情臭?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我俯在,道長竟秫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任跷乐,我火速辦了婚禮肥败,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘劈猿。我一直安慰自己拙吉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布揪荣。 她就那樣靜靜地躺著筷黔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仗颈。 梳的紋絲不亂的頭發(fā)上佛舱,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音挨决,去河邊找鬼请祖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛脖祈,可吹牛的內(nèi)容都是我干的肆捕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼盖高,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼慎陵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起喻奥,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤席纽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后撞蚕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體润梯,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年甥厦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纺铭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡刀疙,死狀恐怖舶赔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情庙洼,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站油够,受9級(jí)特大地震影響蚁袭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜石咬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一揩悄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鬼悠,春花似錦删性、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至它掂,卻和暖如春巴帮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背虐秋。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工榕茧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人客给。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓用押,卻偏偏與公主長得像,于是被迫代替她去往敵國和親靶剑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蜻拨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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