1. 问题
- 打包之后的产物包含哪些内容?
- 分包的原理以及如何分包?
2. Metro 介绍
接受一个入口文件和各种选项,并返回一个包含所有代码及其依赖项的文件。
2.1. 整体流程
- 命令参数解析。
- 启动打包服务。
- 经过解析、转换、生成捆绑包。
- 停止打包服务。
2.2. 核心概念
Resolution:【解析器】生成所有模块构建图表。Transformation:【转换器】将模块转换为目标平台可以理解的格式。Serialization:【生成器】转换后的所有模块,生成一个捆绑包。
2.3. 打包命令
安装 metro、metro-core
npm install metro metro-core
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 对应模块 ID,o 对应模块的依赖列表【存储的是所依赖的其它模块的 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
}
}
通过模块 ID 从 t 中获取到该模块,然后执行模块的工厂方法 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 中的 createModuleIdFactory 和 processModuleFilter 即可达到自动分包的效果。
-
createModuleIdFactory 生成模块
ID -
processModuleFilter 此模块是否需要打入包中
4. 分包实战
基于 2、3 节介绍的思路进行分包,同样使用 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");