React-native 打包流程分析

1,140 阅读6分钟

Metro

众所周知,react-native需要打包成bundle包供AndroidiOS加载,rn默认会使用metro作为打包工具,生成bundle包。 metro 是一个针对 React NativeJavaScript模块打包器,他接收一个entry file (入口文件) 和一些配置作为参数,返回给你一个单独的JavaScript文件,这个文件包含了你写的所有的JavaScript 代码和所有的依赖。

也就是说Metro把你写的几十上百个js文件和几百个node_modules的依赖,打包成了一个文件。

Metro配置

我们在运行npm install 安装react-native依赖的时候其实已经默认安装了Metro只不过可能并不是最新版的,这个跟react-native的版本有关。

metro 配置有三种方法,分别为metro.config.jsmetro.config.jsonpackage.json中添加metro字段,react-naive默认会在根目录有一个metro.config.js如下:

/**
 * Metro configuration for React Native
 * https://github.com/facebook/react-native
 *
 * @format
 */
​
module.exports = {
  resolver: {
    /* resolver options */
  },
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
  serializer: {
    /* serializer options */
  },
  server: {
    /* server options */
  }
};

metro打包bundle文件

  1. 在项目工程中新建一个bundle文件
  2. 在终端执行打包的命令
react-native bundle --entry-file index.js --bundle-output ./bundle/ios.bundle --platform ios --assets-dest ./bundle --dev false
  • platform: 对应的平台, android/ios

  • entry-file: 入口文件

  • bundle-output: 生成的bundle文件输出的位置

  • asset-dest: 图片等资源文件输出的位置

  • sourcemap-output:sourcemap映射文件输出位置

  • config: 额外配置,拆包有用到

    ...

    具体的打包命令可以参考 React-Native 离线bundle

  1. 生成的bundle包大致分为四层:

    • var 声明层: 对当前运行环境, bundle 启动时间,以及进程相关信息
    • polyfill 层: !(function(r){}) , 定义了对 define(__d)require(__r)clear(__c) 的支持,以及 module(react-native 及第三方 dependences 依赖的 module) 的加载逻辑
    • 模块定义层: __d 定义的代码块,包括 RN 框架源码 js 部分、自定义 js 代码部分、图片资源信息,供 require 引入使用
    • require 层: r 定义的代码块,找到 d 定义的代码块 并执行
// var声明层
var __BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now(),__DEV__=false,process=this.process||{},__METRO_GLOBAL_PREFIX__='';process.env=process.env||{};process.env.NODE_ENV=process.env.NODE_ENV||"production";
​
// polyfill层
!(function(r){"use strict";r.__r=o,r.__d=function(r,i,n){if(null!=e[i])return;var o={dependencyMap:n,factory:r,hasError:!1,importedAll:t,importedDefault:t,isInitialized:!1,publicModule:{exports:{}}};e[i]=o}
​
...
  
// 模型定义层
 __d(function(g,r,i,a,m,e,d){var n=r(d[0]),t=r(d[1])(r(d[2]));n.AppRegistry.registerComponent(r(d[3]).name,function(){return t.default})},0,[1,3,401,413]);
      
...
​
// require层
__r(45);
__r(0);
​

metro 打包流程

通过metro官网的介绍,metro打包的流程大致可以分为:

  • 命令参数解析
  • metro打包服务启动
  • 打包js盒资源文件(解析、转化、生产)
  • 停止打包服务
  1. 命令参数解析

    react-native bundle 是react-native的一个子命令,从 react-native/cli.js, @react-native-community/cli/build/commands层层深入,我们可以发现node_modules/@react-native-community/cli-plugin-metro/build/commands/bundle/bundle.js中注册了bundle命令,具体实现是在 buildBundle.js 中。

    var _buildBundle = _interopRequireDefault(require("./buildBundle"));
    ​
    var _bundleCommandLineArgs = _interopRequireDefault(require("./bundleCommandLineArgs"));
    ​
    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
    ​
    function bundleWithOutput(_, config, args, output) 
    {
      // bundle 打包的具体实现
      return (0, _buildBundle.default)(args, config, output);
    }
    ​
    var _default = {
      name: 'bundle',
      description: 'builds the javascript bundle for offline use',
      func: bundleWithOutput,
      options: _bundleCommandLineArgs.default,
      // Used by `ramBundle.js`
      withOutput: bundleWithOutput
    };
    exports.default = _default;
    const withOutput = bundleWithOutput;
    exports.withOutput = withOutput;
    
  2. Metro Server 启动

    node_modules/@react-native-community/cli-plugin-metro/build/commands/bundle/buildBundle.js中默认导出的buildBundle方法才是整个 react-native bundle 执行的入口。主要做了几件事:

    • 合并metro默认配置和自定义配置。
    • 根据解析得到参数,构建 requestoption, 传递给打包函数
    • 实例化metro Server
    • 启动metro 构建 bundle
    • 处理资源文件,解析
    • 关闭 metro server
    // node_modules/@react-native-community/cli-plugin-metro/build/commands/bundle/buildBundle.js
    ​
    // metro 打包服务,核心模块
    function _Server() {
      const data = _interopRequireDefault(require("metro/src/Server"));
    ​
      _Server = function () {
        return data;
      };
    ​
      return data;
    }
    ...
    ​
    // 保存资源文件
    var _saveAssets = _interopRequireDefault(require("./saveAssets"));
    // metro的默认配置
    var _loadMetroConfig = _interopRequireDefault(require("../../tools/loadMetroConfig"));
    ...
    ​
    // 打包的实现模块
    const outputBundle = require('metro/src/shared/output/bundle');
    ​
    async function buildBundle(args, ctx, output = outputBundle) {
      // 合并 metro 默认配置和自定义配,并设置maxWorkers   resetCache
      const config = await (0, _loadMetroConfig.default)(ctx, {
        maxWorkers: args.maxWorkers,
        resetCache: args.resetCache,
        config: args.config
      });
      return buildBundleWithConfig(args, config, output);
    }
    ​
    ​
    async function buildBundleWithConfig(args, config, output = outputBundle) {
      ...
    ​
      process.env.NODE_ENV = args.dev ? 'development' : 'production';
       // 根据命令行的入参 --sourcemap-output 构建 sourceMapUrl
      let sourceMapUrl = args.sourcemapOutput;
    ​
      if (sourceMapUrl && !args.sourcemapUseAbsolutePath) {
        sourceMapUrl = _path().default.basename(sourceMapUrl);
      }
    ​
      // 根据解析得到参数,构建requestOptions,传递给打包函数
      const requestOpts = {
        entryFile: args.entryFile,
        sourceMapUrl,
        dev: args.dev,
        minify: args.minify !== undefined ? args.minify : !args.dev,
        platform: args.platform,
        unstable_transformProfile: args.unstableTransformProfile
      };
      // 实例化metro 服务
      const server = new (_Server().default)(config);
    ​
      try {
        // 启动打包
        const bundle = await output.build(server, requestOpts);
        // 将打包生成的bundle保存到对应的目录
        await output.save(bundle, args, _cliTools().logger.info); // Save the assets of the bundle
    ​
        //  处理资源文件,解析,并在下一步保存在--assets-dest指定的位置
        const outputAssets = await server.getAssets({ ..._Server().default.DEFAULT_BUNDLE_OPTIONS,
          ...requestOpts,
          bundleType: 'todo'
        }); // When we're done saving bundle output and the assets, we're done.
    ​
         // 保存资源文件到指定目录
        return await (0, _saveAssets.default)(outputAssets, args.platform, args.assetsDest);
      } finally {
        server.end();
      }
    }
    ​
    var _default = buildBundle;
    exports.default = _default;

    buildBandule.js中可以看到具体的打包实现是 output.build(server, requestOpts)中,具体代码为:

    // node_modules/metro/src/shared/output/bundle.js
    ...
    ​
    function buildBundle(packagerClient, requestOptions) {
      return packagerClient.build({
        ...Server.DEFAULT_BUNDLE_OPTIONS,
        ...requestOptions,
        bundleType: "bundle",
      });
    }
    ...
    exports.build = buildBundle;
    exports.save = saveBundleAndMap;
    exports.formatName = "bundle";
    

    其实使用的是packagerClient.build,也就是刚刚传入的 server模块。

    metro构建bundle: 流程入口

    通过之前的分析,我们已经知晓整个react-native bundle 打包服务的启动在node_modules/metro/src/Server.js 中的build方法中。

    // node_modules/metro/src/Server.jsclass 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;
        
        this._createModuleId = config.serializer.createModuleIdFactory();
        this._bundler = new IncrementalBundler(config, {
          hasReducedPerformance: options && options.hasReducedPerformance,
          watch: options ? options.watch : undefined,
        });
        this._nextBundleBuildID = 1;
      }
      ...
      async build(options) {
        // 将传递进来的参数进行格式化,按照模块拆分
        const {
          entryFile,
          graphOptions,
          onProgress,
          serializerOptions,
          transformOptions,
        } = splitBundleOptions(options);
    ​
         // metro打包核心:解析(Resolution)和转换(Transformation)
        const { prepend, graph } = await this._bundler.buildGraph(
          entryFile,
          transformOptions,
          {
            onProgress,
            shallow: graphOptions.shallow,
          }
        );
    ​
        // 获取构建入口文件路径
        const entryPoint = this._getEntryPointAbsolutePath(entryFile);
    ​
         // 初始化构建参数,此处的参数来源于: 命令行 && 自定义metro配置metro.config.js && 默认的metro配置
        const bundleOptions = {
          asyncRequireModulePath: await this._resolveRelativePath(
            this._config.transformer.asyncRequireModulePath,
            {
              transformOptions,
              relativeTo: "project",
            }
          ),
          processModuleFilter: this._config.serializer.processModuleFilter,
          // 默认的createModuleIdFactory给每个module生成id; 其默认生成规则详情请见: node_modules/metro/src/lib/createModuleIdFactory.js
          createModuleId: this._createModuleId,
          // 给方法签名 默认值为     getRunModuleStatement: moduleId => `__r(${JSON.stringify(moduleId)});`,
          //  详情请见: node_modules/metro-config/src/defaults/index.js
          getRunModuleStatement: this._config.serializer.getRunModuleStatement,
          dev: transformOptions.dev,
          projectRoot: this._config.projectRoot,
          modulesOnly: serializerOptions.modulesOnly,
          // 指定在主模块前运行的模块, 默认值: getModulesRunBeforeMainModule: () => []
          // 详情请见: node_modules/metro-config/src/defaults/index.js
          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 = await this._config.serializer.customSerializer(
            entryPoint,
            prepend,
            graph,
            bundleOptions
          );
    ​
          if (typeof bundle === "string") {
            bundleCode = bundle;
          } else {
            bundleCode = bundle.code;
            bundleMap = bundle.map;
          }
        } else {
          // 这边转化成两个步骤
    ​
          // 将解析及转化之后的数据,生成如下格式化的数据
          // {
          //   pre: string, // var定义部分及poyfill部分的代码
          //   post: string, // require部分代码
          //   modules: [[number, string]], // 模块定义部分,第一个参数为number,第二个参数为具体的代码
          // }
          const base = baseJSBundle(entryPoint, prepend, graph, bundleOptions)
          // 将js module进行排序并进行字符串拼接生成最终的代码
          bundleCode = bundleToString(base).code;
    ​
          // 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,
        };
      }
    ...
      
    }
    

    在build函数中,首先执行了buildGraphthis_bundler 初始化是在Server的constructorIncrementalBunder 实例。它的buildGraph函数完成了打包过程中前两步 Resolution(解析)Transformation(转换)

    Metro 构建bundle:解析和转换

    metro中的IncrementalBundler 进行解析和转化的主要作用:

    • 返回了以入口文件为入口的所有相关依赖文件的依赖图谱和bable转化后的代码;
    • 返回了var定义部分及polyfill部分所有依赖文件的依赖图谱和bable转化后的代码;

    整体的流程图:

    metro解析和转换

通过 上图流程总结:

  • 整个 metro 进行依赖分析和 babel 转换主要通过了JestHasteMap去做依赖分析;
  • 在做依赖分析的通过,metro 会监听当前目录的文件变化,然后以最小变化生成最终依赖关系图谱;
  • 不管是入口文件解析还是 polyfill 文件的依赖解析都是使用了JestHasteMap;

metro 构建 bundle: 生成

再看 Server 服务启动代码部分我们发现经过buildGraph之后得到了

  • prepend: var及polyfill部分的代码和依赖关系
  • graph: 入口文件的依赖关系及代码

metro 使用了baseJSBundle将依赖关系图谱和每个模块的代码经过一系列的操作最终使用 bundleToString 转换成最终的代码。