背景:其实“打包”对于前端来说再熟悉不过了,但是深入其中的原理,却不是人人都熟悉。由于webpack
功能的强大和盛行,我们大部分都是所谓的“配置工程师”。借此,特地简单分析了一官方文档中提到的一个minipack
项目的源码,以此深入了解下什么是打包?以及打包的原理是什么?
文章写的比较平,是按照分析代码的顺序写的,细微有些总结,有错误或不妥之处,恳请指出。
原始代码:
// 入口文件 entry.js
import message from './message.js';
console.log(message);
// message.js
import {name} from './name.js';
export default `hello ${name}!`;
// name.js
export const name = 'world';
读取文件内容,分析依赖,第一步需要解析源码,生成抽象语法树。
第一步,读取入口文件,生成 AST,递归生成依赖关系对象 graph。
其中,createAsset 函数是解析js文本,生成每个文件对应的一个对象,其中 code 的代码是经过babel-preset-env
转换后可在浏览器中执行的代码。
const {code} = transformFromAst(ast, null, {
presets: ['env'],
});
createGraph 函数生成依赖关系对象。
[
{ id: 0,
filename: './example/entry.js',
dependencies: [ './message.js' ],
code: '"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);',
mapping: { './message.js': 1 } },
{ id: 1,
filename: 'example/message.js',
dependencies: [ './name.js' ],
code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\n\nvar _name = require("./name.js");\n\nexports.default = "hello " + _name.name + "!";',
mapping: { './name.js': 2 } },
{ id: 2,
filename: 'example/name.js',
dependencies: [],
code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nvar name = exports.name = \'world\';',
mapping: {} }
]
有了依赖关系图,下一步就是将代码打包可以在浏览器中运行的包。
首先我们将依赖图解析成如下字符串(其实是对象没用{}
包裹的格式):
关键代码是这句:
modules += `${mod.id}: [
function (require, module, exports) {
${mod.code}
},
${JSON.stringify(mod.mapping)},
],`;
0: [
function (require, module, exports) {
// -------------- mod.code --------------
"use strict";
var _message = require("./message.js");
var _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
console.log(_message2.default);
// --------------------------------------
},
{"./message.js":1},
],
1: [
function (require, module, exports) {
// -------------- mod.code --------------
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _name = require("./name.js");
exports.default = "hello " + _name.name + "!";
// --------------------------------------
},
{"./name.js":2},
],
2: [
function (require, module, exports) {
// -------------- mod.code --------------
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var name = exports.name = 'world';
// --------------------------------------
},
{},
],
这里,我们比较下源码:
// 入口文件 entry.js
import message from './message.js';
console.log(message);
// ---
"use strict";
var _message = require("./message.js");
var _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
console.log(_message2.default);
// message.js
import {name} from './name.js';
export default `hello ${name}!`;
// ---
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _name = require("./name.js");
exports.default = "hello " + _name.name + "!";
// name.js
export const name = 'world';
// ---
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var name = exports.name = 'world';
可以看出,babel在转换原始code的时候,引入了require
函数来解决模块引用问题。但是其实浏览器仍然是不认识的。因此还需要额外定义一个require
函数(其实这部分和requirejs原理类似的模块化解决方案,其中原理其实也很简单)
得到这个字符串后,再最后拼接起来即最终结果 => 然后,我们还需要定义一个自执行函数文本,并将上述字符串传入其中,拼接结果如下:
(function (modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const module = { exports: {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({
0: [
function (require, module, exports) {
"use strict";
var _message = require("./message.js");
var _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
console.log(_message2.default);
},
{ "./message.js": 1 },
],
1: [
function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _name = require("./name.js");
exports.default = "hello " + _name.name + "!";
},
{ "./name.js": 2 },
],
2: [
function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var name = exports.name = 'world';
},
{},
],
})
我们执行最后的结果,会输出"hello world"。
那我们仔细分析下打包后的这段代码:
首先这是一个自执行函数,传入的字符串外面包裹上{}
后是一个对象,形如<moduleId>: <value>
的格式。
自执行函数的主体部分定义了一个require
函数:
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const module = { exports: {} };
fn(localRequire, module, module.exports);
return module.exports;
}
接收一个模块id,过程如下:
- 第一步:解构module(数组解构),获取fn和当前module的依赖路径
- 第二步:定义引入依赖函数(相对引用),函数体同样是获取到依赖module的id,localRequire 函数传入到fn中
- 第三步:定义module变量,保存的是依赖模块导出的对象,存储在module.exports中,module和module.exports也传入到fn中
- 第四步:递归执行,直到子module中不再执行传入的require函数
简单来说,模块之间通过require
和exports
联系,至于模块内部的实现,只在模块内可见。
由此,可以看出,其实原理并不是很复杂,但是却很巧妙,要了解“打包”的原理,也需要了解“模块化”的一些知识。前端发展虽快,但是深入到基础,会发现其实是一脉相通的。
参考中文资料,里面有代码的逐句翻译(外国人的注释写的是真详细啊):
(本文始发于知乎专栏:zhuanlan.zhihu.com/ttys000)