实现了一个 mini 的模块打包器,支持 ES Module 语法。
网上也有一些多少行代码实现 mini webpack 的例子,本文也有参考,但这个例子有两个优点:一是使用基于根目录的绝对路径作为模块文件的唯一索引,确保每个文件只加载一次;二是使用模块缓存,并能处理循环依赖。
代码如下,不过100行(但是引入了babel这些库😁):
// pack.js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');
function createAsset(filename) {
const content = fs.readFileSync(filename, 'utf-8');
const ast = parser.parse(content, {
sourceType: 'module',
});
const dependencies = [];
traverse(ast, {
ImportDeclaration: ({ node }) => {
dependencies.push(node.source.value);
},
});
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env'],
});
return {
filename,
dependencies,
code,
};
}
function createGraph(entry) {
const entryAsset = createAsset(entry);
const assetMap = {
[entry]: entryAsset,
};
const queue = [entryAsset]; // queue 用来递归遍历所有模块及模块的依赖
for (const asset of queue) {
asset.mapping = {};
const dirname = path.dirname(asset.filename);
asset.dependencies.forEach((sourcePath) => {
// sourcePath是该模块文件的相对位置,而filePath是根路径的相对位置
// 只有filePath才能作为一个模块的唯一索引,存在assetMap中
const filePath = './' + path.join(dirname, sourcePath);
asset.mapping[sourcePath] = filePath;
// 如果此模块未被处理
if (!assetMap[filePath]) {
const child = createAsset(filePath);
queue.push(child);
assetMap[filePath] = child;
}
});
}
return assetMap;
}
function bundle(graph, entry) {
let modules = '';
for (const key in graph) {
const mod = graph[key];
modules += `"${mod.filename}": [
function (require, module, exports) { ${mod.code} },
${JSON.stringify(mod.mapping)},
],`;
}
const result = `
(function (modules) {
const module_cache = {};
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const cachedModule = module_cache[id];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
const module = (module_cache[id] = {
exports: {},
});
fn(localRequire, module, module.exports);
return module.exports;
}
require('${entry}');
})({${modules}})
`;
return result;
}
const entry = './example/entry.js'; // 设置入口文件
const graph = createGraph(entry);
const result = bundle(graph, entry);
fs.writeFileSync('output/index.js', result); // 输出打包结果
使用它也很简单,可以先写一个简单的使用 ES 模块语法的例子,如:
// entry.js
import { name } from './name.js';
console.log(`hello ${name}!`);
// name.js
export const name = 'knockkk';
然后在 pack.js 中设置入口文件路径即可。打包结果如下:
(function (modules) {
const module_cache = {};
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const cachedModule = module_cache[id];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
const module = (module_cache[id] = {
exports: {},
});
fn(localRequire, module, module.exports);
return module.exports;
}
require('./example/entry.js');
})({
'./example/entry.js': [
function (require, module, exports) {
'use strict';
var _name = require('./name.js');
console.log('hello '.concat(_name.name, '!'));
},
{ './name.js': './example/name.js' },
],
'./example/name.js': [
function (require, module, exports) {
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true,
});
exports.name = void 0;
var name = 'knockkk';
exports.name = name;
},
{},
],
});
运行后打印:hello knockkk!
nice~👍