摘要:掘金小册《Babel 插件通关秘籍》笔记
Babel 简介
babel 的名字来自巴别塔的典故,它是一个 js 转义器,主要有三个功能:
1、转译 esnext、typescript、flow 等到目标环境支持的 js
2、使用其暴露的 api 可以完成 AST 的转换,以及目标代码的生成,做一些特定用途的代码转换
3、可以对得到的 AST 结构进行静态分析
编译流程
高级语言到高级语言的转换工具,被叫做转换编译器,简称转译器。Babel 就是一个 JavaScript 的转义器。转译器一般都是 parse、transform、generate 这 3 个阶段:
1、parse,通过 parser 把源码转成抽象语法树 AST,整个过程包含词法分析和语法分析。
2、transform,遍历 AST,调用各种 transform 插件对 AST 进行增删改
3、generate,把转换后的 AST 打印成目标代码,并生成 sourcemap。sourcemap 记录了源码到目标代码的转换关系,可以通过它找到目标代码中每一个节点对应的源码位置。
抽象语法树 AST
抽象语法树的概念:
在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
抽象语法树中省略了源码中的分隔符、注释等内容。字面量、标识符、表达式、语句、模块语法、class 语法都有各自的 AST。astexplorer.net可视化的查看 AST。
常见的 AST 节点
有的节点可能会是多种类型,判断 AST 节点是不是某种类型要看它是不是符合该种类型的特点。全部 AST、typescript 类型定义
1、Literal 字面量
字面量是由语法表达式定义的常量,或通过由一定字词组成的语词表达式定义的常量。字面量的值是固定的,而且在程序脚本运行中不可更改,比如 false,3.1415 等。字面量 (Literals)
字面量的种类很多:字符串字面量 StringLiteral、数字字面量 NumericLiteral,布尔字面量 BooleanLiteral 等。名字的英文结构就是什么字面量就是在名称其后面添加 Literal 后缀:xxLiteral xx字面量。
2、Identifer 标识符
标识符只能包含字母或数字或下划线(”_”)或美元符号(”$”),且不能以数字开头。变量名、属性名、参数名等各种声明和引用的名字都属于标识符 Identifer。
3、Statement 语句
语句是代码执行的最小单位,每一条可以独立执行的代码都是语句 Statement。语句末尾一般会加一个分号分隔,或者用换行分隔。比如:break、continue、debugger、return 或者 if 语句、while 语句、for 语句、声明语句、表达式语句等。语句的 AST 结点名字与字面量 Literal 类似,在名称后面添加后缀 Statement:xxStatement xx语句。
4、Declaration 声明语句
声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个变量、函数、class、import、export 等,对应的 AST 结点的命名规则也是名称加 Declaration 后缀: xxDeclaration xx声明语句
5、Expression 表达式
表达式与语句的区别是:表达式执行完以后有返回值。但有的表达式可以独立作为语句执行,会包裹一层 ExpressionStatement,代表这个表达式是被当成语句执行的。
6、Class 类
Class 属于声明语句,整个 class 的内容是 ClassBody,属性是 ClassProperty,方法是 ClassMethod。
7、模块语法
7.1、Import
import 的 3 种语法都是 ImportDeclaration 结点,但 specifiers 属性不同。
7.2、Export
export 也有三种语法,对应的也是三种节点,但不是三种结点都有 specifiers 属性。
7.3、File & Comment
Program 是包裹具体执行语句的节点,它有 body 属性代表程序体,directives 属性存放代码中的指令部分(”use strict” )。
7.4 Program & Directive
babel 的 AST 最外层节点是 File,它有 program、comments、tokens 等属性,分别存放 Program 程序体、注释、token 等,是最外层节点。
AST 的公共属性:
1、type:AST 节点的类型
2、start、end:对应源码字符串的开始、结束下标
3、loc:它是一个对象,包含有 line 和 column 属性分别记录对应源码的开始和结束行列号
4、leadingComments、innerComments、trailingComments:表示开始的注释、中间的注释、结尾的注释
5、extra:记录一些额外的信息,用于处理一些特殊情况
Babel 的 API
主要是以下几个包的使用:
1、@babel/parser 对源码进行 parse,可以通过 plugins、sourceType 等来指定 parse 语法
2、@babel/traverse 通过 visitor 函数对遍历到的 ast 进行处理,分为 enter 和 exit 两个阶段,具体操作 AST 使用 path 的 api,还可以通过 state 来在遍历过程中传递一些数据
3、@babel/types 用于创建、判断 AST 节点,提供了 xxx、isXxx、assertXxx 的 api
4、@babel/template 用于批量创建节点
5、@babel/code-frame 可以创建友好的报错信息
6、@babel/generator 打印 AST 成目标代码字符串,支持 comments、minified、sourceMaps 等选项。
7、@babel/core 基于上面的包来完成 babel 的编译流程,可以从源码字符串、源码文件、AST 开始。
具体的 api 可以查看文档
@babel/parser
目前大部分的 parser 都是基于 acorn,acorn 是按 estree 标准实现的。acorn 速度快,而且支持插件。acorn 主要是一个 Parser 类,不同的方法实现了不同的逻辑,插件扩展就是继承这个 Parser,然后重写一些方法。
@babel/traverse
traverse 的第一个参数是指定要遍历的 AST 节点,第二个参数就是指定 visitor 的对象(包含 enter 或者 exit 时的处理函数)或 enter 时的处理函数。处理函数也是两个参数,第一个参数 path,第二个是 state。path 包含大量的属性和方法,也是是修改 AST 的主要操作对象,state 则包含存储一些遍历过程中的共享数据。
@babel/generate
generate 是把 AST 打印成字符串,是一个从根节点递归打印的过程,它会把抽象语法树中省略掉的一些分隔符重新加回来。同时还可以生成 sourcemap,sourcemap 是源码和目标代码的映射,sourcemap 是源码和目标代码的映射。babel 通过 mozilla 维护的 source-map 这个包来生成的 sourcemap。
@babel/code-frames
主要用来直接准确的打印报错位置的代码。原理:先分割字符串成每一行的数组,然后根据传入的位置计算出 marker(>) 所在的位置。接着对每一行做处理,如果本行有标记,则拼成 marker + gutter(行号) + 代码的格式,下面再打印一行 marker,最后的 marker 行打印 message。没有标记不处理。
plugin
babel 通过配置 plugin 字段来引入各种插件拓展自身的功能,plugin 的数组里的元素可以是字符串或者数组,如果元素是数组,那么数组第二个元素就是该插件的参数。开发的 plugin 插件有返回对象的函数和对象两种格式。函数形式有三个参数,第一个参数是包含了 babel 的各种 api 的对象,第二个参数就是外面传入的配置项 options,第三个参数是目录名。对象形式主要用于不需要处理参数的情况。
每个 plugin 的功能都比较单一,当需要引入很多 plugin 时会很繁琐,并需要配置一堆的参数。这时就需要使用 preset 来简化这个配置操:preset 相当于是将一些 plugin 一同引入好的 plugin 集合。只需要将集合的名字设置到 perset 选项字段上,那么集合中的 plugin 都会一同引入。而且还可以通过 @babel/core 提供的 createConfigItem 方法来抽离 plugin 和 preset 的配置项。
preset 与 plugin 的配置形式相同,但应用顺序不同。babel 会先应用 plugin,再应用 preset;plugin 从前到后应用,preset 从后到前应用。
在定义插件名字的时候,最好是 babel-plugin-xx 和 @scope/babel-plugin-xx 这两种形式,这样就可以简单写为 xx 和 @scope/xx。
单元测试
测试 plugin 插件的常用方法是:得到测试转换后生成的代码后,判断是否符合预期。babel 提供了 babel-plugin-tester 来对比生成的代码,并有三种对比方式:直接对比字符串、指定输入和输出的代码文件和实际执行结果对比、生成快照对比快照。
内置功能
内置 plugin
babel 插件需要转换的特性包括 es 标准、proposal,还有 react、flow、typescript 等。proposal 分为几个阶段:
1、阶段 0 - Strawman: 只是一个设想
2、阶段 1 - Proposal: 提案阶段,比较正式的提议
3、阶段 2 - Draft: 建立 spec
4、阶段 3 - Candidate: 完成 spec 并且在浏览器实现
5、阶段 4 - Finished: 会加入到下一年的语言标准
babel 内置实现转换这些特性的插件分别为:syntax、proposal、transform。syntax plugin 的目的就是让 parser 能够准确的将某些语法特性解析成 AST,最终的 parse 逻辑还是 babel parser(babylon) 实现的。未加入语言标准的特性的 AST 转换插件叫 proposal plugin。各种将要加入标准的语言特性、typescript、jsx 等的转换都是在 transform plugin 里面实现的。
babel 的 plugin 大致就分为这三种:@babel/plugin-syntax-xxx、@babel/plugin-proposal-xxx、@babel/plugin-transform-xxx。
内置 preset
用于不同的目的需要不同的插件:
1、不同版本的语言标准支持:之前是 preset-es2015、preset-es2016 等,babel7 后用 preset-env 代替
2、未加入标准的语言特性的支持:之前用 stage0、stage1、stage2 的特性,babel7 后单独引入 proposal plugin
3、对 react、jsx、flow 的支持:分别封装相应的插件为 preset-react、preset-jsx、preset-flow
共享机制 helper
babel helpers 是用于 babel plugin 逻辑复用的一些工具函数,分为用于注入 runtime 代码的 helper 和用于简化 AST 操作 的 helper 两种。前者都在 @babel/helpers 包里,直接 this.addHelper(name) 就可以引入,而后者需要手动引入包和调用 api。
babel runtime
babel runtime 里面放运行时加载的模块,会被打包工具打包到产物中,并在运行时加载,它包括 helper、regenerator、core-js 三部分。
@babel/compat-data
@babel/compat-data 这个包里面维护了各种特性与环境支持版本的映射关系,借助 browerslist 可以查询到各环境的版本支持情况。
@babel/preset-env
@babel/compat-data 提供环境支持的特性信息,再由 browserslist 指定需要支持的目标环境,这样就知道了哪些特性是目标环境所不支持的,之后使用 preset-env 按需引入这些不支持特性对应的插件。preset-env 的配置是重点,文档
@babel/plugin-transform-runtime
preset-env 会在使用到新特性的地方注入 helper 到 AST 中,并且会引入用到的特性的 polyfill,但这么做会重复注入 helper 导致代码冗余,并且 polyfill 会污染全局环境。使用 @babel/plugin-transform-runtime 插件可以把直接注入全局的方式改成模块化引入,这样多模块复用的是同一份代码,且模块化引入 polyfill 也不会污染全局。
因为处理机制是先 plugin 再 preset,plugin 从左到右,preset 从右到左。这样使得 plugin-transform-runtime 的执行是在 preset-env 前面的,而且 plugin-transform-runtime 还不支持 targets 的精确配置,这样会出现问题:如果环境中已经支持某一特性,但 plugin-transform-runtime 仍然会将其转换,然后再交给 preset-env 处理。这样会导致 preset-env 的配置无效果,整个过程会有多余的转换和 polyfill。babel8 会解决这个问题:babel8 不再需要 transform-runtime 插件,而且还支持了 polyfill provider 的配置。