在前文《babel是怎样工做的》中介绍了 Bable 中的的AST,此次我们给 bable 写一个插件,文中会覆盖大部份的用法,若是你对某些细节不是很明白,能够去看一下官方的 Babel 手册,中文版在这里:javascript
https://github.com/jamiebuild...前端
不过有的部分尚未翻译完。java
首先要找到要修改的节点,假设咱们要帮一个特定的函数 myFunction
加上调试用的信息,在这里只加上文件名就好了,而这个 myFunction
长成这样:node
function myFunction(data, optionalFilename)
使用者能够本身在 optionalFilename
加上想要的名字,或者用插件加上去,第一步就是打开 AST Explorer(https://astexplorer.net/)并写一个简单的测试程序来肯定 AST 应该是什么样的:git
myFunction(foo, __filename)
结果是这样:程序员
因为咱们要可以判断使用者传入的几个参数,也要能肯定使用者是在调用咱们的函数,因此应该在 CallExpression
中进行处理:es6
// babel 的 plugin 能够用 module.exports 或 es6 的 export default // 函数的第一个参数是使用者正在使用的 `@babel/core` module.exports = function ({ types: t }) { return { name: 'add-debug-information', // plugin 的名字,个加不加都行 // pre(state) {}, // 要处理一个新的档案时会调用这个函数 // post(state) {}, // 文件处理完成时要调用的函数 visitor: { CallExpression(path) { console.log(path) // 这样就能够获得 CallExpression }, // babel 能够在进入或是离开节点时调用 plugin 的函数,不过由于一般会须要在进入节点时处理, // 因此 babel 让使用者能够简写成上面那样,若是要在进入和离开时存取节点的话要写成像下面这样 // CallExpression: { // enter() { // 进入时 // }, // leave() { // 离开时 // }, // } } } }
下一步就是要判断是否是咱们要作处理的节点了,这里先只简单的判断两个条件,函数名是 myFunction
而且只能用一个参数:github
// 这里只写 CallExpression 的内容 if ( t.isIdentifier(path.node.callee, { name: 'myFunction' }) && // 判断函数名是 `myFunction` ,这里的 t 就是 babel 传来的 types // 另外也能够直接判断 node 的 name,好比:t.isIdentifier(path.node.callee) && path.node.callee.name === 'myFunction' path.node.arguments.length < 2) { // 肯定没有传入第二个参数 // 处理目标节点 }
若是要判断的目标比较复杂,目前也没有比较好的方法,只能这样比较。另外由于 babel 中只能拿到到 AST 信息,若是要判断类型等几乎是没有什么办法的,因此实际在写插件时必须考虑全部合理的写法,若是真的没办法处理时必定要要告诉使用者必须按照某种格式写,不然不会被处理。面试
在已经找到目标目标的前提下,要把文件名加入到参数中。这里直接加入 node 中的 `__filename
变量,这个变量在 node 的模块中是那个原始码文件的文件名。segmentfault
// 在上面的 if 中 path.pushContainer('arguments', t.identifier({ name: '__filename' })) // 若是要加载开头,能够用 unshiftContainer
那么为何要用 pushContainer
修改 AST 的内容呢?直接用 push
加到 arguments
中不行吗?这里最大的差异在于 plugin 新增了节点,若是有上游的添加、删除等改变,babel 也必需要便利新的节点,因此要用 babel 的 API 让它知道有节点被改变了。
完整的代码以下:
module.exports = function ({ types: t }) { return { name: 'add-debug-information', visitor: { CallExpression(path) { if (t.isIdentifier(path.node.callee, { name: 'myFunction' }) && path.node.arguments.length > 1) { path.pushContainer('arguments', t.identifier('__filename')) } } } } }
接下来再来看看其余例子。
假如要在正式环境把除错信息移除的话,就把 myFunction
第二个之后的参数都移除掉:
module.exports = function ({ types: t }) { return { visitor: { CallExpression(path) { if (t.isIdentifier(path.node.callee, {name: 'myFunction'})) { while (path.node.arguments.length > 1) { // 只要参数数量超过 1 个 path.get(`arguments.1`).remove() //就把第二个参数移除,而下一个会补上来,因此下一次循环会再移除掉下一个 } } } } }; }
path
的 get
能够用于获取指定位置的 Path
对象,可用于处理特定的子节点。
此次需求变成了在代码中加上对 NODE_ENV
的判断,若是是生产环境就不要除错信息,结果像这样:
// 原来 myFunction(data) // 变为 process.env.NODE_ENV === 'production' ? myFunction(data) : myFunction(data, __filename)
一般上面的代码在正式环境中不会真的多出一个判断,由于通常 bundler 会把 NODE_ENV 换成字串常量,而后再由 minifier 移除掉不须要的部分。
由于要产出的代码变多了,此次就用 template
module.exports = function ({ types: t, template }) { // 这里用到的 `%%data%%` 表明稍后咱们能够放节点去取代那个位置,只须要用两个 `%` 包起来便可, // 这个是 babel 7.4 之后才支持的语法,若是想支持之前的版本,就要把它改为 `DATA` (必定要全大写) // template 的返回值是一个函数 const tpl = template(`process.env.NODE_ENV === 'production' ? myFunction(%%data%%) : myFunction(%%data%%, %%source%%);`) // 用来标记已经遍历过的节点,用 Symbol 能够防止产生命名上的冲突 const visited = Symbol() return { visitor: { CallExpression(path) { // 检查节点是否遍历过 if (path.node[visited]) { return } if (t.isIdentifier(path.node.callee, {name: 'myFunction'})) { // 替换节点 path.replaceWith( // tpl 是一个函数,只须要把 placeholder 的部份传进去,就会返回 AST tpl({ // 这里要避免使用者没有传入第一个参数的状况,否则后面的参数会变成第一个参数 // 也能够抛出 error 或者让 myFunction 在运行时进行判断 data: path.node.arguments[0] || t.identifier('undefined'), // 若是使用者本身提供了除错信息,那么就用使用者提供的,否则就用 __filename source: path.node.arguments[1] || t.identifier('__filename') }) ) // 把节点下的 `myFunction` 都标记为遍历过 path.node.consequent[visited] = true path.node.alternate[visited] = true } } } } }
前面说过,要是新加入节点的话,babel 也会去遍历它,而咱们加入的节点中就包含了要处理的目标节点,若是不进行特殊处理的话就会一直无限的遍历下去,因此要给添加的节点加上本身的标记,这样就能够避免重复处理。
在上一个例子中,为了要避免使用者少传参数而给了默认值,那若是要在少传参数时抛出错误又要怎么作呢。
module.exports = function ({ types: t, template }) { // 和上一个例子差很少 const tpl = template(`process.env.NODE_ENV === 'production' ? myFunction(%%data%%) : myFunction(%%data%%, %%source%%);`) const visited = Symbol() // 建立一个函数来帮助抛出 error function throwMissingArgument(path) { // 这里用 path 上的 buildCodeFrameError ,这样显示的时候就可以标记有问题的代码在什么地方 throw path.buildCodeFrameError('`myFunction` required at least 1 argument') } return { visitor: { CallExpression(path) { if (path.node[visited]) { return } if (t.isIdentifier(path.node.callee, {name: 'myFunction'})) { path.replaceWith( tpl({ // 这里改用 throwMissingArgument data: path.node.arguments[0] || throwMissingArgument(path), source: path.node.arguments[1] || t.identifier('__filename') }) ) path.node.consequent[visited] = true path.node.alternate[visited] = true } } } } }
若是没传参数的话应该会看到 babel 输出了这样的 error
code.js: `myFunction` expect at least 1 argument > 1 | myFunction() | ^^^^^^^^^^^^
到此为止,咱们终于写出了本身的第一个 babel 插件。