minipack 是什么
A simplified example of a modern module bundler written in JavaScript
正如 github 介绍,这是一个用 JavaScript 编写的现代模块构建工具的简化示例。
作为一名 FE, 平时可能会使用 Webpack/Browserify/Rollup/Parcel 等构建工具。了解这些构建工具的工作原理可以帮助我们更好地决定编写代码的方式,所以了解他们的工作原理还是很有必要的。
本文将围绕 minipack 的源码来分析如何实现一个 module bundlers.
module bundlers
module bundlers 将小块代码编译成更大和更复杂的代码,让其运行在 Web 浏览器中。 它拥有入口文件的概念,我们让 module bundlers 知道哪个文件是我们应用程序的入口文件,然后让 module bundlers 从该文件开始,并去尝试理解它依赖哪些文件,然后它会尝试了解这些文件的依赖关系,直到它发现应用程序中的每个模块,以及它们如何相互依赖,最后构成 dependenciesGraph(依赖图).
源码解析
代码主要有三个重要方法
createAsset
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| 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, }; }
|
createGraph
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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; }
|
bundle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| 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; }
|
总结
回过头来看,思路还是比较清晰的,主要分为两步:
- 通过 babel 以及 babel 插件,分析、收集依赖,得到整个程序的依赖图
- 实现一个 loader, 用于 require module
这是一个简化的构建工具设计流程,例子中还有一些问题尚未处理,比如只支持 es6 模块收集,无法兼容 CommonJS/CMD/AMD 模块; 模块引用路径必须写后缀名等问题。需要优化的地方还有很多,感兴趣的同学可以 fork 该项目完善。
参考链接
minipack-explain