# Babel 插件开发入门
Babel
是一个通用的 JavaScript
编译器,它可以将源码转换成源码,不同的功能具体由不同的插件来实现,比如 @babel/plugin-transform-runtime (opens new window) 以及 UI
库中将代码转换成按需引用的 babel-plugin-import (opens new window)。
下面将抛砖引玉,介绍如何写一个 Babel
插件,以及开发前的一些前置知识要点。
因为 Babel
是基于 AST
的,所以这里推荐阅读 AST 与 Babel,先了解什么是 AST
。
# Babel 基本原理
Babel
本身就是一基于 AST
的编译器,其工作主要分为以下 3 个阶段。
Parse
(解析),将代码解析成AST
,解析阶段又分成以下两个阶段。- 词法分析(Lexical Analysis)。
- 语法分析(Syntactic Analysis)。
Transform
(转换),遍历、访问AST
,对其进行修改。Generate
(生成),将修改过后的AST
转换、生成代码。
# Babel 插件开发常用工具链
# @babel/parser (opens new window)
可以将代码转换成 Babel
规范的 AST
。
# @babel/traverse (opens new window)
用于遍历、访问和修改 AST
。
# @babel/types (opens new window)
AST
可以是一个 JavaScript
对象,所以判断、修改节点可以是下面这样。
traverse(ast, {
enter(path) {
if (path.node.type === 'Identifier' && path.node.name === 'n') {
path.node.name = 'x'
}
}
})
然而,遵循 DRY
原则,自然有了 @babel/types
,里面有许多方法能够构建、校验、转换 AST
节点,帮助我们更加简便地操作 AST
。有了它上面的代码就可以简化为下方代码。
traverse(ast, {
enter(path) {
if (t.isIdentifier(path.node, { name: 'n' })) {
path.node.name = 'x'
}
}
})
# @babel/generator (opens new window)
可以将 AST
转换、生成为代码。
# @babel/template (opens new window)
AST
是一个比较复杂的对象,如果要手动生成这个巨大的 AST
那将是一个苦力活。
@babel/template
,能让我们通过更简单、熟悉的写代码字符串的方式来生成 AST
,并且支持字符串模版。
import template from '@babel/template'
import generate from '@babel/generator'
import * as t from '@babel/types'
const buildRequire = template(`
var IMPORT_NAME = require(SOURCE);
`)
const ast = buildRequire({
IMPORT_NAME: t.identifier('myModule'),
SOURCE: t.stringLiteral('my-module')
})
console.log(generate(ast).code)
# 编写一个 Babel 插件
下面写一个插件将代码里的 console.log
调用删除。
# 插件基本骨架
从 writing-your-first-babel-plugin (opens new window) 可知,插件可以是下方这么一个函数。
export default function(babel) {
return {
visitor: {
Identifier(path, state) {},
Function(path, state) {}
}
}
}
函数传入了 babel (opens new window) 对象,里面也包含了一些常用的对象、方法,比如可用于判断节点类型的 babel.types
。
该函数会返回一个 visitor
对象,在 Babel
执行时就会调用 visitor
里面对应的方法,并传入当前路径信息 path
和状态 state
,state
中可以拿到调用插件时的参数。
因为要删除 console.log
的方法调用,所以我们在 visitor
中指定访问调用表达式 CallExpression (opens new window)。更多的 AST
节点类型可从 @babel/types (opens new window) 中查看。
新建 src/index.js
,插件基本代码骨架如下。
module.exports = function(babel) {
return {
visitor: {
CallExpression(path, state) {
console.log(path)
}
}
}
}
# 测试驱动开发
接触过单元测试的同学可能能够想到可以使用 @babel/core (opens new window) 的 API
方法调用目标插件,然后对经过 Babel
转换的 AST
或代码输出、使用 jest
等工具来完成单元测试。
但官方给出了更加简单的专门用于测试 Babel
插件的 package
,babel-plugin-tester (opens new window)。
安装依赖。
npm install --save-dev jest @babel/core babel-plugin-tester
编写单元测试。
新建
__tests__/index.spec.js
。下方case
表示期望把源码source
经过编译后转换为删了console.log
调用的代码。babel-plugin-tester
的详细用法请看 https://github.com/kentcdodds/babel-plugin-tester (opens new window)。const pluginTester = require('babel-plugin-tester').default const plugin = require('../src/index') const source = ` const a = 123; console.log(a) alert(123) ` const expect = ` const a = 123; alert(123) ` pluginTester({ plugin, pluginName: 'remove console', tests: [ { code: source, output: expect } ] })
添加
npm script
"scripts": { "test": "jest" }
配置完成后执行 npm test
可以看见单元测试运行起来,并且上方的 case
没有通过。
# 完善插件逻辑
下方相关代码可以从 https://github.com/xuwenchao66/babel-plugin-development (opens new window) 中进行查阅。
单元测试配置完成之后,我们开始补充插件逻辑,目标是通过单测的 case
。
因为 AST
的节点 node
就是一个对象,所以可以像访问一个对象一样访问 node
的属性。
AST
的属性多且复杂,那么可以在 https://astexplorer.net/ (opens new window) 上先看看所需要操作的 AST
的节点大概长什么样,或者参考一些已有的插件源码,配合断点调试,这样在插件开发初期就不会太盲目。
了解到了 node
节点有一个 callee
属性表示所调用的方法,callee.object
就是方法的所属对象, callee.property
就是调用的属性方法。所以我们可以使用下方的条件判断来找出 console.log
调用,最后通过 path.remove
来将该 node
删除。
module.exports = function(babel) {
return {
visitor: {
CallExpression(path) {
const { callee } = path.node
if (
callee.object &&
callee.property &&
callee.object.name === 'console' &&
callee.property.name === 'log'
) {
path.remove()
}
}
}
}
}
再次执行 npm test
,单元测试通过。
像上面的 node
类型判断,Babel
提供了一些实用的工具来简化开发。无论是 console
还是 log
都属于标识符(Identifier
),所以可以用 @babel/types (opens new window)
的 isIdentifier
方法来简化验证 node
的代码。
代码可以调整为。
module.exports = function({ types: t }) {
return {
visitor: {
CallExpression(path) {
const { callee } = path.node
if (
t.isIdentifier(callee.object, { name: 'console' }) &&
t.isIdentifier(callee.property, { name: 'log' })
) {
path.remove()
}
}
}
}
}
如果想要了解更多 Babel
插件开发的相关资料,推荐阅读 Babel Plugin Handbook (opens new window),这里有更详细的介绍,包括了插件的基本实现以及最佳实践推荐。