简介
在前端项目中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('文件名:index.js,行号: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和官网文档
文献链接
工程化思维:主题切换架构
Babel 插件通关秘籍
babel-handbook
Abstract syntax tree
AST详解与运用
What is a Polyfill