手写一个webpack,看看AST怎么用 (4)

image-20210207154741847

使用traverse遍历AST

对于这样一个生成好的AST,我们可以使用@babel/traverse来对他进行遍历和操作,比如我想拿到ImportDeclaration进行操作,就直接这样写:

// 使用babel traverse来遍历ast上的节点 traverse(ast, { ImportDeclaration(path) { console.log(path.node); }, });

上面代码可以拿到所有的import语句:

image-20210207162114290

将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);

这个打印结果是:

image-20210207172310114

可以看到这个结果里面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);

拿到的结果跟之前的差不多:

image-20210207173744463

好了,现在需要将使用import的地方也替换了,因为我们已经知道了这个地方是将它作为函数调用的,也就是要将

const helloWorldStr = helloWorld();

转为这个样子:

const helloWorldStr = (0,_helloWorld__WEBPACK_IMPORTED_MODULE_0__.default)();

这行代码的效果其实跟_helloWorld__WEBPACK_IMPORTED_MODULE_0__.default()是一样的,为啥在前面包个(0, ),我也不知道,有知道的大佬告诉下我呗。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsxzpz.html