如何开发babel插件(入门)
前置知识
学习 babel 前,必须要了解的核心概念就是 AST。
同时,希望你的项目中已经安装好了 babel 相关依赖。
npm install @babel/generator @babel/parser @babel/traverse @babel/types babel-cli
什么是 AST ?
来自百科的解释:
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构 "源代码语法结构的一种抽象表示",注意这句话,它是我们理解 AST 的关键。这句话大概的意思就是,按照某种约定的规范,以树形的数据结构,把我们的代码描述出来,让 JS 引擎和转义器能够理解。 举个栗子:react、vue等前端框架中的虚拟DOM,其实就是把 HTML 的真实DOM描绘成 JS 的虚拟机DOM。对将在页面真实DOM修改的操作,在 JS 的虚拟DOM上执行一遍,从而得到最终的DOM结构,最后才反映到真实DOM上,以此减少对真实DOM的操作,降低对浏览器性能的消耗。而对于底层的代码来说,AST 就相对于它们的虚拟机DOM。 当然,AST 不是 JS 特有的,每个语言都可以转换成对应的 AST,并且 AST 结构的规范也有很多,不同的语言对应不同的规范,而 JS 使用的规范大部分是estree,对于这个规范我们只做简单的了解即可。
AST 长什么样?
对 AST 有个基本的理解后,那 AST 到底长什么样? astexplorer.net这个网站可以在线生成AST, 我们可以在里面进行尝试生成AST,用来学习一下 AST 的结构。
babel处理过程
在了解完 AST 后,我们可以开始进入 babel 的学习了。 首先,babel 作为我们即熟悉又陌生的工具,它帮我们处理代码的时候,大致分为以下几步。
- 解析,对代码进行 AST 编译,得到代码的 AST 结构
- 转换,按我们的要求,对代码的 AST 进行处理,得到处理后的 AST 结构
- 生成,将 AST 转回成并生成我们的目标代码
解析
通过 parser 把源码转换成抽象语法书 AST 。 这个阶段的主要任务就是把代码转成 AST,其中经过两个阶段,一个是词法解析和语法解析。当 parser 阶段开始时,首先会进行文档扫描,并在此期间进行词法分析。举例:“const a = 1” 会被词法分析拆解为颗粒度最细的标记(tokens): “const”、“a”、"="、“1”。
词法分析结束后,将分析得到的 tokens 交给语法分析。而语法分析的主要任务就是根据 tokens 生成 AST。它会对 tokens 进行遍历,最终生成特定结构的 tree,而这个 tree 就是 AST。 以 const a = 1 AST 结构为例:
{"type":"File","start":0,"end":11,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":11}},"errors":[],"program":{"type":"Program","start":0,"end":11,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":11}},"sourceType":"script","interpreter":null,"body":[{"type":"VariableDeclaration","start":0,"end":11,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":11}},"declarations":[{"type":"VariableDeclarator","start":6,"end":11,"loc":{"start":{"line":1,"column":6},"end":{"line":1,"column":11}},"id":{"type":"Identifier","start":6,"end":7,"loc":{"start":{"line":1,"column":6},"end":{"line":1,"column":7},"identifierName":"a"},"name":"a"},"init":{"type":"NumericLiteral","start":10,"end":11,"loc":{"start":{"line":1,"column":10},"end":{"line":1,"column":11}},"extra":{"rawValue":1,"raw":"1"},"value":1}}],"kind":"const"}],"directives":[]},"comments":[]}
如下所示,包裹的外层 type 为 VariableDeclaration,从单词意思可得知是声明,所使用的 kind 是 const 类型声明。而字段 declarations 描述中,还有 VariableDeclarator 声明对象,id 为声明对象的名称,同样也是个对象,它的 name 才是声明对象名称的值,init 为声明对象初始化的值(还是对象),里面的 value 才是这个初始值
{
"type": "Program",
"start": 0,
"end": 11,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 11,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 11,
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"name": "a"
},
"init": {
"type": "Literal",
"start": 10,
"end": 11,
"value": 1,
"raw": "1"
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
除了上面所说的字段外,还有包括第几行,第几列,值类型等详细信息。这就是我们所得到的 AST。
那在开发过程中,我们如何将代码转换为 AST 呢?这就需要 babel 提供的解析器 @babel/parser,之前叫 Babylon,它并非 babel 团队开发的,而是基于 fork 的 acorn 项目。 使用过程(已按照babel相关依赖):
const parser = require('@babel/parser');
const ast = parser.parse('const a = 1'); // 转换成AST
更多信息可以访问官方文档查看@babel/parser
转换
在 parse 解析阶段,我们已经成功得到 AST 了。babel 接受到 AST 后,会使用 @babel/traverse 对其进行深度优先遍历,插件会在这个阶段被触发,以 visitor 函数的形式,访问每种不同类型的 AST 节点。已上述为例,我们可以使用 VariableDeclaration 函数对 VariableDeclaration节点进行回调处理,每个该类型的节点,都会触发这个函数回调:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default
const ast = parser.parse('const a = 1'); // 转换成AST
traverse(ast, {
VariableDeclaration(path, state) {
// 操作处理...
}
});
这个函数有两个形参:
path
- path为当前访问的路径, 包含了节点的信息、父节点信息以及对节点操作的方法。可以利用这些方法,对 ATS 进行添加、更新、移动和删除等等操作。
state
- state包含了当前plugin插件的信息和参数信息等等,并且也可以用来自定义在节点之间传递数据。
生成
最后的阶段就是 generate,把转换后的 AST 打印成目标代码,并生成 sourcemap 这个阶段就比较简单了, 在转换阶段处理 AST 结束后,将 AST 转换回 code, 在此期间会对 AST 进行深度优先遍历,根据节点所包含的信息生成对应的代码,并且会生成对应的 sourcemap。
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const ast = parser.parse('const a = 1'); // 转换成AST
// 转换回code代码
traverse(ast, {
// 声明变量都会触发这个VariableDeclaration函数
VariableDeclaration(path, state) {
// 通过 path.node 访问实际的 AST 节点
path.node.kind = 'var'
}
});
// 将处理好的 AST 放入 generate
const transformedCode = generate(ast).code
console.log(transformedCode,"new Code")
开发成插件
从上面步骤分析可得知,我们重点关注的其实就是转换处理这个阶段。所以开发插件时,我们也只需关注这个阶段就行。而插件最终只需导出一个函数,该函数需要返回一个对象,而我们只需要改对象的 visitor ,这里就类似 traverse 转换处理阶段即可。 既然是函数,当然会接受几个参数:
- api, 继承了babel提供的一系列方法
- options, 是我们使用插件时所传递的参数
- dirname, 为处理时期的文件路径
项目目录结构
├── src
│ ├── index.js
│ ├── plugin.js
├── .babelrc
├── .editorconfig
├── README.md
├── package.json
└── pnpm-lock.yaml
plugin.js将ES6声明变量方式转var的babel插件代码如下:
module.exports = (api, options, dirname) => {
return {
visitor: {
VariableDeclaration(path, state) {
path.node.kind = 'var'
}
}
}
}
配置插件
在.babelrc文件中加入自己编写的插件
{
"plugins": [
"./src/plugin.js"
]
}
进行编译
在package.json文件中加入命令,指定转换后的文件输出到./dist/compiled.js
"scripts": {
: "babel ./src/index.js --out-file ./src/compiled.js"
},