babel主要处理步骤分为三个:解析、转换、生成
解析步骤接受代码输出AST
,该步骤分为两个阶段:词法分析、语法分析。
词法分析主要是对源代码进行分词,产生一个叫做token
的数组,分割的单位是运算符、括号、数字、字符串、标点符号等可以处理的最小单元。
然后语法分析再将所有的tokens
组合成一个整体,分析它们的语法和关系,最后输出AST
(源代码的抽象语法树)。
分成两个阶段后,更容易的对解析步骤作优化,因为解析步骤大部分的时间都在词法分析过程中,同时也能提高可移植性。
对AST
进行遍历,遇到需要处理的节点就操作,包括添加、移除和更新等。这个也是插件介入的主要工作内容。(babel也提供了自定义解析步骤的插件功能)
最终再将AST
转换回字符串形式的代码。
function customPlugin(babel) {
return {
visitor: {
// 定义多个访问者
}
}
}
如上所示,一个插件就是一个普通的函数,函数接受一个babel对象(包含babel所有的api
),最后返回一个包含visitor
属性的对象,visitor属性中每个key都是一个ast节点的类型,值就是访问这个节点的函数。
每个访问者函数都会接受两个参数:path
和state
。path对象表示两个节点之间连接的对象,例如:
{
"parent": {
"type": "FunctionDeclaration",
"id": {...},
....
},
"node": {
"type": "Identifier",
"name": "square"
}
}
state对象包含一些额外的状态信息,例如可以从state.opts
中取出为插件配置的特定选项,甚至可以取出path
对象,具体内容可以自己打印看看。
下面开发一个小插件,为代码中的console.log
添加调用位置的信息。
module.exports = function (babel) {
const t = babel.types
return {
name: 'custom-babel-plugin-demo',
visitor: {
CallExpression(path) {
const obj = path.node.callee.object
const prop = path.node.callee.property
const arguments = path.node.arguments
if (t.isIdentifier(obj) && t.isIdentifier(prop) && obj.name === 'console' && prop.name === 'log') {
const location = `---trace: line ${path.node.loc.start.line}, column ${path.node.loc.start.column}---`;
arguments.push(t.stringLiteral(location))
}
}
}
}
}
首先你需要知道你要访问节点的类型,如果不清楚,可以到这个网站查看。babel.types
则提供了类似lodash的工具库功能(api)。
最后再测试下插件功能是否正常:
const { transform } = require('@babel/core')
const options = {
plugins: [ ['./src/index.js', {
option1: true,
options2: false
}] ]
}
const code = `
const str1 = 'hello'
console.log(str1)
const str2 = 'babel'
console.log(str2)
const str3 = 'plugin'
console.log(str3)
`
transform(code, options, function(err, result) {
console.log(result.code)
})
得到输出:
const str1 = 'hello';
console.log(str1, "---trace: line 3, column 4---");
const str2 = 'babel';
console.log(str2, "---trace: line 5, column 4---");
const str3 = 'plugin';
console.log(str3, "---trace: line 7, column 4---");
如果你要编写良好的测试用例,可以借助babel-plugin-tester库。