ReactNative 分包-理论篇

518 阅读2分钟

1. 问题

  • 打包之后的产物包含哪些内容?
  • 分包的原理以及如何分包?

2. Metro 介绍

接受一个入口文件和各种选项,并返回一个包含所有代码及其依赖项的文件。

2.1. 整体流程

  • 命令参数解析。
  • 启动打包服务。
  • 经过解析、转换、生成捆绑包。
  • 停止打包服务。 RN metro 打包流程.jpg

2.2. 核心概念

  • Resolution:【解析器】生成所有模块构建图表。
  • Transformation:【转换器】将模块转换为目标平台可以理解的格式。
  • Serialization:【生成器】转换后的所有模块,生成一个捆绑包。

2.3. 打包命令

安装 metrometro-core

npm install metro metro-core

metro 命令配置

2.4. 产物分析

2.4.1. 示例代码
// utils.js
function printLog(msg) {
  console.log("[Metro Log]", msg)
}

module.exports = {
  printLog
}
// index.js
const { printLog } = require("./utils")

function sayHello() {
  printLog("hello world")
}

sayHello();
2.4.2. 配置&打包

package.json 中的 script 增加如下脚本 build: metro build index.js --out bundle.js -z flase,然后执行 npm run build 打包命令。

2.4.3. 产物
  • var 声明层 包含当前运行环境、bundle 启动时间、进程相关信息。
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 层 包含 define(__d)require(__r)clear(__c) 等定义的支持,以及 module 的加载逻辑。
!(function(e){"use strict";e.__r=i,e[`${__METRO_GLOBAL_PREFIX__}__d`]=function(e,n,o){if(null!=t[n])return;const i={dependencyMap:o,factory:e,hasError:!1,importedAll:r,importedDefault:r,isInitialized:!1,publicModule:{exports:{}}};t[n]=i},e.__c=o,e.__registerSegment=function(e,r,n){p[e]=r,n&&n.forEach(r=>{t[r]||h.has(r)||h.set(r,e)})};var t=o();const r={},{hasOwnProperty:n}={};function o(){return t=Object.create(null)}function i(e){const r=e,n=t[r];return n&&n.isInitialized?n.publicModule.exports:d(r,n)}function l(e){const n=e;if(t[n]&&t[n].importedDefault!==r)return t[n].importedDefault;const o=i(n),l=o&&o.__esModule?o.default:o;return t[n].importedDefault=l}function u(e){const o=e;if(t[o]&&t[o].importedAll!==r)return t[o].importedAll;const l=i(o);let u;if(l&&l.__esModule)u=l;else{if(u={},l)for(const e in l)n.call(l,e)&&(u[e]=l[e]);u.default=l}return t[o].importedAll=u}i.importDefault=l,i.importAll=u;let c=!1;function d(t,r){if(!c&&e.ErrorUtils){let n;c=!0;try{n=_(t,r)}catch(t){e.ErrorUtils.reportFatalError(t)}return c=!1,n}return _(t,r)}const s=16,a=65535;function f(e){return{segmentId:e>>>s,localId:e&a}}i.unpackModuleId=f,i.packModuleId=function(e){return(e.segmentId<<s)+e.localId};const p=[],h=new Map;function _(r,n){if(!n&&p.length>0){var o;const e=null!==(o=h.get(r))&&void 0!==o?o:0,i=p[e];null!=i&&(i(r),n=t[r],h.delete(r))}const c=e.nativeRequire;if(!n&&c){const{segmentId:e,localId:o}=f(r);c(o,e),n=t[r]}if(!n)throw m(r);if(n.hasError)throw g(r,n.error);n.isInitialized=!0;const{factory:d,dependencyMap:s}=n;try{const t=n.publicModule;return t.id=r,d(e,i,l,u,t,t.exports,s),n.factory=void 0,n.dependencyMap=void 0,t.exports}catch(e){throw n.hasError=!0,n.error=e,n.isInitialized=!1,n.publicModule.exports=void 0,e}}function m(e){return Error('Requiring unknown module "'+e+'".')}function g(e,t){return Error('Requiring module "'+e+'", which threw an exception: '+t)}})('undefined'!=typeof globalThis?globalThis:'undefined'!=typeof global?global:'undefined'!=typeof window?window:this);
  • __d 模型定义层 __d 的实现在 polyfill 层,实现如下:
function(e,n,o) {
    if(null!=t[n])return;
    const i={
      dependencyMap:o,
      factory:e,
      hasError:!1,
      importedAll:r,
      importedDefault:r,
      isInitialized:!1,
      publicModule:{
        exports:{}
      }
    };
    // 存储所有模块的 t
    t[n]=i
  }

函数接受 3 个参数, e 对应工厂方法,n 对应模块 IDo 对应模块的依赖列表【存储的是所依赖的其它模块的 ID】。

__d(
function(g,r,i,a,m,e,d){"use strict";const{printLog:o}=r(d[0]);o("hello world")},
0,
[1]
);

__d(
function(g,r,i,a,m,e,d){"use strict";m.exports={printLog:function(o){console.log("[Metro Log]",o)}}},
1,
[]
);
  • __r require 层 __d 的实现在 polyfill 层,实现如下:
function i(e){
  // r 为模块 ID
  // t 为模块表
  const r=e,n=t[r];
  return n&&n.isInitialized?n.publicModule.exports:d(r,n)
}
  
function d(t,r){
  if(!c&&e.ErrorUtils){
    let n;c=!0;
    try{
      n=_(t,r)
    } catch(t){
      e.ErrorUtils.reportFatalError(t)
    }
    return c=!1,n
  }
  return _(t,r)
}

function _(r,n){
  if(!n&&p.length>0){
    var o;
    const e=null!==(o=h.get(r))&&void 0!==o?o:0,
    i=p[e];
    null!=i&&(i(r),n=t[r],h.delete(r))
  }
  const c=e.nativeRequire;
  if(!n&&c){const{segmentId:e,localId:o}=f(r);c(o,e),n=t[r]}
  if(!n)throw m(r);if(n.hasError)throw g(r,n.error);n.isInitialized=!0;
  const{factory:d,dependencyMap:s}=n;

  try{
    const t=n.publicModule;
    return t.id=r,
    d(e,i,l,u,t,t.exports,s),
    n.factory=void 0,
    n.dependencyMap=void 0,
    t.exports
  }catch(e){
    throw n.hasError=!0,
    n.error=e,
    n.isInitialized=!1,
    n.publicModule.exports=void 0,
    e
  }
}

通过模块 IDt 中获取到该模块,然后执行模块的工厂方法 d(e,i,l,u,t,t.exports,s),方法中会对 exports 对象进行修改。

__r(0);

3. 分包思路

基于对 2.4.3. 的产物分析,可以将 var 声明层polyfill 层,一些基础模块 __d 层 放在基础包中,一些业务模块 __d 层 以及 __r 放到业务包中,从而达到分包的目的。

3.1. 手动分包

2.4.3. 的产物进行分包,可以手动分为 基础包 (utils)业务包(index) 等两个包,内容如下:

  • 业务包
// index.bundle.js
__d(
function(g,r,i,a,m,e,d){"use strict";const{printLog:o}=r(d[0]);o("hello world")},
0,
[1]
);
  • 基础包
// utils.bundle.js
...
__d(
function(g,r,i,a,m,e,d){"use strict";m.exports={printLog:function(o){console.log("[Metro Log]",o)}}},
1,
[]
);

3.2. 自动分包

打包过程中主要依赖的配置文件如下:

module.exports = {
  /* general options */

  resolver: {
    /* resolver options */
  },
  transformer: {
    /* transformer options */
  },
  serializer: {
    /* serializer options */
  },
  server: {
    /* server options */
  },
}

主要在 Serialization 阶段进行分包,通过配置 serializer 中的 createModuleIdFactoryprocessModuleFilter 即可达到自动分包的效果。

  • createModuleIdFactory 生成模块 ID

  • processModuleFilter 此模块是否需要打入包中

4. 分包实战

基于 23 节介绍的思路进行分包,同样使用 2.4.1 的代码进行演示。

4.1. 基础包

4.1.1. 代码示例
// utils.js
function printLog(msg) {
  console.log("[Metro Log]", msg)
}

module.exports = {
  printLog
}
4.1.2. 配置&打包

首先新增 base.config.js 配置文件,

const fs = require('fs')
const pathSep = require('path').sep;

// 输出主包清单
function manifest (path) {
  if (path.length) {
    const manifestFile = `./dist/common_manifest_${process.env.PLATFORM}.txt`;
    if (!fs.existsSync(manifestFile)) {
        fs.writeFileSync(manifestFile, path);
    } else {
        fs.appendFileSync(manifestFile, '\n' + path);
    }
  }
}

function createModuleIdFactory() {
  return function(path) {
    let name = '';
    if (path.startsWith(__dirname)) {
        name = path.substr(__dirname.length + 1);
    }
    let regExp = pathSep == '\\' ?
        new RegExp('\\\\', "gm") :
        new RegExp(pathSep, "gm");
    return name.replace(regExp, '_');
  }
}

function processModuleFilter(module) {
  manifest(module['path']);
  return true;
}

module.exports = {
  serializer: {
    createModuleIdFactory,
    processModuleFilter
  }
}

其中 common_manifest_undefined.txt 文件主要用来打业务包时,过滤掉主包中的模块。

然后在 package.json 中的 script 增加如下脚本 build-base": "metro build utils.js --out ./dist/base.bundle -z flase --config base.config.js,最后执行 npm run build-base 命令生成产物。

4.1.3. 产物
  • common_manifest_undefined.txt 主包清单
__prelude__
/Users/lyc/Desktop/D/MetroDemo/node_modules/metro-runtime/src/polyfills/require.js
require-/Users/lyc/Desktop/D/MetroDemo/utils.js
/Users/lyc/Desktop/D/MetroDemo/utils.js
__prelude__
/Users/lyc/Desktop/D/MetroDemo/node_modules/metro-runtime/src/polyfills/require.js
/Users/lyc/Desktop/D/MetroDemo/utils.js
  • base.bundle.js 主包
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";
!(function(e){"use strict";e.__r=i,e[`${__METRO_GLOBAL_PREFIX__}__d`]=function(e,n,o){if(null!=t[n])return;const i={dependencyMap:o,factory:e,hasError:!1,importedAll:r,importedDefault:r,isInitialized:!1,publicModule:{exports:{}}};t[n]=i},e.__c=o,e.__registerSegment=function(e,r,n){p[e]=r,n&&n.forEach(r=>{t[r]||h.has(r)||h.set(r,e)})};var t=o();const r={},{hasOwnProperty:n}={};function o(){return t=Object.create(null)}function i(e){const r=e,n=t[r];return n&&n.isInitialized?n.publicModule.exports:d(r,n)}function l(e){const n=e;if(t[n]&&t[n].importedDefault!==r)return t[n].importedDefault;const o=i(n),l=o&&o.__esModule?o.default:o;return t[n].importedDefault=l}function u(e){const o=e;if(t[o]&&t[o].importedAll!==r)return t[o].importedAll;const l=i(o);let u;if(l&&l.__esModule)u=l;else{if(u={},l)for(const e in l)n.call(l,e)&&(u[e]=l[e]);u.default=l}return t[o].importedAll=u}i.importDefault=l,i.importAll=u;let c=!1;function d(t,r){if(!c&&e.ErrorUtils){let n;c=!0;try{n=_(t,r)}catch(t){e.ErrorUtils.reportFatalError(t)}return c=!1,n}return _(t,r)}const s=16,a=65535;function f(e){return{segmentId:e>>>s,localId:e&a}}i.unpackModuleId=f,i.packModuleId=function(e){return(e.segmentId<<s)+e.localId};const p=[],h=new Map;function _(r,n){if(!n&&p.length>0){var o;const e=null!==(o=h.get(r))&&void 0!==o?o:0,i=p[e];null!=i&&(i(r),n=t[r],h.delete(r))}const c=e.nativeRequire;if(!n&&c){const{segmentId:e,localId:o}=f(r);c(o,e),n=t[r]}if(!n)throw m(r);if(n.hasError)throw g(r,n.error);n.isInitialized=!0;const{factory:d,dependencyMap:s}=n;try{const t=n.publicModule;return t.id=r,d(e,i,l,u,t,t.exports,s),n.factory=void 0,n.dependencyMap=void 0,t.exports}catch(e){throw n.hasError=!0,n.error=e,n.isInitialized=!1,n.publicModule.exports=void 0,e}}function m(e){return Error('Requiring unknown module "'+e+'".')}function g(e,t){return Error('Requiring module "'+e+'", which threw an exception: '+t)}})('undefined'!=typeof globalThis?globalThis:'undefined'!=typeof global?global:'undefined'!=typeof window?window:this);
__d(function(g,r,i,a,m,e,d){"use strict";m.exports={printLog:function(o){console.log("[Metro Log]",o)}}},"utils.js",[]);
__r("utils.js");

4.2. 业务包

4.2.1. 示例代码
// index.js
const { printLog } = require("./utils")

function sayHello() {
  printLog("hello world")
}

sayHello();
4.2.2. 配置&打包

首先新增 index.config.js 配置文件,

const fs = require('fs');

const pathSep = require('path').sep;
var commonModules = null;

// 是否已经在主包清单中
function isInManifest (path) {
    const manifestFile = `./dist/common_manifest_${process.env.PLATFORM}.txt`;

    if (commonModules === null && fs.existsSync(manifestFile)) {
        const lines = String(fs.readFileSync(manifestFile)).split('\n').filter(line => line.length > 0);
        commonModules = new Set(lines);
    } else if (commonModules === null) {
        commonModules = new Set();
    }

    if (commonModules.has(path)) {
        return true;
    }

    return false;
}

// 是否打入当前的包
function processModuleFilter(module) {
    if (isInManifest(module['path'])) {
        return false;
    }
    return true;
}

// 生成 require 语句的模块 ID
function createModuleIdFactory() {
    return path => {
        let name = '';
        if (path.startsWith(__dirname)) {
            name = path.substr(__dirname.length + 1);
        }
        let regExp = pathSep == '\\' ?
            new RegExp('\\\\',"gm") :
            new RegExp(pathSep,"gm");
        
        return name.replace(regExp,'_');
    };
}


module.exports = {
    serializer: {
        createModuleIdFactory,
        processModuleFilter,
    }
};

模块已经出现在主包清单中,则不打入此包中。

然后在 package.json 中的 script 增加如下脚本 build-index": "metro build utils.js --out ./dist/index.bundle -z flase --config index.config.js,最后执行 npm run build-index 命令生成产物。

4.2.3. 产物
  • index.bundle.js 业务包
__d(function(g,r,i,a,m,e,d){"use strict";const{printLog:o}=r(d[0]);o("hello world")},"index.js",["utils.js"]);
__r("index.js");

5. 分包进阶

ReactNative 分包实战篇

6. Demo

MetroDemo

7. 参考

Metro

react-native bundle 到 bundle 生成到底发生了什么(metro 打包流程简析)

React Native 按需加载实战(一)