对于这样一个生成好的AST,我们可以使用@babel/traverse来对他进行遍历和操作,比如我想拿到ImportDeclaration进行操作,就直接这样写:
// 使用babel traverse来遍历ast上的节点 traverse(ast, { ImportDeclaration(path) { console.log(path.node); }, });上面代码可以拿到所有的import语句:
将import转换为函数调用前面我们说了,我们的目标是将ES6的import:
import helloWorld from "./helloWorld";转换成普通浏览器能识别的函数调用:
var _helloWorld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/helloWorld.js");为了实现这个功能,我们还需要引入@babel/types,这个库可以帮我们创建新的AST节点,所以这个转换代码写出来就是这样:
const t = require("@babel/types"); // 使用babel traverse来遍历ast上的节点 traverse(ast, { ImportDeclaration(p) { // 获取被import的文件 const importFile = p.node.source.value; // 获取文件路径 let importFilePath = path.join(path.dirname(config.entry), importFile); importFilePath = `./${importFilePath}.js`; // 构建一个变量定义的AST节点 const variableDeclaration = t.variableDeclaration("var", [ t.variableDeclarator( t.identifier( `__${path.basename(importFile)}__WEBPACK_IMPORTED_MODULE_0__` ), t.callExpression(t.identifier("__webpack_require__"), [ t.stringLiteral(importFilePath), ]) ), ]); // 将当前节点替换为变量定义节点 p.replaceWith(variableDeclaration); }, });上面这段代码我们用了很多@babel/types下面的API,比如t.variableDeclaration,t.variableDeclarator,这些都是用来创建对应的节点的,。注意这个代码里面我有很多写死的地方,比如importFilePath生成逻辑,还应该处理多种后缀名的,还有最终生成的变量名_${path.basename(importFile)}__WEBPACK_IMPORTED_MODULE_0__,最后的数字我也是直接写了0,按理来说应该是根据不同的import顺序来生成的,但是本文主要讲webpack的原理,这些细节上我就没花过多时间了。
上面的代码其实是修改了我们的AST,修改后的AST可以用@babel/generator又转换为代码:
const generate = require('@babel/generator').default; const newCode = generate(ast).code; console.log(newCode);这个打印结果是:
可以看到这个结果里面import helloWorld from "./helloWorld";已经被转换为var __helloWorld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/helloWorld.js");。
替换import进来的变量前面我们将import语句替换成了一个变量定义,变量名字也改为了__helloWorld__WEBPACK_IMPORTED_MODULE_0__,自然要将调用的地方也改了。为了更好的管理,我们将AST遍历,操作以及最后的生成新代码都封装成一个函数吧。
function parseFile(file) { // 读取入口文件 const fileContent = fs.readFileSync(file, "utf-8"); // 使用babel parser解析AST const ast = parser.parse(fileContent, { sourceType: "module" }); let importFilePath = ""; // 使用babel traverse来遍历ast上的节点 traverse(ast, { ImportDeclaration(p) { // 跟之前一样的 }, }); const newCode = generate(ast).code; // 返回一个包含必要信息的新对象 return { file, dependcies: [importFilePath], code: newCode, }; }然后启动执行的时候就可以调这个函数了
parseFile(config.entry);拿到的结果跟之前的差不多:
好了,现在需要将使用import的地方也替换了,因为我们已经知道了这个地方是将它作为函数调用的,也就是要将
const helloWorldStr = helloWorld();转为这个样子:
const helloWorldStr = (0,_helloWorld__WEBPACK_IMPORTED_MODULE_0__.default)();这行代码的效果其实跟_helloWorld__WEBPACK_IMPORTED_MODULE_0__.default()是一样的,为啥在前面包个(0, ),我也不知道,有知道的大佬告诉下我呗。