bebel系列- 插件开发
我们知道 babel 的编译流程分为三步:parse、transform、generate,每一步都暴露了一些 api 出来。
@babel/parser
,功能是把源码转成 AST@babel/traverse
,可以遍历 AST,并调用 visitor 函数修改 AST,修改 AST 自然涉及到 AST 的判断、创建、修改等,这时候就需要 @babel/types
了,当需要批量创建 AST 的时候可以使用 @babel/template
来简化 AST 创建逻辑。@babel/generator
包@babel/code-frame
包@babel/core
提供,基于上面的包完成 babel 整体的编译流程,并实现插件功能。我们主要学习的就是 @babel/parser
,@babel/traverse
,@babel/generator
,@babel/types
,@babel/template
这五个包的 api 的使用。
第二个参数 state 则是遍历过程中在不同节点之间传递数据的机制,插件会通过 state 传递 options 和 file 信息,我们也可以通过 state 存储一些遍历过程中的共享数据。
遍历 AST 的过程中需要创建一些 AST 和判断 AST 的类型,这时候就需要 @babel/types
包。
isXxx 会返回 boolean 表示结果,而 assertXxx 则会在类型不一致时抛异常。
通过 @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 节点。
AST 转换完之后就要打印成目标代码字符串,通过 @babel/generator
包的 generate api
当有错误信息要打印的时候,需要打印错误位置的代码,可以使用@babel/code-frame
。
前面的包是完成某一部分的功能的,而 @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
插件做的事情就是通过 api 拿到 types、template 等,通过 state.opts 拿到参数,然后通过 path 来修改 AST。可以通过 state 放一些遍历过程中共享的数据,通过 file 放一些整个插件都能访问到的一些数据,除了这两种之外,还可以通过 this 来传递本对象共享的数据。
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。
{
"presets": [["@babel/preset-env", {
"targets": "> 0.25%, not dead",
"useBuiltIns": "usage",// or "entry" or "false"
"corejs": 3
}]]
}
preset-env 会在使用到新特性的地方注入 helper 到 AST 中,并且会引入用到的特性的 polyfill (corejs + regenerator),这样会导致两个问题:
解决这两个问题的思路就是抽离出来,然后作为模块引入,这样多个模块复用同一份代码就不会冗余了,而且 polyfill 是模块化引入的也不会污染全局环境。
babel 中插件的应用顺序是:先 plugin 再 preset,plugin 从左到右,preset 从右到左
解决: corejs 的重复注入和全局引入 polyfill 的两个问题。
注入的代码和 core-js 全局引入的代码转换成从 @babel/runtime-corejs3 中引入的形式。
plugin-transform-runtime 是在 preset-env 前面的。等 @babel/plugin-transform-runtime 转完了之后,再交给 preset-env 这时候已经做了无用的转换了。而 @babel/plugin-transform-runtime 并不支持 targets 的配置,就会做一些多余的转换和 polyfill。+