「 用JavaScript编写的现代模块捆绑器{modern module bundler}
的简化示例 」
Explanation
"version": "1.0.0"
介绍
作为前端开发人员,我们花费大量时间处理Webpack
,Browserify
和Parcel
等工具。
了解这些工具的工作方式可以帮助我们更好地决定编写代码的方式。通过理解我们的代码如何变成一个包以及该包的外观如何,我们也可以更好地进行调试。
这个项目的目的是解释大多数捆绑商{bundlers}
如何在隐藏的条件下工作。它包含一个简化但仍然相当准确的捆绑器的简短实现。随着代码,有评论解释代码试图实现什么。
我习惯把代码搬上台面
本目录
好点, 让我们从运行开始
npm i
node src/minipack.js
第一眼, 乱糟糟的结果 -🖐️
(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'; },
{},
],})
好乱啊, 一步一步来吧
是什么❓
模块捆绑器 将 小块代码 编译成 更大和更复杂的代码,可以运行在Web浏览器中.
这些小块只是JavaScript文件以及它们之间的依赖关系,而这正是由模块系统表示 https://webpack.js.org/concepts/modules
模块捆绑器具有 入口文件 的这种概念,而不是添加一些
脚本标签在浏览器中并让它们运行,我们让 捆绑器 知道
哪个文件 是我们应用程序的 主要文件. 这是应该的文件
引导我们的整个应用程序.
我们的打包程序将从该 入口文件 开始,并尝试理解 它依赖于哪些文件. 然后,它会尝试了解哪些文件 依赖关系取决于它,它会继续这样做,直到它发现 我们应用程序中的 每个模块,以及它们如何 相互依赖.
这种对项目的理解被称为依赖图
.
在这个例子中,我们将创建一个 依赖关系图 并将其用于打包 它的所有模块都捆绑在一起.
让我们开始 : )
请注意: 这是一个非常简化的例子 循环依赖,缓存模块导出,仅解析每个模块一次 和其他人跳过,使这个例子尽可能简单.
// minipack.js 导入与全局变量
const fs = require('fs');
const path = require('path');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const {transformFromAst} = require('babel-core');
let ID = 0;
来自
minipack.js
#L1-35
放开函数的定义, 来到运行的部分
// minipack.js
const graph = createGraph('./example/entry.js'); 🧠
const result = bundle(graph);
console.log(result);
我们把 ./example/entry.js
路径带入了
// entry.js
import message from './message.js';
console.log(message);
2. 从 入口文件开始, 遍历 整座依赖图
const graph = createGraph('./example/entry.js');
🧠
我们需要可以提取单个模块的依赖关系,我们将通过提取入口文件{entry}
的依赖关系来解决问题.
那么,我们将提取它的每一个依赖关系的依赖关系. 循环下去
直到我们了解应用程序中的每个模块以及它们如何相互依赖. 这个项目的理解被称为依赖图
.
function createGraph(entry) {
首先解析整个文件.
const mainAsset = createAsset(entry);
我们将使用队列{queue}
来解析每个资产{asset}
的依赖关系. 我们正在定义一个只有 入口资产 的数组.
const queue = [mainAsset];
我们使用一个for ... of
循环遍历 队列. 最初 这个队列 只有一个 资产,但是当我们迭代它时,我们会将额外的 新资产 推入 队列 中. 这个循环将在 队列 为空时终止.
for (const asset of queue) {
我们的每一个 资产 都有它所依赖模块的相对路径列表. 我们将重复它们,用我们的createasset()
函数解析它们,并跟踪此模块在此对象中的依赖关系.
asset.mapping = {};
这是这个模块所在的目录.
const dirname = path.dirname(asset.filename);
我们遍历其相关路径的列表.
asset.dependencies.forEach(relativePath => {
我们的createasset()
函数需要一个绝对文件名. 该依赖性数组是相对路径的数组. 这些路径与导入它们的文件相关. 我们可以通过将相对路径与父资源目录的路径连接,将相对路径转变为绝对路径.
const absolutePath = path.join(dirname, relativePath);
解析资产,读取其内容并提取其依赖关系.
const child = createAsset(absolutePath);
这对我们来说很重要, 知道asset
依赖于取决于child
. 通过增加一个新的属性来表达这种关系mapping
带有child
-id
的对象.
asset.mapping[relativePath] = child.id;
最后,我们将child asset
推入队列,这样它的依赖关系也将被迭代和解析.
queue.push(child);
});
}
在这一点上,队列 就是一个包含目标应用中 每个模块 的数组: 这就是我们的表示图.
return queue;
完整函数 createGraph
function createGraph(entry) {
const mainAsset = createAsset(entry);
const queue = [mainAsset];
for (const asset of queue) {
asset.mapping = {};
const dirname = path.dirname(asset.filename);
asset.dependencies.forEach(relativePath => {
const absolutePath = path.join(dirname, relativePath);
const child = createAsset(absolutePath);
asset.mapping[relativePath] = child.id;
queue.push(child);
});
}
return queue;
}
1. 单个文件的 解析, 和依赖结构返回
我们首先创建一个函数,该函数将接受 文件路径 ,读取内容并提取其相关性.
function createAsset(filename) {
以字符串形式读取文件的内容.
const content = fs.readFileSync(filename, 'utf-8');
现在我们试图找出这个文件依赖于哪个文件. 我们可以通过查看其内容
来获取 import
字符串. 然而,这是一个非常笨重的方法,所以我们将使用JavaScript解析器.
JavaScript解析器是可以读取和理解JavaScript代码的工具.
它们生成一个更抽象的模型,称为ast (抽象语法树)
.
我强烈建议你看看ast explorer
看看 ast
是如何的
ast
包含很多关于我们代码的信息. 我们可以查询它了解我们的代码正在尝试做什么.
const ast = babylon.parse(content, {
sourceType: 'module',
});
这个数组将保存这个模块依赖的模块的相对路径.
const dependencies = [];
我们遍历ast
来试着理解这个模块依赖哪些模块. 要做到这一点,我们检查ast
中的每个 import
声明. ❤️
Ecmascript
模块相当简单,因为它们是静态的. 这意味着你不能import
一个变量,或者有条件地import
另一个模块.
每次我们看到import
声明时,我们都可以将其数值视为依赖性
.
我们将依赖关系数组推入我们导入的值. ⬅️
// ❤️
traverse(ast, {
ImportDeclaration: ({node}) => {
dependencies.push(node.source.value); ⬅️
// entry.js 中
// import message from './message.js';
// node.source.value === './message.js'
},
});
我们还通过递增简单计数器为此模块分配唯一标识符.
const id = ID++;
我们使用Ecmascript
模块和其他JavaScript功能,可能不支持所有浏览器. 为了确保我们的bundle
在所有浏览器中运行,我们将使用babel来传输它
该presets
选项是一组规则,告诉babel
如何传输我们的代码. 我们用`babel-preset-env``将我们的代码转换为浏览器可以运行的东西.
const {code} = transformFromAst(ast, null, {
presets: ['env'],
});
返回有关此模块的所有信息.
return {
id, // 唯一id
filename, // 文件名字
dependencies, // import 的文件路劲
code, // 转换的代码
};
}
完整函数 createAsset
function createAsset(filename) {
const content = fs.readFileSync(filename, 'utf-8');
const ast = babylon.parse(content, {
sourceType: 'module',
});
const dependencies = [];
traverse(ast, {
ImportDeclaration: ({node}) => {
dependencies.push(node.source.value);
},
});
const id = ID++;
const {code} = transformFromAst(ast, null, {
presets: ['env'],
});
return {
id,
filename,
dependencies,
code,
};
}
3. 整顿依赖图, 返回结果
接下来,我们定义一个函数,它将使用我们的graph
并返回一个可以在浏览器中运行的包.
我们的软件包将只有一个自我调用函数:
(function() {})()
该函数将只接收一个参数: 一个包含graph
中每个模块信息的对象.
function bundle(graph) {
let modules = '';
在我们到达该函数的主体之前,我们将构建我们将要提供它的对象. 请注意,我们构建的这个字符串被两个花括号 ({}) 包裹,因此对于每个模块,我们添加一个这种格式的字符串: key: value,
.
graph.forEach(mod => {
图表中的每个模块在这个对象中都有一个entry
. 我们使用模块的id
作为key
和value
的数组 (我们在每个模块中有2个值) .
第一个值是用函数包装的每个模块的代码. 这是因为模块应该被 限定范围: 在一个模块中定义变量不会影响 其他模块 或 全局范围.
我们的模块在我们将它们转换后,使用commonjs
模块系统: 他们期望一个require
, 一个module
和exports
对象可用. 那些在浏览器中通常不可用,所以我们将它们实现并将它们注入到函数包装中.
对于第二个值,我们将模块及其依赖关系之间的映射串联起来. 这是一个看起来像这样的对象: {'./relative/path': 1}
.
这是因为我们模块的传输代码{被 babel 转译}
已经调用require()
与相对路径. 当调用这个函数时,我们应该能够知道图中的哪个模块对应于该模块的相对路径.
// entry.js
import message from './message.js';
console.log(message);
被 babel 转译 变成
"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);
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
${JSON.stringify(mod.mapping)},
],`;
});
最后,我们实现自调函数的主体.
我们首先创建一个require()
⏰函数: 它接受一个 模块ID
并在其中查找它模块
我们之前构建的对象. 通过解构const [fn, mapping] = modules[id]
来获得我们的函数包装器 和mappings
对象.
我们模块的代码已经调用require()
用相对文件路径而不是模块ID. 我们的require
🌟函数需要 模块ID
. 另外,两个模块可能require()
相同的相对路径,但意味着两个不同的模块.
// 模块转译并
// 1. 捆绑器内部请求
var _message = require("./message.js"); // ⏰
// 2. 参数带入捆绑
localRequire == require
fn(localRequire, module, module.exports);
// 3. 捆绑内部请求 路径 转换 为 id
function localRequire(name) {
return require(mapping[name]); // require(id) 🌟
}
// 4. 实际
// “./message.js" === 1
return require(1); // require(id)
// 5. 从内部找到并运行
// 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
函数供它使用. 它将是特定的,并将知道通过使用模块的映射对象
将 其相对路径
转换为
ID
.
该映射 恰好是该特定模块的相对路径和模块ID
之间的映射.
最后,使用commonjs
,当需要模块时,它可以通过改变它的值来暴露值exports
目的. exports
对象在被模块代码改变之后,将被返回require()
功能.
const result = `
(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); // 🌟
})({${modules}})
`;
我们只需返回结果,欢呼!:)
return result;
完整函数 bundle
function bundle(graph) {
let modules = '';
graph.forEach(mod => {
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
${JSON.stringify(mod.mapping)},
],`;
});
const result = `
(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);
})({${modules}})
`;
return result;
}
让我们, 重新看一下吧
你可以将, 示例项目中 minipack/exmaples.js
加加减减
(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'; },
{},
],})