美文网首页
react-native metro 分析

react-native metro 分析

作者: LaxusJ | 来源:发表于2019-12-30 07:18 被阅读0次

    前言

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

    堆栈信息


    image.png

    可以看到最后调用到resolve

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

    最终输出是一个resolution,里面包含两个属性filePath和type


    image.png

    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

    相关文章

      网友评论

          本文标题:react-native metro 分析

          本文链接:https://www.haomeiwen.com/subject/pnthoctx.html