欢迎大家来到IT世界,在知识的湖畔探索吧!
文章来自公众号@大转转FE,https://mp.weixin.qq.com/s/LlQRx5SPmFgnTDO8VunGnw
本文将引导你一步一步的学会babel,在学习的过程将着重介绍以下几点:
- babel转译的过程
- AST介绍
- babel常用的api
- @babel/preset-env
- @babel/plugin-transform-runtime
1.babel的作用
官方定义:Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
2.babel转译的三个阶段
- 源码 parse 生成 AST(parse)
- 遍历 AST 并进行各种增删改(核心)(transform)
- 转换完 AST 之后再打印成目标代码字符串(generate)
3. AST 如何生成
在学习AST和babel转换时,可以借助下面两个网站辅助查看代码转换成AST之后的结果。
https://esprima.org/demo/parse.html
https://astexplorer.net/
整个解析过程主要分为以下两个步骤:词法分析和语法分析
3.1 词法分析
词法分析,这一步主要是将字符流(char stream)转换为令牌流(token stream),又称为分词。其中拆分出来的各个部分又被称为 词法单元 (Token)。
可以这么理解,词法分析就是把你的代码从 string 类型转换成了数组,数组的元素就是代码里的单词(词法单元), 并且标记了每个单词的类型。
比如:
const a = 1
欢迎大家来到IT世界,在知识的湖畔探索吧!
生成的tokenList
欢迎大家来到IT世界,在知识的湖畔探索吧![
{ type: 'Keyword', value: 'const' },
{ type: 'Identifier', value: 'a' },
{ type: 'Punctuator',value: '=' },
{ type: 'Numeric', value: '1' },
{ type: 'Punctuator', value: ';' },
];
词法分析结果,缺少一些比较关键的信息:需要进一步进行 语法分析 。
3.2 语法分析
语法分析会将词法分析出来的 词法单元 转化成有语法含义的 抽象语法树结构(AST),同时,验证语法,语法如果有错,会抛出语法错误。
这里截取AST树上的program的body部分(采用@babel/parser进行转化)
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 11,
"loc": {},
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 11,
"loc": {},
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"loc": {},
"name": "a"
},
"init": {
"type": "NumericLiteral",
"start": 10,
"end": 11,
"loc": {},
"extra": {},
"value": 1
}
}
],
"kind": "const"
}
]
可以看到,经过语法分析阶段转换后生成的AST,通过树形的层属关系建立了语法单元之间的联系。
4. AST节点
转换后的AST是由很多AST节点组成,主要有以下几种类型:字面量(Literal)、标志符(Identifer)、语句(statement)、声明(Declaration)、表达式(Expression)、注释(Comment)、程序(Program)、文件(File)。
每种 AST节点都有自己的属性,但是它们也有一些公共属性:
- 结点类型(type):AST 节点的类型。
- 位置信息(loc):包括三个属性start、 end、 loc。其中start 和 end 代表该节点对应的源码字符串的开始和结束下标,不区分行列。loc 属性是一个对象,有 line 和 column 属性分别记录开始和结束行列号。
- 注释(comments):主要分为三种leadingComments、innerComments、trailingComments ,分别表示开始的注释、中间的注释、结尾的注释。
5. babel 常用的api
babel中有五个常用的api:
- 针对parse阶段有@babel/parser,功能是把源码转成 AST
- 针对transform 阶段有 @babel/traverse,用于增删改查AST
- 针对generate 阶段有@babel/generate,会把 AST 打印为目标代码字符串,同时生成 sourcemap
- 在transform阶段,当需要判断和生成结点时,需要@babel/types,
- 当需要批量创建 AST 的时候可以使用 @babel/template 来简化 AST 创建逻辑。
我们可以通过这些常用的api来自己实现一个plugin,对代码进行转换。接下来就介绍一下几个常见的api。
5.1 @babel/parser
babelParser.parse(code, [options])— 返回的 AST 根节点是File节点babelParser.parseExpression(code, [options])—返回的AST根结点是Expression
第一个参数是源代码,第二个参数是options,其中最常用的就是 plugins、sourceType 这两个:
- sourceType: 指示分析代码的模式,主要有三个值:script、module、unambiguous。
- plugins:指定要使用插件数组。
5.2 @babel/traverse(核心)
function traverse(ast, opts)
ast:经过parse之后的生成的ast
opts :指定 visitor 函数–用于遍历节点时调用(核心)
方法的第二参数中的visitor是我们自定义插件时经常用到的地方,你可以通过两种方式来定义这个参数
第一种是以方法的形式声明visitor
欢迎大家来到IT世界,在知识的湖畔探索吧!traverse(ast, {
BlockStatement(path, state) {
console.log('BlockStatement>>>>>>')
}
});
第二种是以对象的形式声明visitor
traverse(ast, {
BlockStatement: {
enter(path, state) {
console.log('enter>>>', path, state)
},
exit(path, state) {
console.log('exit>>>', path, state)
}
}
});
每一个visitor函数会接收两个参数 path 和 state,path用来操作节点、遍历节点和判断节点,而state则是遍历过程中在不同节点之间传递数据的机制, 我们也可以通过 state 存储一些遍历过程中的共享数据。
5.3. @babel/generator
转换完AST之后,就要打印目标代码字符串,这里通过@babel/generator来实现,其方法常用的参数有两个:
- 要打印的 AST
- options–指定打印的一些细节,比如comments指定是否包含注释
6. babel的内置功能
上面我们介绍了几个用于实现插件的api,而babel本身为了实现对语法特性的转换以及对api的支持(polyfill),也内置了很多的插件(plugin)和预设(preset)。
其插件主要分为三类:
- syntax plugin:只是在parse阶段使用,可以让 parser 能够正确的解析对应的语法成 AST
- transform plugin:是对 AST 的转换,针对es20xx 中的语言特性、typescript、jsx 等的转换都是在这部分实现的
- proposal plugin:未加入语言标准的特性的 AST 转换插件
那么预设是什么呢?预设其实就是对于插件的一层封装,通过配置预设,使用者可以不用关心具体引用了什么插件,从而减轻使用者的负担。
而根据上面不同类型的插件又产生了如下几种预设:
- 专门根据es标准处理语言特性的预设 — babel-preset-es20xx
- 对其react、ts兼容的预设 — preset-react preset-typescript
我们目前最常使用的便是 @babel/preset-env这个预设,下文将会通过一个例子来介绍它的使用。
7. 案例1–自定义插件
需求
如果有一行代码
const a = 1
我需要通过babel自定义插件来给标识符增加类型定义,让它成为符合ts规范的语句,结果:const a: number = 1。
实现
通过babel处理代码,其实就是在对AST节点进行处理。
我们先搭起一个架子
// 源代码
const sourceCode = `
const a = 1
`;
// 调用parse,生成ast
const ast = parser.parse(sourceCode, {})
// 调用traverse执行自定义的逻辑,处理ast节点
traverse(ast, {})
// 生成目标代码
const { code } = generate(ast, {});
console.log('result after deal with》〉》〉》', code)
在引入对应的包后,我们的架子主要分为三部分,我们首先需要知道这句话转换完之后的AST节点类型
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 11,
"loc": {...},
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 11,
"loc": {...},
"id": {...},
"init": {...}
}
],
"kind": "const"
}
]
上图可以看出这句话的类型是VariableDeclaration,所以我们要写一个可以遍历VariableDeclaration节点的visitor。
// 调用traverse执行自定义的逻辑,处理ast节点
traverse(ast, {
VariableDeclaration(path, state) {
console.log('VariableDeclaration>>>>>>', path.node.type)
}
})
继续观察结构,该节点下面有declarations属性,其包括所有的声明,declarations[0]就是我们想要的节点。
traverse(ast, {
VariableDeclaration(path, state) {
console.log('VariableDeclaration>>>>>>', path.node.type)
const tarNode = path.node.declarations[0]
console.log('tarNode>>>>>>', tarNode)
}
})
每一个声明节点类型为VariableDeclarator,该节点下有两个重要的节点,id(变量名的标识符)和 init(变量的值)。这里我们需要找到变量名为 a 的标识符,且他的值类型为number(对应的节点类型为NumericLiteral)。
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 11,
"loc": {...},
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"loc": {...},
"name": "a"
},
"init": {
"type": "NumericLiteral",
"start": 10,
"end": 11,
"loc": {...},
"extra": {...},
"value": 1
}
}
]
这时候就需要我们使用一个新的包 @babel/types 来判断类型。判断类型时只需调用该包中对应的判断方法即可,方法名都是以isXxx或者assertXxx来命名的(Xxx代表节点类型),需要传入对应的节点才能判断该节点的类型。
traverse(ast, {
VariableDeclaration(path, state) {
const tarNode = path.node.declarations[0]
if(types.isIdentifier(tarNode.id) && types.isNumericLiteral(tarNode.init))
{
console.log('inside>>>>>>')
}
}
})
锁定了节点后,我们需要更改id节点的name内容, 就可以实现需求了。
traverse(ast, {
VariableDeclaration(path, state) {
const tarNode = path.node.declarations[0]
if(types.isIdentifier(tarNode.id)&&types.isNumericLiteral(tarNode.init))
{
console.log('inside>>>>>>')
tarNode.id.name = `${tarNode.id.name}: number`
}
}
})
8. 案例2–工程化使用
8.1 准备工作
@babel/core是babel的核心库,@babel/cli是babel的命令行工具。如果要使用babel,首先要安装 @babel/core和@babel/cli。
源代码为:
const fn = () => 1 ;
位置放在src下的test.js文件。
8.2 通过配置文件使用
根据官方文档的说法,目前有两类配置文件:项目范围配置 和 文件相对配置。
1.项目范围配置(全局配置) — babel.config.json
2.文件相对配置(局部配置) — .babelrc.json、package.json
区别:第一种配置作用整个项目,如果 babel 决定应用这个配置文件,则一定会应用到所有文件的转换。而第二种配置文件只能应用到“当前目录”下的文件中。
babel 在决定一个 js 文件应用哪些配置文件时,会执行如下策略: 如果这个 js 文件在当前项目内,则会递归向上搜索最近的一个 .babelrc 文件(直到遇到package.json),将其与全局配置合并。
这里我们只需使用babel.config.json的形式进行配置
配置文件:
{
"presets": [
[
"@babel/preset-env"
]
]
}
再在package.json里配置一下执行的脚本
"dev": "./node_modules/.bin/babel src --out-dir lib"
8.4 常用的包
我们在工程里常用包主要有两个:
- @babel/preset-env
- @babel/plugin-transform-runtime
8.4.1 @babel/preset-env
@babel/preset-env是一个智能的预设,它允许你使用最新的JavaScript,而不需要微管理你的目标环境需要哪些语法转换,根据babel官网上的描述,它是通过browsersList、compat-table相结合来实现智能的引入语法转换工具。
compat-data形如如下,其注明了什么特性,在什么环境下支持,再结合通过browsersList查询出的环境版本号,就可以确定需要引入哪些plugin或者preset。
"es6.array.fill": {
"chrome": "45",
"opera": "32",
"edge": "12",
"firefox": "31",
"safari": "7.1",
"node": "4",
"ios": "8",
"samsung": "5",
"rhino": "1.7.13",
"electron": "0.31"
},
@babel/preset-env有三个常用的关键可选项:
- targets
- useBuiltIns
- corejs
targets
描述项目支持的环境/目标环境,支持browserslist查询写法
{ "targets": "> 0.25%, not dead" } // 全球使用人数大于0.25%且还没有废弃的版本
支持最小环境版本构成的对象
{ "targets": { "chrome": "58", "ie": "11" } }
如果没配置targets, Babel会假设你的目标是最老的浏览器 @babel/preset-env将转换所有ES2015-ES2020代码为ES5兼容
useBuiltIns
可以使用三个值:”usage” 、”entry” 、 false,默认使用false
false
当使用false时:在不主动import的情况下不使用preset-env来处理polyfills
entry
babel将会根据浏览器目标环境(targets)的配置,引入全部浏览器暂未支持的polyfill模块,只要我们在打包配置入口 或者 文件入口写入 import “core-js” 这样的引入, babel 就会根据当前所配置的目标浏览器(browserslist)来引入所需要的polyfill 。
usage
设置useBuiltIns的值为usage时,babel将会根据我们的代码使用情况自动注入polyfill。
8.4.2entry与usage的区别:
在上文所示例子的基础上,我们修改一下源代码
function test() {
new Promise()
}
test()
const arr = [ 1 , 2 , 3 , 4 ].map(item => item * item)
console.log(arr)
我们没有配置useBuiltIns时,preset-env只对代码的语法进行了处理,对于新增的api并没有引入对应的polyfill。下图是转换结果:
"use strict";
function test() {
new Promise();
}
test();
var arr = [1, 2, 3, 4].map(function (item) {
return item * item;
});
console.log(arr);
当我们使用useBuiltIns:“entry”时(入口文件需要引入core-js),由于我们没有指定targets,结果当然是引入了一堆包。
加入”targets”: “> 0.25%, not dead”后,很明显少了很多的引入(如下图所示),这也印证了上面所说的, 当 useBuiltIns 的值为 entry 时,@babel/preset-env 会按照你所设置的targets来引入所需的polyfill。
当我们使用useBuiltIns:“usage”时,这时就无须在入口文件引入core-js了。
可以看出引入的包非常精准,需要哪些就引入哪些polyfill。当然你也可以配置targets,这样的话targets会辅助preset-env引入,从而进一步控制引入包数量。
corejs
corejs是JavaScript的模块化标准库,其中包括各种ECMAScript特性的polyfill。上面我们转换后的代码中引入的polyfill都是来源于corejs。它现有2和3两个版本,目前2版本已经进入功能冻结阶段了,新的功能会添加到3版本中。
具体的变化可以查看corejs的github的说明文档:core-js@3, babel and a look into the future
这个选项只有在和 useBuiltIns: “usage” 或 useBuiltIns:”entry” 一起使用时才有效果,该属性默认为”2.0″。其作用是进一步约束引入的polyfill的数量。
8.4.3 @babel/plugin-transform-runtime
虽然经过了preset-env的转换,代码已经可以实现不同版本的特性兼容了。但是会产生两个问题:
1.preset-env转换后引入的polyfill,是通过require进行引入的,这就意味着,对于Array.from 等静态方法,以及 includes 等实例方法,会直接在 global 上添加。这就导致引入的polyfill方法可能和其他库发生冲突。
2.babel转换代码的时候,可能会使用一些帮助函数来协助转换,比如class
class a {}
转换之后:
这里就使用了_classCallCheck这样的辅助函数,如果有多个文件声明class的话,就会重复创建这样的方法。
@babel/plugin-transform-runtime这个插件的作用就是为了处理这样的问题。该插件也有一个corejs的配置,这里配置的是runtime-corejs 的版本,目前有 2、3 两个版本。
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": "3.0"
}
]
]
}
转换结果:
这里由于babel是先执行plugins后执行presets的内容,@babel/plugin-transform-runtime插件先于preset-env将polyfill引入了,并且做了一层包装,所以就无须再通过@babel/preset-env来引入polyfill了。
可以看到,转换之后的_classCallCheck的方法定义全部改为了从runtime-corejs中引入,对于新特性的polyfill也不再挂载在全局了。这样的方法适合定义类库时使用,可以防止变量污染全局。
结束语
相信通过这么一篇文章,大家基本都了解了babel的基础原理,以及它是如何实现对代码的转换的,并可以自己实现简单的一个babel的插件。当然,本文中所述之内容只是babel全部内容的十之一二,只是作一个学习babel的引路石,如果有较强烈的需求,还是要常翻阅官方文档。最后希望大家可以将bebel学的更透彻。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/41305.html