-
Notifications
You must be signed in to change notification settings - Fork 0
Description
100 行代码实现 webpack
前段时间在油管上看到一个 BUILD YOUR OWN WEBPACK 的演讲,作者以简单易懂的方式,现场一步步实现了一个 100 行代码左右的webpack,顿时让我对研究 webpack 多了一丢丢的兴趣。webpack 功能虽然很强大,但那复杂的配置,也是让一波猿望而却步。今天我们就来看下,这 100 行代码是如何实现一个简易的 webpack 的。
在正式分析前,我们先来看几个 babel 的包:
@babel/parser 的作用是把一段代码转换成 AST,比如下面这段代码:
import message from '/message.js';
console.log(message);经过转换变成:
你可以通过 astexplorer 自己试下。
@babel/traverse 的作用是可以增删改查 parse 生成的语法树。
@babel/core 的作用就是代码的转换。
好了,了解过这三个包的大致用法后,我们来看下下面这个函数,
let ID = 0;
function createAsset(filename) {
const content = fs.readFileSync(filename, 'utf-8');
const ast = babylon.parse(content, {
sourceType: 'module'
});
let dependencies = [];
traverse(ast, {
ImportDeclaration: ({ node }) => {
dependencies.push(node.source.value);
}
});
const id = ID++;
const { code } = babel.transformFromAstSync(ast, null, {
presets: ['@babel/preset-env']
});
return {
id,
filename,
dependencies,
code
};
}createAsset 方法很简单,根据传入的文件路径,获取到文件内容,然后通过 @babel/parser 包生成对应的 AST;接着使用 traverse 方法来获取该文件 import 了哪些文件,并且把这些依赖存储在 dependencies 数组中;然后用 transformFromAstSync 方法把 AST 转换成 es5 的代码。最终返回一个对象,包括文件 ID,文件路径,依赖及内容代码。(下文中提到的对象数据,都是指返回的这个对象)
那么对于那些被依赖的文件,如何来遍历查询他们的依赖关系呢?
function createGraph(entry) {
const mainAsset = createAsset(entry);
const queue = [mainAsset];
for (const asset of queue) {
const dirname = path.dirname(asset.filename);
asset.mapping = {};
asset.dependencies.forEach(relativePath => {
const absolutePath = path.join(dirname, relativePath);
const child = createAsset(absolutePath);
asset.mapping[relativePath] = child.id;
queue.push(child);
});
}
return queue;
}首页根据入口文件拿到它所对应的对象数据,既然要做广度遍历,那先放入一个数组当中,然后逐层开始遍历。首先获取 entry 文件所在目录,然后依赖文件路径,获取依赖文件的绝对路径,再次调用 createAsset 方法返回依赖文件所包含的对象数据,同时标记文件 ID,然后 push 到数组中,等待下次的循环遍历。最后的 queue 队列,则包含了所有文件的依赖关系。
那么当我们拿到完整的依赖关系后,怎么让代码能在浏览器中跑起来呢,我们知道经过 babel 转换后的代码是 CommonJS 规范的,如果不转换格式,是不能在浏览器端执行的。
function bundle(graph) {
let modules = '';
graph.forEach(mod => {
modules += `${mod.id}:[
function(module, exports, require) {
${mod.code}
},
${JSON.stringify(mod.mapping)},
],`;
});
const results = `
(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(relativePath) {
return require(mapping[relativePath]);
}
const module = { exports: {} };
fn(module, module.exports, localRequire);
return module.exports;
}
require(0);
})({${modules}})
`;
return results;
}要想跑起来,我们得先提供 module、exports 和 require 三个变量。如果结合经过 babel 转换后的代码来看,可能会更好的理解上面这段代码:
// name.js
export const nickname = 'jax';转换后变成:
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.nickname = void 0;
var nickname = 'jax';
exports.nickname = nickname;// message.js
import { nickname } from './name.js';
export default `hello ${nickname}!`;转换后变成:
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _name = require("./name.js");
var _default = "hello ".concat(_name.nickname, "!");
exports.default = _default;思考题
在分析模块之间依赖关系的时候,我们的实现有什么问题吗?如果存在一个模块被多个模块引用,或者存在两个模块互相引用又会出现上面问题呢?
答案在 minipack 的 PR 中可以找到。
