需求背景
在前端开发中,开发环境下很常见的调试方法就是使用console.log,但是随着项目规模的扩大和复杂度的上升,console.log的内容需要写的比较详细才能定位到触发的调用点。为了减少不必要的心智负担和源码和控制台,页面之间的无效切换,希望能增强console的功能,让开发人员只需要书写console.log()就能在控制台中定位到具体文件中的具体行的代码。
技术选型
目前常用的前端工程化打包工具有webpack,vite等,本文基于webpack实现开发demo。
前置知识
编译原理和babel
编译器工作的原理是这样的:
- 通过词法分析,生成token序列,在这一步会去掉无效的缩进,空格和注释。
- 通过语法分析,生成AST抽象语法书
- 通过语义分析,验证AST是否符合预期。
- 中间代码生成:生成一种中间表示形式,能够简化目标代码的生成和优化过程,JS生成的是字节码。
- 中间代码优化:对中间代码进行一系列的转换和优化,以提高代码的执行效率。
- 目标代码生成:将中间代码转换为目标平台上的机器代码,包括汇编代码和 ELF 文件等,比如JS会生成对应的机器码。
而babel是一个js的编译器,用于将ES6+的代码经过编译生成大多数浏览器可以识别的ES5代码。 比较重要的有以下的库:
- babel/types:用来生成或者判断节点的AST语法树的节点
- babel/core: babel核心模块
也就是说,我们的工作需要在拿到老的代码,老代码生成AST后,对AST进行操作,生成新的代码,而对AST的具体操作也就是去增强console的功能。
必备工具
- AST explorer 使用该工具我们可以输入代码,查看生成的AST,鼠标选中某一块代码,会定位到AST书的具体节点
- 使用fs.appendFileSync()输出打包过程的细节 工程中的调试是必不可少的,但是在webpack成功编译后,会把打包过程中控制台的内容清除,只保留了编译成功及细节。
手很快才截到的,打印转换后的code的图:
最终稳定的控制台输出:
而且代码文件一般内容不少,直接打印也不是一个好方法,所以需要把输出的内容进行存储。 有以下两个方案:
- 输出到浏览器的缓存中,localstorage或者indexDB,但是打包过程的工具没有涉及到浏览器,无法在webpack打包工作过程中向浏览器缓存写入内容。有知道这个方案怎么解到大佬么?
- 使用fs模块,把内容输出到文件系统中,在这里输出到code.txt文件。
工作细节
无论做什么事情,我习惯于先完成顶层设计,再倒推实现细节。顶层设计的步骤是,书写loader,在webpack的配置中使用该loader。 而书写loader又分为定义输入,进行转换,定义输出。使用loader重点在决定loader的位置。
书写loader
定义输入
我们知道loader的输入是上一个loader的输出,这里从上帝视角看我们知道这个loader的位置是需要放在babel-loader的左边的(在babel-loader执行后才会执行,将一系列不同类型的代码(jsx 高版本js等)都转化为ES5的js,这样我们在使用AST explorer验证AST的时候只需要使用纯ES5即可。所以在这里的输入是ES5的JS代码。
进行转换
转换这里使用babel-core提供的功能,核心代码如下:
js
const targetSource = core.transform(sourceCode, {
plugins: [logPlugin], //使用插件
filename: fileName,
});
其中fileName是文件名,而logPlugin是我们需要编写的插件
visitor用于在遍历树节点的时候,只要节点的 type 在 visitor 对象中出现,就会调用该方法。
观察树的结构可以知道,console表达式位于node.callee.object.name
而log函数内的内容位于argument这一层级
根据上述的分析写出如下代码:
js
const logPlugin = {
visitor: {
CallExpression(path, state) {
const { node } = path;
if (types.isMemberExpression(node.callee)) {
if (node.callee.object.name === "console") {
//找到console
if (
["log", "info", "warn", "error"].includes(node.callee.property.name)
) {
//找到文件名
const filename = state.file.opts.filename.split('"')[1];
fs.appendFileSync(
"/Users/zhengmingsheng/Code/gongchenghua1/src/loader/code.txt",
"================================================\n" + filename
);
node.arguments.push(
types.stringLiteral(`调用点位于${filename}文件`)
); //向右边添加我们的行和列信息
const { line } = node.loc.start; //找到所处位置的行和列
// 其实这个行数还是有问题的,编译后有一些额外的代码"var _jsxDevRuntime = require("react/jsx-dev-runtime");
node.arguments.push(types.stringLiteral(`第${line}行`)); //向右边添加我们的行和列信息
}
}
}
},
},
};
进行输出
js
if (typeof targetSource.code === "string") {
return targetSource.code;
} else {
return sourceCode;
}
注意不能直接输出targetSource,它是一个object。
使用loader
和之前提到的一样,因为loader是从右到左执行的(从后到前)。
Console Loader完整代码
js
const core = require("@babel/core"); //babel核心模块
let types = require("@babel/types"); //用来生成或者判断节点的AST语法树的节点
const fs = require("fs");
const logPlugin = {
visitor: {
CallExpression(path, state) {
const { node } = path;
if (types.isMemberExpression(node.callee)) {
if (node.callee.object.name === "console") {
//找到console
if (
["log", "info", "warn", "error"].includes(node.callee.property.name)
) {
//找到文件名
const filename = state.file.opts.filename.split('"')[1];
fs.appendFileSync(
"/Users/zhengmingsheng/Code/gongchenghua1/src/loader/code.txt",
"================================================\n" + filename
);
node.arguments.push(
types.stringLiteral(`调用点位于${filename}文件`)
); //向右边添加我们的行和列信息
const { line } = node.loc.start; //找到所处位置的行和列
// 其实这个行数还是有问题的,编译后有一些额外的代码"var _jsxDevRuntime = require("react/jsx-dev-runtime");
node.arguments.push(types.stringLiteral(`第${line}行`)); //向右边添加我们的行和列信息
}
}
}
},
},
};
module.exports = function (sourceCode) {
sourceCode = String(sourceCode);
let fileName = null;
if (sourceCode.indexOf("_jsxFileName") !== -1) {
fileName = sourceCode.match(/\"\S+\"/)[0];
} else {
fileName = "default";
}
const targetSource = core.transform(sourceCode, {
plugins: [logPlugin], //使用插件
filename: fileName,
});
fs.appendFileSync(
"/Users/zhengmingsheng/Code/gongchenghua1/src/loader/code.txt",
"================================================\n" + targetSource.code
);
if (typeof targetSource.code === "string") {
return targetSource.code;
} else {
return sourceCode;
}
};
希望能帮到你~