乐趣区

关于javascript:手写一个-babel-插件

在前文《babel 是怎么工作的》中介绍了 Bable 中的的 AST,这次咱们给 bable 写一个插件,文中会笼罩大部份的用法,如果你对某些细节不是很明确,能够去看一下官网的 Babel 手册,中文版在这里:

https://github.com/jamiebuild…

不过有的局部还没有翻译完。

拜访节点

首先要找到要批改的节点,假如咱们要帮一个特定的函数 myFunction 加上调试用的信息,在这里只加上文件名就行了,而这个 myFunction 长成这样:

function myFunction(data, optionalFilename)

使用者能够本人在 optionalFilename 加上想要的名字,或者用插件加上去,第一步就是关上 AST Explorer(https://astexplorer.net/)并写一个简略的测试程序来确定 AST 应该是什么样的:

myFunction(foo, __filename)

后果是这样:

因为咱们要可能判断使用者传入的几个参数,也要能确定使用者是在调用咱们的函数,所以应该在 CallExpression 中进行解决:

// 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 并且只能用一个参数:

// 这里只写 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 的模块中是那个原始码文件的文件名。

// 在下面的 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() // 就把第二个参数移除,而下一个会补上来,所以下一次循环会再移除掉下一个}
        }
      }
    }
  };
}

pathget 能够用于获取指定地位的 Path 对象,可用于解决特定的子节点。

替换节点和 template

这次需要变成了在代码中加上对 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 也会去遍历它,而咱们退出的节点中就蕴含了要解决的指标节点,如果不进行非凡解决的话就会始终有限的遍历上来,所以要给增加的节点加上本人的标记,这样就能够防止反复解决。

抛出 error

在上一个例子中,为了要防止使用者少传参数而给了默认值,那如果要在少传参数时抛出谬误又要怎么做呢。

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 插件。


本文首发微信公众号:前端先锋

欢送扫描二维码关注公众号,每天都给你推送陈腐的前端技术文章


欢送持续浏览本专栏其它高赞文章:

  • 深刻了解 Shadow DOM v1
  • 一步步教你用 WebVR 实现虚拟现实游戏
  • 13 个帮你进步开发效率的古代 CSS 框架
  • 疾速上手 BootstrapVue
  • JavaScript 引擎是如何工作的?从调用栈到 Promise 你须要晓得的所有
  • WebSocket 实战:在 Node 和 React 之间进行实时通信
  • 对于 Git 的 20 个面试题
  • 深刻解析 Node.js 的 console.log
  • Node.js 到底是什么?
  • 30 分钟用 Node.js 构建一个 API 服务器
  • Javascript 的对象拷贝
  • 程序员 30 岁前月薪达不到 30K,该何去何从
  • 14 个最好的 JavaScript 数据可视化库
  • 8 个给前端的顶级 VS Code 扩大插件
  • Node.js 多线程齐全指南
  • 把 HTML 转成 PDF 的 4 个计划及实现

  • 更多文章 …
退出移动版