bebel系列- 插件开发
我们知道 babel 的编译流程分为三步:parse、transform、generate,每一步都暴露了一些 api 出来。
- parse 阶段有
@babel/parser
,功能是把源码转成 AST - transform 阶段有
@babel/traverse
,可以遍历 AST,并调用 visitor 函数修改 AST,修改 AST 自然涉及到 AST 的判断、创建、修改等,这时候就需要@babel/types
了,当需要批量创建 AST 的时候可以使用@babel/template
来简化 AST 创建逻辑。 - generate 阶段会把 AST 打印为目标代码字符串,同时生成 sourcemap,需要
@babel/generator
包 - 中途遇到错误想打印代码位置的时候,使用
@babel/code-frame
包 - babel 的整体功能通过
@babel/core
提供,基于上面的包完成 babel 整体的编译流程,并实现插件功能。
我们主要学习的就是 @babel/parser
,@babel/traverse
,@babel/generator
,@babel/types
,@babel/template
这五个包的 api 的使用。
path
state
第二个参数 state 则是遍历过程中在不同节点之间传递数据的机制,插件会通过 state 传递 options 和 file 信息,我们也可以通过 state 存储一些遍历过程中的共享数据。
@babel/types
遍历 AST 的过程中需要创建一些 AST 和判断 AST 的类型,这时候就需要 @babel/types
包。
isXxx 会返回 boolean 表示结果,而 assertXxx 则会在类型不一致时抛异常。
@babel/template
通过 @babel/types 创建 AST 还是比较麻烦的,要一个个的创建然后组装,如果 AST 节点比较多的话需要写很多代码,这时候就可以使用 @babel/template
包来批量创建。
如果是根据模版创建整个 AST,那么用 template.ast 或者 template.program 方法,这俩都是直接返回 ast 的,template.program 返回的 AST 的根节点是 Program。
如果知道具体创建的 AST 的类型,可以使用 template.expression、template.statement、template.statements 等方法创建具体的 AST。
默认 template.ast 创建的 Expression 会被包裹一层 ExpressionStatement 节点(会被当成表达式语句来 parse),但当 template.expression 方法创建的 AST 就不会。
如果模版中有占位符,那么就用 template 的 api,在模版中写一些占位的参数,调用时传入这些占位符参数对应的 AST 节点。
@babel/generator
AST 转换完之后就要打印成目标代码字符串,通过 @babel/generator
包的 generate api
@babel/code-frame
当有错误信息要打印的时候,需要打印错误位置的代码,可以使用@babel/code-frame
。
@babel/core
前面的包是完成某一部分的功能的,而 @babel/core
包则是基于它们完成整个编译流程,从源码到目标代码,生成 sourcemap。
@babel/parser
对源码进行 parse,可以通过 plugins、sourceType 等来指定 parse 语法@babel/traverse
通过 visitor 函数对遍历到的 ast 进行处理,分为 enter 和 exit 两个阶段,具体操作 AST 使用 path 的 api,还可以通过 state 来在遍历过程中传递一些数据@babel/types
用于创建、判断 AST 节点,提供了 xxx、isXxx、assertXxx 的 api@babel/template
用于批量创建节点@babel/code-frame
可以创建友好的报错信息@babel/generator
打印 AST 成目标代码字符串,支持 comments、minified、sourceMaps 等选项。@babel/core
基于上面的包来完成 babel 的编译流程,可以从源码字符串、源码文件、AST 开始。
插入函数调用参数 实战
插入 AST 可以使用 path.insertBefore 的 api, 而替换整体节点用 path.replaceWith
有时你需要从一个路径向上遍历语法树,直到满足相应的条件。
对于每一个父路径调用callback
并将其NodePath
当作参数,当callback
返回真值时,则将其NodePath
返回。
路径和作用域
babel 会在 traverse 的过程中在 path 里维护节点的父节点引用,在其中保存 scope(作用域)的信息,同时也会提供增删改 AST 的方法。
parse – transform -generate
generator 和 sourcemap 的原理
- generate 就是递归打印 AST 成字符串,在递归打印的过程中会根据源码位置和计算出的目标代码的位置来生成 mapping,加到 sourcemap 中。
- sourcemap 是源码和目标代码的映射,用于开发时调试源码和生产时定位线上错误。 babel 通过 source-map 这个包来生成的 sourcemap,
plugin
插件做的事情就是通过 api 拿到 types、template 等,通过 state.opts 拿到参数,然后通过 path 来修改 AST。可以通过 state 放一些遍历过程中共享的数据,通过 file 放一些整个插件都能访问到的一些数据,除了这两种之外,还可以通过 this 来传递本对象共享的数据。
preset
plugin 是单个转换功能的实现,当 plugin 比较多或者 plugin 的 options 比较多的时候就会导致使用成本升高。这时候可以封装成一个 preset,用户可以通过 preset 来批量引入 plugin 并进行一些配置。preset 就是对 babel 配置的一层封装。
preset 格式和 plugin 一样,也是可以是一个对象,或者是一个函数,函数的参数也是一样的 api 和 options,区别只是 preset 返回的是配置对象,包含 plugins、presets 等配置。
顺序
preset 和 plugin 从形式上差不多,但是应用顺序不同。
babel 会按照如下顺序处理插件和 preset:
0.先应用 plugin,再应用 preset
1.plugin 从前到后,preset 从后到前
这个顺序是 babel 的规定。
名字
babel 希望插件名字中能包含 babel plugin,这样写 plugin 的名字的时候就可以简化,然后 babel 自动去补充。所以我们写的 babel 插件最好是 babel-plugin-xx 和 @scope/babel-plugin-xx 这两种,就可以简单写为 xx 和 @scope/xx。
通过 syntax transform
+ api polyfill
,我们就能在目标环境用高版本 javascript 的语法和 api。
@babel/preset-env
{
"presets": [["@babel/preset-env", {
"targets": "> 0.25%, not dead",
"useBuiltIns": "usage",// or "entry" or "false"
"corejs": 3
}]]
}
- corejs 就是 babel 7 所用的 polyfill,需要指定下版本,corejs 3 才支持实例方法(比如 Array.prototype.fill )的 polyfill。
- useBuiltIns 就是使用 polyfill (corejs)的方式,是在入口处全部引入(entry),还是每个文件引入用到的(usage),或者不引入(false)。
@babel/plugin-transform-runtime (类似于,webpack optimization 中的 runtime, 都是为了共用公共模块代码)
preset-env 会在使用到新特性的地方注入 helper 到 AST 中,并且会引入用到的特性的 polyfill (corejs + regenerator),这样会导致两个问题:
- 重复注入 helper 的实现,导致代码冗余
- polyfill 污染全局环境
解决这两个问题的思路就是抽离出来,然后作为模块引入,这样多个模块复用同一份代码就不会冗余了,而且 polyfill 是模块化引入的也不会污染全局环境。
babel 中插件的应用顺序是:先 plugin 再 preset,plugin 从左到右,preset 从右到左
解决: corejs 的重复注入和全局引入 polyfill 的两个问题。
注入的代码和 core-js 全局引入的代码转换成从 @babel/runtime-corejs3 中引入的形式。
babel7 存在的问题
plugin-transform-runtime 是在 preset-env 前面的。等 @babel/plugin-transform-runtime 转完了之后,再交给 preset-env 这时候已经做了无用的转换了。而 @babel/plugin-transform-runtime 并不支持 targets 的配置,就会做一些多余的转换和 polyfill。+
实现原理
- preset-env : 就是根据 targets 的配置查询内部的 @babe/compat-data 的数据库,过滤出目标环境不支持的语法和 api,引入对应的转换插件。
- @babel/plugin-transform-runtime: 因为插件在 preset 之前调用,所以可以提前把 polyfill 转换了,而且注入了 helpGenerator 来修改 @babel/preset-env 生成 helper 代码的行为。