27

WubaRN 分步打包流程

 5 years ago
source link: https://mp.weixin.qq.com/s/KVDKheeUvaeLwxERcY-nbQ?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

WubaRN采用分步加载的方式加载和运行bundle文件以提升速度。本文按照打包流程来解释WubaRN是如何拆分打包的。

bundle文件实际就是一个大的文本文件。

ZB3YvuR.jpg!web

那么整个打包过程可以概括为将.js代码『翻译』成jsc可以执行的代码并序列化到文件。

bundle文件内容是由一个个Module组成,甚至一张图片都会被当做Module加载。所以bundle实际上大致可分为module声明和module调用。

打包命令的『翻译』

用以下打包命令打core.bundle为背景,看打包脚本是如何『翻译』的。

bundle —dev false —entry-file /xxx/node_modules/react-native/Libraries/react-native/react-native-implementation.js —bundle-output /xxx/core.android.bundle —platform android —manifest-output core.android.manifest.json

这条命令的意思是『 我需要打一个输入文件为/xxx/node_modules/react-native/Libraries/react-native/react-native-implementation.js,输出文件为/xxx/core.android.bundle的release的bundle文件,并将id和module路径的映射关系存储到core.android.manifest.json文件中

为什么打core.bundle的输入文件是 react-native-implementation.js 呢?首先需要明确的是core.bundle承载的是ReactNative的元组件。而 react-native-implementation.js 就是记录所有元组件的类。

react-native-implementation.js
const ReactNative = {// Components
  get AccessibilityInfo() { return require('AccessibilityInfo'); },  get ActivityIndicator() { return require('ActivityIndicator'); },  get ART() { return require('ReactNativeART'); },  get Button() { return require('Button'); },  get DatePickerIOS() { return require('DatePickerIOS'); },  get DrawerLayoutAndroid() { return require('DrawerLayoutAndroid'); },  get FlatList() { return require('FlatList'); },  get Image() { return require('Image'); },  get ImageEditor() { return require('ImageEditor'); },  get ImageStore() { return require('ImageStore'); },  get KeyboardAvoidingView() { return require('KeyboardAvoidingView'); },  get ListView() { return require('ListView'); },  get Modal() { return require('Modal'); },
  ···
}
···
module.exports = ReactNative;

通过读取 react-native-implementation.js 文件即可得到所有需打到core.bundle里的元组件,这就是为什么 react-native-implementation.js 会作为打core.bundle的输入文件的原因。

下面进入打包脚本看这条命令是如何被执行的。

首先会创建一个 Server 实例。 Server 会借用 Bundler 的能力执行打包流程。

Sercer/index.js
buildBundle(options) {
···const opts = bundleOpts(options);const building = this._bundler.bundle(opts);
···
}

若要保证core.bundle能正常运行,肯定需要将 react-native-implementation.js 依赖的Module一并打包到core.bundle。所以

Bundler 中,会首先解析 react-native-implementation.js 的依赖关系。那么如何解析依赖关系呢?

如何解析Module依赖关系

打包脚本首先会去读Module的js源码,然后从源码中分析依赖关系。

这里还是以 react-native-implementation.js 为例。

react-native-implementation.js
'use strict';

var invariant=require('fbjs/lib/invariant');
var warning=require('fbjs/lib/warning');

var ReactNative={
···get ListView(){return require('ListView');},get Text(){return require('Text');},
···
}

···
module.exports=ReactNative;

打包脚本在分析依赖过程中,首先会先检查 react-native-implementation.js require了哪些组件。可以代码看到require了 fbjs/lib/invariantfbjs/lib/warning 。接下来打包脚本会分别到这两个模块的js代码里去分析依赖关系。有的同学已经意识到这是个递归。

严格来说 fbjs/lib/invariantfbjs/lib/warning 是主要是向Javascript解释器上下文注入一些能力的模块,并不是真正的业务依赖模块。类似功能的模块被声明在 packager/defaults.js 中。

exports.polyfills = [
  require.resolve('./react-packager/src/Resolver/polyfills/polyfills.js'),
  require.resolve('./react-packager/src/Resolver/polyfills/console.js'),
  require.resolve('./react-packager/src/Resolver/polyfills/error-guard.js'),
  require.resolve('./react-packager/src/Resolver/polyfills/Number.es6.js'),
  require.resolve('./react-packager/src/Resolver/polyfills/String.prototype.es6.js'),
  require.resolve('./react-packager/src/Resolver/polyfills/Array.prototype.es6.js'),
  require.resolve('./react-packager/src/Resolver/polyfills/Array.es6.js'),
  require.resolve('./react-packager/src/Resolver/polyfills/Object.es7.js'),
  require.resolve('./react-packager/src/Resolver/polyfills/babelHelpers.js'),
];

那么这里我先跳出递归,假设打包脚本已经分析拿到 fbjs/lib/invariantfbjs/lib/warning 的依赖关系,把焦点继续放到 react-native-implementation.js 上。

到这一步已经确定 react-native-implementation.js 的依赖关系表。

模块 fbjs/lib/invariant fbjs/lib/warning

继续往下看代码。

react-native-implementation.js
var ReactNative={
···get ListView(){return require('ListView');},get Text(){return require('Text');},
···
}

这里我只截取了变量 ReactNative 的部分代码。按照这里的逻辑 react-native-implementation.js 也应该依赖 ListViewText 。这里打包脚本依赖 babel-core 的能力来对代码进行转译分析。

babel-core 的作用是把 js 代码分析成 AST(Abstract Syntax Tree) ,方便各个插件分析语法进行相应的处理。AST简称语法树,是源代码语法结构的一种抽象标识。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

假如现在你面前有一个物体,它是一个不规则的圆体,整个身体通红,头部还有一根细长稍微弯曲偏右呈棕色的圆柱体。

在中文我们称之为「苹果」。

在英文我们称之为「Apple」。

在日文中我们称之为「アップル」。

在法语中我们称之为「pomme」。

在德语中我们称之为「Apfel」。

无论用不同的语言,苹果在文字上、发音上都完全不一样,但苹果确确实实的存在这个时空上,颜色、气味、形状都不曾因为语言而改变过。

如果我们需要让计算机帮忙算一下 (1+2)*3 的结果,解构成Json格式的AST为

{
    "type": "Program",
    "body": [
        {
            "type": "ExpressionStatement",
            "expression": {
                "type": "BinaryExpression",
                "operator": "*",
                "left": {
                    "type": "BinaryExpression",
                    "operator": "+",
                    "left": {
                        "type": "Literal",
                        "value": 1,
                        "raw": "1"
                    },
                    "right": {
                        "type": "Literal",
                        "value": 2,
                        "raw": "2"
                    }                },
                "right": {
                    "type": "Literal",
                    "value": 3,
                    "raw": "3"
                }            }        }
    ],
    "sourceType": "script"}

eeErMz3.png!web

AST可以用来做「语法高亮」、「关键字匹配」、「作用域判断」、以及「代码压缩」等等。

babel-corereact-native-implementation.js 代码 get Text(){return require('Text');} 解构成的AST内容如下

6R3meiZ.png!web

打包脚本通过对AST节点的筛选策略确定当前代码是否属于依赖关系。

extract-dependencies.js
function extractDependencies(code: string) {  const ast = babylon.parse(code);  const dependencies = new Set();  const dependencyOffsets = [];

  babel.traverse(ast, {
    CallExpression(path) {      const node = path.node;      const callee = node.callee;      const arg = node.arguments[0];      if (callee.type !== 'Identifier' || callee.name !== 'require' || !arg || arg.type !== 'StringLiteral') {        return;
      }
      dependencyOffsets.push(arg.start);
      dependencies.add(arg.value);
    }
  });  return {dependencyOffsets, dependencies: Array.from(dependencies)};
}

经过以上逻辑 react-native-implementation.js 的依赖关系表扩充到

模块 fbjs/lib/invariant fbjs/lib/warning Text ListView

以上依赖关系已经解析完成了。但是如果每次打包都读依赖关系解析一次就变得浪费资源。以 react-native-implementation.js 为例,该类属于RN框架内部代码,日常开发过程中除了RN框架升级,可以说每次打包的时候这个类的代码肯定是不变的。那么打包脚本就采用了缓存策略来避免每次打包做无意义的重复工作。

缓存策略

打包脚本缓存策略依赖于 jest-haste-map 。在打包命令执行时会将一系列环境上下文作为参数构建 JestHasteMap 对象。

node-haste/index.js
this._haste = new JestHasteMap({
      extensions: this._opts.extensions.concat(this._opts.assetExts),
      forceNodeFilesystemAPI: this._opts.forceNodeFilesystemAPI,
      ignorePattern: {test: this._opts.ignoreFilePath},
      maxWorkers: typeof mw === 'number' && mw >= 1 ? mw : getMaxWorkers(),
      mocksPattern: '',
      name: 'react-native-packager',
      platforms: Array.from(this._opts.platforms),
      providesModuleNodeModules: this._opts.providesModuleNodeModules,
      resetCache: this._opts.resetCache,
      retainAllFiles: true,
      roots: this._opts.roots.concat(this._opts.assetRoots_DEPRECATED),
      useWatchman: this._opts.useWatchman,
      watch: this._opts.watch,
    });

JestHasteMap 会根据传进来的参数计算缓存存储根目录。计算核心算法为取MD5值。

jest-haste-map/build/index.js
···this._cachePath = HasteMap.getCacheFilePath(    this._options.cacheDirectory,
    `haste-map-${ this._options.name }`,
    VERSION,    this._options.roots.join(':'),    this._options.extensions.join(':'),    this._options.platforms.join(':'),
    options.mocksPattern);
···static getCacheFilePath(tmpdir, name) {    const hash = crypto.createHash('md5');
    Array.from(arguments).slice(1).forEach(arg => hash.update(arg));    return path.join(
    tmpdir,
    name.replace(/\W/g, '-') + '-' + hash.digest('hex'));
  }

这里我的Mac上计算出的缓存根目录为 /var/folders/fx/49djd5k50834kmjp4q73h6v40000gn/T/haste-map-react-native-packager-8728b0f3d9c11381ce27c075522e548c

打包脚本计算出 react-native-implementation.js 的依赖关系后,会计算 react-native-implementation.js 绝对路径的hash值,然后生成 react-native-implementation.js 的缓存地址。

TransformCache.js
···function writeSync(props: {
  filePath: string,
  sourceCode: string,
  transformCacheKey: string,
  transformOptions: mixed,
  result: CachedResult,
}): void {  const cacheFilePath = getCacheFilePaths(props);
  ···
}
···function getCacheFilePaths(props: {
  filePath: string,
  transformOptions: mixed,
}): CacheFilePaths {  const hasher = imurmurhash()
    .hash(props.filePath)
    .hash(jsonStableStringify(props.transformOptions) || '');
  let hash = hasher.result().toString(16);
  hash = Array(8 - hash.length + 1).join('0') + hash;  const prefix = hash.substr(0, 2);  const fileName = `${hash.substr(2)}${path.basename(props.filePath)}`;  const base = path.join(getCacheDirPath(), prefix, fileName);  return {transformedCode: base, metadata: base + '.meta'};
}

可以看到getCacheFilePaths()方法的返回值有 transformedCodemetadata 两个属性。

这两个属性的值分别是

属性 值 transformedCode /var/folders/fx/49djd5k50834kmjp4q73h6v40000gn/T/react-native-packager-cache-9134ca1a/cf/a206e8react-native-implementation1.js metadata /var/folders/fx/49djd5k50834kmjp4q73h6v40000gn/T/react-native-packager-cache-9134ca1a/cf/a206e8react-native-implementation1.js.meta

那么接下来首先看 transformedCode 记录什么内容。这里我使用两张截图说明。

uaERzy6.jpg!web

6jyyYzA.jpg!web

通过和源码的对比发现其实就是去除Debug逻辑的源码。

meta里存储的是解析的依赖关系等『加工产物』。

[
  2954132535,
  1122713798,
  [
    "fbjs/lib/invariant",
    "fbjs/lib/warning",
    "ListView",
    "Text",
    ···
  ],
  [
    495,
    538,
    688,
    751,
    ···
  ],
  {
    "version": 3,
    "sources": [
      "/Users/kent/WorkSpace/TZRN/node_modules/react-native/Libraries/react-native/react-native-implementation.js"
    ],
    "names": [
      "invariant",
      "warning",
      "FlatList",
      "Text",
      ···
    ],
    "mappings": "AAAA;;;;;;;;;;;AAWA;;AAEA,GAAMA,WAAYC,QAAQ,oBAAR,CAAlB;AACA,GAAMC,SAAUD,QAAQ,kBAAR,CAAhB;;;;;;;;;;;;;;AAcA;AACA,GAAME,aAAc;AAClB;AACA,GAAIC,kBAAJ,···,
    "file": "react-native-implementation.js",
    "sourcesContent": [
      ""
    ]
  }
]

经过上面的缓存策略,打包脚本在下次再次打包的时候会优先读取上次打包记录的缓存信息来跳过解析流程,从而加快打包时间。

『加工』并序列化

打core.bundle的输入文件为 react-native-implementation.js ,打包脚本就会将 react-native-implementation 作为入口Module。

上面分析出的依赖关系列表中的Module的id会以入口Module的id为基准逐一递增。 react-native-implementation 的id为0.

Bundler/index.js
function createModuleIdFactory({extenalModules, startId: nextId = 0}) {  const fileToIdMap = Object.create(null);  return (module: {
    path: string,
    name?: "string"
  }) => {    if (extenalModules && module.name) {      if (module.name in extenalModules) {        return extenalModules[module.name].id;
      }
    }    if (!(module.path in fileToIdMap)) {
      fileToIdMap[module.path] = nextId;
      nextId += 1;
    }    return fileToIdMap[module.path];
  };
}

文章开头说道,bundle文件内容大致可分为module声明和module调用。下面看module是如何在bundle中变成可调用的。

当给module分配好id后,会对每个module的源码进行进一步包装,使其成为js中可执行的函数。

Resolver/index.js
function defineModuleCode(moduleName, code, verboseName = '', dev = true, extenalModules, manifestReferrence) {  const pkgName = verboseName.split('/')[0]; if (manifestReferrence && !extenalModules[verboseName] && pkgName !== appName) {
    verboseName = `${appName}@${verboseName}`;
  }  return [
    `__d(/* ${verboseName} */`,    'function(global, require, module, exports) {', // module factory
      code,    '\n}, ',    // `${JSON.stringify(moduleName)}`, // module id, null = id map. used in ModuleGraph
    `${JSON.stringify(verboseName)}`,
    dev ? `, null, ${JSON.stringify(verboseName)}` : '',    ');',
  ].join('');
}

react-native-implementation 经过加工后的代码如下:

__d(/* react-native-implementation */function(global, require, module, exports) {/**
'use strict';

var invariant=require('fbjs/lib/invariant');
var warning=require('fbjs/lib/warning');

var ReactNative={
···
get ListView(){return require('ListView');},
get Text(){return require('Text');},
···
}
···
module.exports=ReactNative;
}, "react-native-implementation");

当所有module都被加工后会序列化到文件,也就是本次打包要产出的core.bundle。所以core.bundle中的内容简化后大致如下。

__d(function(global, require, module, exports)){// code},"react-native-implementation");
__d(function(global, require, module, exports)){// code},"ListView");
__d(function(global, require, module, exports)){// code},"Text");
···
;require('InitializeCore');
;require('react-native-implementation');

打出core.bundle的同时会将依赖关系表中的module和其对应的id以Json格式序列化到core.manifest.json中。

core.manifest.json
{
  "modules": {
    "react-native-implementation": {
      "id": 0
    },
    ···
    "Text": {
      "id": 265
    },
    ···
    "ListView": {
      "id": 303
    },
    ···
  },
  "lastId": 372
}

打buz.bundle时会读取 core.manifest.json 的内容,然后分析业务js的依赖关系并缓存,然后剔除已经在 core.manifest.json 中记录的module后,以 core.manifest.json 中记录的 lastId 为基准继续加一递增给过滤后的module分配moduleId。剩下的步骤和打core.bundle的流程一致。

bundle如何运行

和Java不同的是,JS是解释型语言。程序不需要编译,程序在运行时才翻译成机器语言,每执行一次都要翻译一次。

无论把core.bundle还是buz.bundle『放进』JSContext,实质上都是将一堆字符串『放进』JSContext。JSContext会将这堆字符串转换成AST,当Native启动mainComponent时,这里假设mainComponent为”Wuba”,JSContext会执行方法

__d(function(global, require, module, exports)){// code},"Wuba");

然后分析该方法的AST,找到向上依赖关系依次执行。以上就是bundle的大致运行原理。

参考资料

  • 何为语法树


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK