平时喜欢看一些文章,从中可以学到一些零零散散的东西,但是不够全面与详细,而且实战也不够,于是决定开始系统学babel,并记录下来,然后做些实战练习,。
关于babel,好像是之前学习webpack时接触到的,我还叫babel [ˈbʌbl]
,然后百度搜索的正确读法是babel [ˈbeɪbl]
, 之前大概的了解是es6 to es5
,是有这作用,但不止,现在就来一探究竟。
babel的用途
转译
这个是最常用的功能,用来把代码中的 esnext
的新的语法、typescript
和 flow
的语法转成基于目标环境支持的语法的实现。并且还可以把目标环境不支持的api
进行 polyfill
。
代码转换
babel
是一个转译器,暴露了很多api
,用这些api
可以完成代码到AST
的 parse
,AST
的转换,以及目标代码的生成。
babel的编译流程
babel
是 source to source
的转换,整体编译流程分为三步:
parse
:通过 parser 把源码转成抽象语法树(AST)transform
:遍历AST
,调用各种transform
插件对AST
进行增删改generate
:把转换后的AST
打印成目标代码,并生成sourcemap
为了让计算机理解代码需要先对源码字符串进行 parse,生成 AST,把对代码的修改转为对 AST 的增删改,转换完 AST 之后再打印成目标代码字符串。
parse
parse
阶段的目的是把源码字符串转换成机器能够理解的 AST
,这个过程分为词法分析、语法分析。
比如 const name = 'miku39';
这样一段源码,我们要先把它分成一个个不能细分的单词(token)
,也就是 const
, name
, =
, 'miku39'
,这个过程是词法分析,按照单词的构成规则来拆分字符串成单词
之后要把 token
进行递归的组装,生成 AST
,这个过程是语法分析,按照不同的语法结构,来把一组单词组合成对象。
transform
transform
阶段是对 parse
生成的 AST
的处理,会进行 AST
的遍历,遍历的过程中处理到不同的 AST
节点会调用注册的相应的 visitor
函数,visitor
函数里可以对AST
节点进行增删改,返回新的 AST
(可以指定是否继续遍历新生成的 AST)。这样遍历完一遍 AST
之后就完成了对代码的修改。
generate
generate
阶段会把AST
打印成目标代码字符串,并且会生成 sourcemap
。不同的 AST
对应的不同结构的字符串。比如 IfStatement
就可以打印成 if(test) {}
格式的代码。这样从AST
根节点进行递归打印,就可以生成目标代码的字符串。sourcemap
记录了源码到目标代码的转换关系,通过它我们可以找到目标代码中每一个节点对应的源码位置。
parse阶段
parse
阶段有@babel/parser
,@babel/parser
是babel的一个api,功能是把源码转成 AST, babel parser
叫 babylon,是基于 acorn 实现的,扩展了很多语法,可以支持 es next(现在支持到 es2020)、jsx、flow、typescript
等语法的解析,其中 jsx、flow、typescript
这些非标准的语法的解析需要指定语法插件。
它提供了有两个 api:parse
和 parseExpression
。两者都是把源码转成 AST,不过 parse
返回的 AST 根节点是 File(整个 AST),parseExpression
返回的 AST 根节点是是 Expression(表达式的 AST),粒度不同。
function parse(input: string, options?: ParserOptions): File
function parseExpression(input: string, options?: ParserOptions): Expression
复制代码
详细的 options 可以查看文档。其实主要分为两类,一是 parse 的内容是什么,二是以什么方式去 parse
parse 的内容是什么:
plugins
: 指定jsx、typescript、flow
等插件来解析对应的语法allowXxx
: 指定一些语法是否允许,比如函数外的 await、没声明的 export等sourceType
: 指定是否支持解析模块语法,有module、script、unambiguous
3个取值,module
是解析 es module 语法,script
则不解析 es module语法,当作脚本执行,
unambiguous` 则是根据内容是否有 import 和 export 来确定是否解析 es module 语法。
以什么方式 parse
strictMode
是否是严格模式startLine
从源码哪一行开始 parseerrorRecovery
出错时是否记录错误并继续往下 parsetoken
parse 的时候是否保留 token 信息range
是否在 ast 节点中添加 ranges 属性
其实最常用的 option 就是 plugins、sourceType 这两个,比如要 parse tsx 模块,那么就可以这样来写
require("@babel/parser").parse("code", {
sourceType: "module",
plugins: [
"jsx",
"typescript"
]
});
复制代码
transform
阶段
transform
阶段是将 parse 出的 AST 由 @babel/traverse
来遍历和修改,babel traverse
包提供了 traverse
方法:
function traverse(parent, opts)
复制代码
常用的就前面两个参数,parent 指定要遍历的 AST 节点,opts 指定 visitor 函数。babel 会在遍历 parent 对应的 AST 时调用相应的 visitor 函数。
遍历过程
visitor 对象的 value 是对象或者函数:
- 如果 value 为函数,那么就相当于是 enter 时调用的函数。
- 如果 value 为对象,则可以明确指定 enter 或者 exit 时的处理函数。
函数会接收两个参数 path 和 state。
visitor: {
Identifier (path, state) {},
StringLiteral: {
enter (path, state) {},
exit (path, state) {}
}
}
复制代码
enter 时调用是在遍历当前节点的子节点前调用,exit 时调用是遍历完当前节点的子节点后调用。
可以为单个节点的类型,也可以是多个节点类型通过 |
连接,还可以通过别名指定一系列节点类型。
// 进入 FunctionDeclaration 节点时调用
traverse(ast, {
FunctionDeclaration: {
enter(path, state) {}
}
})
// 默认是进入节点时调用,和上面等价
traverse(ast, {
FunctionDeclaration(path, state) {}
})
// 进入 FunctionDeclaration 和 VariableDeclaration 节点时调用
traverse(ast, {
'FunctionDeclaration|VariableDeclaration'(path, state) {}
})
// 通过别名指定离开各种 Declaration 节点时调用
traverse(ast, {
Declaration: {
exit(path, state) {}
}
})
复制代码
具体的别名有哪些在babel-types 的类型定义可以查。
path
path
是遍历过程中的路径,会保留上下文信息,有很多属性和方法,比如:
path.node
指向当前 AST 节点path.get、path.set
获取和设置当前节点属性的 pathpath.parent
指向父级 AST 节点path.getSibling、path.getNextSibling、path.getPrevSibling
获取兄弟节点path.find
从当前节点向上查找节点
这些属性和方法是获取当前节点以及它的关联节点的
path.scope
获取当前节点的作用域信息
这个属性可以获取作用域的信息
path.isXxx
判断当前节点是不是 xx 类型path.assertXxx
判断当前节点是不是 xx 类型,不是则抛出异常
isXxx、assertXxx 系列方法可以用于判断 AST 类型
path.insertBefore、path.insertAfter
插入节点path.replaceWith、path.replaceWithMultiple、replaceWithSourceString
替换节点path.remove
删除节点
这些方法可以对 AST 进行增删改
path.skip
跳过当前节点的子节点的遍历path.stop
结束后续遍历
这俩方法可以跳过一些遍历,当然,path 的 api 不是只有这些
state
第二个参数 state 则是遍历过程中在不同节点之间传递数据的机制,插件会通过 state 传递 options 和 file 信息,我们也可以通过 state 存储一些遍历过程中的共享数据。
generate
阶段
AST 转换完之后就要打印成目标代码字符串,通过 @babel/generator
包的 generate api
function (ast: Object, opts: Object, code: string): {code, map}
复制代码
第一个参数是要打印的 AST
第二个参数是 options,指定打印的一些细节,比如通过 comments 指定是否包含注释,通过 minified 指定是否包含空白字符
第三个参数当多个文件合并打印的时候需要用到
options 中常用的是 sourceMaps,开启了这个选项才会生成 sourcemap
const { code, map } = generate(ast, { sourceMaps: true })
复制代码
总结
babel是一个 js 转译器,用于 es next、typescript等代码的转换,同时还暴露出了 api 让开发者可以进行特定用途的转换,babel 编译流程的三个步骤 parse、transform、generate,分别用到babel的@babel/parser
、@babel/traverse
、@babel/generator
api
@babel/parser
对源码进行 parse,可以通过 plugins、sourceType 等来指定 parse 语法@babel/traverse
通过 visitor 函数对遍历到的 ast 进行处理,分为 enter 和 exit 两个阶段,具体操作 AST 使用 path 的 api,还可以通过 state 来在遍历过程中传递一些数据@babel/generator
打印 AST 成目标代码字符串,支持 comments、minified、sourceMaps 等选项。
当然bebel的api不止这些, 还得一点一点去学习扩展