react-native metro 分析

前言

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值