主要知识点 |
require
函数的polyfillrequire
函数的循环依赖问题GitHub:https://github.com/Hans774882968/mini-webpack
npm install
npm run bundle
修改index.html依赖的js文件路径(bundle_ts.js),复制到dist文件夹,然后点击打开index.html。
npm i @babel/parser
npm i @babel/traverse
npm i -D @types/babel__traverse
npm i @babel/core @babel/preset-env
npm i -D @babel/preset-typescript
npm i -D @types/babel__core
npm i -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm i [email protected]
注意点:
eslint
后记得重启一下vscode,IDE提示才会生效。@babel/core7.18.13
,对应的ts版本要指定为[email protected]
,否则运行代码会报错。主要是借鉴参考链接1来实现一个mini-webpack,但在功能上有所超越:
最大的缺憾是不清楚ts-loader
怎么实现,因此这里编译ts的做法是直接判定入口文件的扩展名为.ts
,然后用babel实现。
因为参考链接1写得很清晰了,本文仅定位为一个额外补充,不会写得很详细。
npm init
tsc --init
tsconfig.json
主要需要设置:
"compilerOptions": {
"module": "commonjs",
},
"include": [
"bundle.ts"
]
样就能用tsc
命令编译入口文件了。
接下来给package.json
加一个命令:"bundle": "tsc && node bundle.js"
,以后可以直接npm run bundle
模拟打包命令了。
目前除了在nodejs代码里用'@babel/preset-typescript'
插件以外,不知道怎么快速方便地编译src
文件夹下的ts,只好:先手工修改tsconfig.json
的include
和compilerOptions.module
,接着tsc
编译,最后还原tsconfig.json
。
getModuleInfo
函数主要分析文件的依赖和完成代码转换。
import
语句,把依赖的文件(相对路径)转换为相对于项目根目录的路径(下称“绝对路径”)。使用babel相关的库@babel/parser
、@babel/traverse
和@babel/core
完成。@babel/core
的transformFromAst
方法完成。commonjs
。对于编译js的情况不需要特别指明,而编译ts的情况需要指明插件:plugins: ['@babel/plugin-transform-modules-commonjs']
(参考链接5)。只需要修改@babel/parser
的parse
方法和@babel/core
的transformFromAst
方法的调用方式。
需要用到@babel/preset-typescript
这个插件。
@babel/preset-typescript
没有@babel/preset-env
方便,
需要指明filename
属性和@babel/plugin-transform-modules-commonjs
插件。
相关语句:
const ast = parser.parse(body, {
sourceType: 'module',
plugins: getType() === 'ts' ? ['decorators-legacy', 'typescript'] : []
});
babel.transformFromAst(ast, undefined,
getType() === 'ts' ?
{
presets: ['@babel/preset-typescript'],
filename: file,
plugins: ['@babel/plugin-transform-modules-commonjs']
} :
{ presets: ['@babel/preset-env'] },
(err, result) => {
if (err) reject(err);
resolve(result as babel.BabelFileResult);
}
);
parseModule
函数。因为循环依赖也只不过是形成递归,所以依赖图不局限于DAG,可以是任意有向图。所以只需要用bfs遍历一下(这里更正参考链接1的一处小错误,遍历算法不是递归而是bfs)。
parseModule
函数中的for循环for (const { deps } of a)
用到了它会继续遍历新加入的元素的特性,不能替换为forEach
,是js实现bfs的最简方案。parseModule
函数中的await Promise.all
是循环中使用async/await
的解决方案(参考链接4)。parseModule
函数的输出为depGraph
哈希表,其一个对象的deps
属性应该设计为一个哈希表,而非直接设计为数组,下文会解释原因。
getBundle
函数把上面生成的depGraph
哈希表扔进代码模板里,这就是打包结果。
为了在浏览器环境给出一个合法的commonjs
的polyfill(这里只需要给出require
和exports
对象),我们在代码模板中定义了自己的require
函数。对于一个代码文件来说,其返回值为这个文件的exports
对象,其副作用为把整个文件的代码执行了一遍。
`;(function (graph) {
var exportsInfo = {};
function require (file) {
if (exportsInfo[file]) return exportsInfo[file];
function absRequire (relPath) {
return require(graph[file].deps[relPath]);
}
var exports = {};
exportsInfo[file] = exports;
(function (require, exports, code) {
eval(code);
})(absRequire, exports, graph[file].code);
return exports;
}
require('${entryPath}');
})(${depGraph});`
值得注意的是,这个require
函数实际上是一个递归函数。在eval(code)
时可能产生递归。
depGraph
哈希表的一个对象的deps
属性为什么设计为一个哈希表,而非直接设计为数组?因为待执行的代码中所有的路径都是相对路径,我们需要用graph[file].deps[relPath]
这样的方式把它转换为绝对路径。为了完成这个转换,我们还需要设计absRequire
函数,它只不过起到一个拦截器的作用。
此时我们如果打包一个含有循环依赖的入口文件,运行时会栈溢出。以最简单的情况为例:a
模块的a
函数引用b
模块的b
函数,b
模块的b
函数引用a
模块的a
函数。
怎么解决呢?根据参考链接3,我们可以用“记忆化搜索”的思路,开一个全局变量var exportsInfo = {};
。并在exports
对象生成以后,立即exportsInfo[file] = exports;
。上文案例中,b
模块获得的a
模块的exports
对象的值是空的,但因为对象的浅拷贝特性,对象地址是正确的,在require
函数解析a
模块完毕后,b
模块也就能获得a
模块的exports
对象的正确值了。
相信对acmer来说这个算法很经典,没有相关背景的话可以尝试在浏览器打断点帮助理解。
main
函数在最开始读取配置(模仿webpack.config.js
),在最后把getBundle
生成的单个文件写入文件系统。
@babel/core
官方文档:@babel/coreasync/await
的解决方案:五种在循环中使用 async/await 的方法 - 知乎