简介
在前端项目中babel无处不在,这章将实现一个将代码console.log(xxx)转换为console.log('行号:',xxx)的babel插件。让我们对babel插件开发有初步的知识体系,从而可以针对自己业务实现对应的构建需求。例如可以做自动埋点|自动国际化 | 代码高亮 | 页面主题工程化等功能解放自己提升效率。
babel
Babel 是一个通用的多用途 JavaScript 编译器。通过 Babel 你可以使用(并创建)下一代的 JavaScript,以及下一代的 JavaScript 工具。Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。其还支持持语法扩展, 例如JSX | 静态类型检查等
AST
抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,抽象表示把js代码进行了结构化的转化,转化为一种数据结构。
来自Abstract syntax tree文章的截图,代码最终生成的结构模板
下面基于astexplorer.net在线将代码转换为AST。后续我们将对如下的数据进行处理从而达到我们需要的效果
开始干活
功能分析
- 先来看看我们要转换的
原始代码与转换后的代码效果:
// 原始代码
function AKclown () {
console.log('AKclown')
}
// 转换后的代码
function AKclown () {
console.log('2','AKclown')
}
- 如下是
原始代码与转换后的代码AST语法的差异: 转换前的AST转换后的AST
根据上面图片对比不难看出,我们只需要
CallExpression表达式下新增一个aruguments参数,类型为StringLiteral
- 获取
行号: 通过path.node.loc.start.line就可以获取到行号了
通过上面分析我们console.log(xxx)转换为console.log('行号:',xxx)的实现有了大致的理解。其涉及到bable的三步骤: 首先将console.log(xxx)解析成AST、其次对原始AST进行改造生成新的AST、生成最终代码
Babel 的三个主要处理步骤分别是: 解析(parse) ,转换(transform) ,生成(generate) 。
解析(parse)
使用@babel/parser将sourceCode解析成AST语法树。
解析分为两个步骤:词法分析(Lexical Analysis)和 语法分析(Syntactic Analysis)。.
const parser = require('@babel/parser');
const sourceCode = 'console.log('AKclown')'
const ast = parser.parse(sourceCode, {
sourceType: 'unambiguous'
});
与astexplorer.net生成的一致
转换(transform)
第一步: 我们需要定义Visitors(访问者)对象并且给其添加CallExpression方法。然后通过@babel/traverse遍历上面的AST语法树。在遍历中,每当树遇到了CallExpression就会调用CallExpression()方法。
const traverse = require('@babel/traverse').default;
traverse(ast, {
CallExpression(path, state) {
}
});
第二步: 编写CallExpression方法,进行代码转换添加aruguments参数其类型为StringLiteral, 添加。这里需要结合@babel/types和@babel/generator
@babel/types提供了手动构建AST和检查AST类型的方法。例如接下用到t.StringLiteral创建一个StringLiteral的节点,也可以通过t.isStringLiteral来判断某个节点是不是isStringLiteral- babel节点类型很多,我们不可能完全记住节点的数据结构。可以通过类型声明文件来查看
方式一: 判断条件过于复杂
const calleeName = ['log', 'info', 'error', 'debug'];
traverse(ast, {
CallExpression (path, state) {
if (types.isMemberExpression(path.node.callee)
&& path.node.callee.object.name === 'console'
&& calleeName. includes(path.node.callee.property.name)
) {
const { line } = path.node.loc.start;
path.node.arguments.unshift(types.stringLiteral(`(${line}:`))
}
}
});
方式二: 结合@babel/generator直接生成console.log进行比较,而无需先比较console再比较是否为log。一次判断直接搜哈(推荐)
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generate = require('@babel/generator').default;
const targetCalleeName = ['log', 'info', 'error', 'debug'].map(item => `console.${item}`);
traverse(ast, {
CallExpression(path, state) {
const calleeName = generate(path.node.callee).code; // 生成console.~字符串
if (targetCalleeName.includes(calleeName)) {
const { line } = path.node.loc.start;
path.node.arguments.unshift(t.stringLiteral(`${line}:`))
}
}
});
生成(generate)
通过@babel/generator将修改后的AST生成最终的代码
const code = generate(ast).code;
console.log('code: ', code);
插件化
上面已经实现了需求设定接下来只需要把它进行插件化即可.
定义一个函数并且exports出去,该函数的第一个参数为babel对象。
const generate = require('@babel/generator').default;
const targetCalleeName = ['log', 'info', 'error', 'debug'].map(item => `console.${item}`);
module.exports = function ({ types: t }) {
console.log('babel: ', babel);
return {
visitor: {
CallExpression(path, state) {
const calleeName = transformFromAst(path.node.callee).code; // 生成console.~字符串
if (targetCalleeName.includes(calleeName)) {
const { line } = path.node.loc.start;
path.node.arguments.unshift(t.stringLiteral(`${line}:`))
}
}
}
};
}
在@babel/core中使用上面编写的自定义babel插件
const { transformSync } = require("@babel/core");
const customPlugin = require('./custom-plugin');
const sourceCode = `
function AKclown(){
console.log('AKclown')
}
`
const { code } = transformSync(sourceCode, {
plugins: [customPlugin],
parserOpts: {
sourceType: 'unambiguous',
}
});
console.log('code: ', code);
总结
通过上面步骤我们对babel、AST以及babel插件开发有了初步的认识。如果想更加深入了解建议阅读Babel 插件通关秘籍、babel-handbook和官网文档。此次demo的github地址
在这里我推荐一下我使用babel能力构建的vscode插件JS To TS。让你更加便捷的获取到TS类型声明
文献链接
工程化思维:主题切换架构
Babel 插件通关秘籍
babel-handbook
Abstract syntax tree
AST详解与运用
What is a Polyfill