前端需要掌握的Babel知识
Babel 是怎么工作的
Babel
是一个 JavaScript
编译器。
做与不做
注意很重要的一点就是,Babel
只是转译新标准引入的语法,比如:
- 箭头函数
- let / const
- 解构
哪些在 Babel 范围外?对于新标准引入的全局变量、部分原生对象新增的原型链上的方法,Babel 表示超纲了。
- 全局变量
- Promise
- Symbol
- WeakMap
- Set
- includes
- generator 函数
对于上面的这些 API,Babel
是不会转译的,需要引入 polyfill
来解决。
Babel 编译的三个阶段
Babel 的编译过程和大多数其他语言的编译器相似,可以分为三个阶段:
- 解析(Parsing):将代码字符串解析成抽象语法树。
- 转换(Transformation):对抽象语法树进行转换操作。
- 生成(Code Generation): 根据变换后的抽象语法树再生成代码字符串。
为了理解 Babel
,我们从最简单一句 console
命令下手
解析(Parsing)
Babel
拿到源代码会把代码抽象出来,变成 AST
(抽象语法树),学过编译原理的同学应该都听过这个词,全称是 Abstract Syntax Tree。
抽象语法树是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,只所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,
比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现,它们主要用于源代码的简单转换。
console.log('zcy');
的 AST 长这样:
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"computed": false,
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "log"
}
},
"arguments": [
{
"type": "Literal",
"value": "zcy",
"raw": "'zcy'"
}
]
}
}
],
"sourceType": "script"
}
上面的 AST
描述了源代码的每个部分以及它们之间的关系,可以自己在这里试一下 astexplorer。
AST 是怎么来的?
整个解析过程分为两个步骤:
- 分词:将整个代码字符串分割成语法单元数组 在线分词工具
语法单元通俗点说就是代码中的最小单元,不能再被分割,就像原子是化学变化中的最小粒子一样。Javascript
代码中的语法单元主要包括以下这么几种:
- 关键字:
const
、let
、var
等 - 标识符:可能是一个变量,也可能是 if、else 这些关键字,又或者是 true、false 这些常量
- 运算符
- 数字
- 空格
- 注释:对于计算机来说,知道是这段代码是注释就行了,不关心其具体内容
其实分词说白了就是简单粗暴地对字符串一个个遍历。为了模拟分词的过程,写了一个简单的 Demo,仅仅适用于和上面一样的简单代码。Babel 的实现比这要复杂得多,但是思路大体上是相同的。
- 语法分析:建立分析语法单元之间的关系
语义分析是将得到的词汇进行一个立体的组合,确定词语之间的关系。考虑到编程语言的各种从属关系的复杂性,语义分析的过程又是在遍历得到的语法单元组,相对而言就会变得更复杂。
简单来说语法分析是对语句和表达式识别,这是个递归过程,在解析中,Babel
会在解析每个语句和表达式的过程中设置一个暂存器,用来暂存当前读取到的语法单元,如果解析失败,就会返回之前的暂存点,再按照另一种方式进行解析,
如果解析成功,则将暂存点销毁,不断重复以上操作,直到最后生成对应的语法树。
转换(Transformation)
Plugins
插件应用于babel
的转译过程,尤其是第二个阶段Transformation
,如果这个阶段不使用任何插件,那么babel
会原样输出代码。
Presets
plugins是一个小型的js代码程序,告诉Babel如何转换你的源码,比如 @babel/plugin-transform-arrow-functions
的作用就是将es2015的箭头函数转换成普通函数:
有那么多新的语法, 我们总不能一个一个的引入吧,于是就产生了预设: Presets,
官方帮我们做了一些预设的插件集,称之为
顾名思义——预设,它包含了一组我们需要的plugins BabelPreset
,
这样我们只需要使用对应的 Preset 就可以了, 而 babel-preset-env
相当于 ES2015 ,ES2016 ,ES2017 及最新版本。
Polyfill
中文翻译是垫片,之前没有详细了解babel之前,我也很迷茫这个polyfill是啥,因为语法不都给你转换好了,还需要这个东西干啥,后来仔细想了一下,要适应新特性应该从两方面入手:
-
语法转换:
() => {};
for (let i of items) {};
比如箭头函数、for...of,在不支持这些语法的环境下,直接会报语法错误,因为编译器根本不知道 =>
这些是什么鬼符号,要做到让编译器识别,那就要将这样的语法转换成浏览器能识别的代码,那么就需要语法转换。
2.功能补充
比如 'foo'.includes('f'),
es2015里不仅只有新的语法,还有实例的扩展,比如String,其实这里只是调用了String实例的一个方法,我们无论怎么语法转换也没有什么用吧,如果我们在不支持String.prototype.includes的编译器里跑这些代码,会得到 'foo'.includes is not a function. 这样的一个报错,而不是语法报错。
Polyfill提供的就是一个这样功能的补充,实现了Array、Object等上的新方法,实现了Promise、Symbol这样的新Class等。
虽然@babel/polyfill提供了我们想要的所有新方法新类,但是这里依然存在一些问题:
- 体积太大:比如我只用了String的新特性,但是我把整个包都引进来了,这不是徒增了很多无用的代码。
- 污染全局环境:如果你引用了
@babel/polyfill
,那么像Promise这样的新类就是挂载在全局上的,这样就会污染了全局命名空间。可能在一个团建建立的项目问题不太大,但是如果你是一个工具的开发者,你把全局环境污染了,别人用你的工具,就有可能把别人给坑了。
一个解决方案就是引入transform runtime 来替代 @babel/polyfill,像下面这样的配置:
{
"plugins": [
["transform-runtime", {
"helpers": false, //自动引入helpers
"polyfill": false, //自动引入polyfill(core-js提供的polyfill)
"regenerator": true, //自动引入regenerator
}]
]
}
另一个解决方案就是 @babel/preset-env 这个preset,它有一个useBuiltIns选项,如果设置成"usage"
,那么将会自动检测语法帮你require你代码中使用到的功能。
const presets = [
[
"@babel/env",
{
useBuiltIns: "usage",
},
],
];
比如我在代码中:
Promise.resolve().finally();
如果在edge17不支持这个特性的环境里运行,将会帮你编译成:
require("core-js/modules/es.promise.finally");
Promise.resolve().finally();
比较 transform-runtime 与 babel-polyfill 引入垫片的差异:
1.使用runtime是按需引入,需要用到哪些polyfill,runtime就自动帮你引入哪些,不需要再手动一个个的去配置plugins,只是引入的polyfill不是全局性的,有些局限性。而且runtime引入的polyfill不会改写一些实例方法,比如Object和Array原型链上的方法,像前面提到的Array.protype.includes。
2.babel-polyfill就能解决runtime的那些问题,它的垫片是全局的,而且全能,基本上ES6中要用到的polyfill在babel-polyfill中都有,它提供了一个完整的ES6+的环境。babel官方建议只要不在意babel-polyfill的体积,最好进行全局引入,因为这是最稳妥的方式。
3.一般的建议是开发一些框架或者库的时候使用不会污染全局作用域的babel-runtime,而开发web应用的时候可以全局引入babel-polyfill避免一些不必要的错误,而且大型web应用中全局引入babel-polyfill可能还会减少你打包后的文件体积(相比起各个模块引入重复的polyfill来说)。
Plugin/Preset 路径
如果 Plugin 是通过 npm 安装,可以传入 Plugin 名字给 Babel,Babel 将检查它是否安装在 node_modules
中
"plugins": ["babel-plugin-myPlugin"]
也可以指定你的 Plugin/Preset 的相对或绝对路径。
"plugins": ["./node_modules/asdf/plugin"]
Plugin/Preset 排序
如果两次转译都访问相同的节点,则转译将按照 Plugin 或 Preset 的规则进行排序然后执行。
- Plugin 会运行在 Preset 之前。
- Plugin 会从第一个开始顺序执行。
- Preset 的顺序则刚好相反(从最后一个逆序执行)。
例如:
{
"plugins": [
"transform-decorators-legacy",
"transform-class-properties"
]
}
将先执行transform-decorators-legacy
再执行transform-class-properties
但 preset 是反向的
{
"presets": [
"es2015",
"react",
"stage-2"
]
}
会按以下顺序运行:stage-2
,react
, 最后es2015
。
那么问题来了,如果presets
和plugins
同时存在,那执行顺序又是怎样的呢?答案是先执行plugins
的配置,再执行presets
的配置。所以以下代码的执行顺序为
- @babel/plugin-proposal-decorators
- @babel/plugin-proposal-class-properties
- @babel/plugin-transform-runtime
- @babel/preset-env
// .babelrc 文件
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
"@babel/plugin-transform-runtime",
]
}
生成(Code Generation)
用 babel-generator
通过 AST 树生成 ES5 代码
演示代码地址:点我
参考 :
前端工程师必须掌握的Babel知识 , Babel 7.1介绍 , Babel教程 , Babel该如何配置