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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容