前言
在进行Webpack或者Rollup配置的时候,难免会进行Babel的相关配置,因为代码中往往会使用比较新的JS语言的特性,然而可能运行环境并不支持这些特性,这个时候就需要Babel去帮助实现转换。
Babel的作用
第一眼看到Babel时,会不知道如何下手去了解,需要花一段时间去理清各个新名词的意思和作用,本人自己一开始也没少看资料,但是一旦理解Babel主要的作用,就知道工具链上的各个名词就是为了实现这些主要功能:
- 语法转换
- 对目标环境进行特性补充
polyfill - 代码替换
这里最常用可能就是语法转换了,开发过程中时常会使用ES 2015+的语法,这个时候就需要使用babel来进行语法的转换,转换到运行环境支持的语法特性,另外还有一些运行环境不支持的特性,例如Promise,Array.prototype.includes等,也需要使用polyfill进行补充。
每一个上述的功能都牵涉到几个babel的工具包,下面就一层层地去介绍这些工具包:
命令行和核心包
babel进行编译解析,是存在两种方式的:
- 一种是通过编程的方式,也就是
@babel/core,在你的js文件中引入@babel/core,然后就可以调用使用其中的方法进行编译解析。
import { transform } from '@babel/core';
babel.transform("code();", options, function(err, result) {
console.log(result.code);
});
除了
babel.transform方法,@bable/core提供很多其他transform的API以供使用。
- 另一种是通过命令行进行触发,
babel提供@babel/cli工具包,开发者可以使用命令行的方式对代码进行转换,需要注意的是@bable/cli需要同时依赖@babel/core提供核心功能,cli提供多种参数以达到开发想要的编译效果:
babel script.js --out-file script-compiled.js
配置文件
如果了解编译原理的话,都知道编译的过程一般都经过词法分析、语法解析、生成中间代码、生成目标代码,而babel也类似,经过三个主要的流程解析、转换、生成。
- 解析阶段主要进行词法和语法解析,生成AST语法树,主要由
babyIon完成; - 转换阶段将AST语法树转换为目标的AST语法树,主要由
@babel/traverse完成; - 生成阶段将目标语法树生成最终代码,主要由
@babel/generator完成;
这里既然需要进行转换,那就必须知道如何转换,遵循怎样的转换规则,这里就需要配置文件与定义这些规则了。
babel的配置文件中包含presets和plugins两个配置项:
{
"presets": ["@babel/preset-env"]
"plugins": ["@babel/plugin-transform-runtime"]
}
在语法解析时,我们需要知道一些高级的语法应该怎么去转换,这个时候,就可以在plugins配置项中添加规则去告诉babel应当怎么去转,例如,我们需要转换箭头函数时:
const arrowFunc = () => {
console.log('hello');
}
就需要在.babelrc中这么配置:
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
@babel/plugin-transform-arrow-functions中实现了转换箭头函数的方法,这样babel就能够转换了。 当有新增的语法需要解析的时候,在plugins中继续添加规则即可。
实际上,plugins分为语法插件(syntax plugin)和转换插件(transform plugin),这两种插件的差别在于语法插件用于识别语法,但不包含转换语法,语法插件一般以@babel/plugin-syntax-开头;而转换插件可以对语法进行转换,一般以@babel/plugin-transform-开头。
随着代码中使用的ES2015+语法越来越多,
plugins中的规则也将会越来越多,这个时候presets就起作用了。
预设(presets)帮助我们简化plugins配置的数量,例如比较经典的@babel/preset-env,就能帮助将语法转换到目标环境能支持的语法:
{
"presets": ["@babel/presets-env"]
}
每一个presets都支持各自的配置,例如@babel/preset-env需要指定目标环境时,可以设置targets参数:
{
"presets": [
["@babel/presets-env", {
targets: {
browsers: ["iOS >= 7", "Android >= 4"]
}
}]
]
}
但是有时候,会发现@babel/preset-env可以帮助我们实现高级语法的转换,但是有一些新的内置函数Promise,或者实例方法Array.prototype.includes无法进行转换,这个时候就需要使用@babel/polyfill来帮助填补这部分的缺陷了。
执行顺序
了解了presets和plugins的具体功能以后,那它们之间的执行顺序是怎样的呢?
这里就直接说结论:
plugins会在presets前执行;plugins间的执行顺序是从靠前声明的往后执行;presets间的执行则是从靠后声明的往前执行
Polyfill
介绍@babel/polyfill前,先从最简单的场景开始:
当代码运行的环境缺少类似
Promise、Array.from时,这个时候在代码头部中引入@babel/polyfill,可以帮助我们填补这部分的缺陷。
import '@babel/polyfill';
const pm = function() {
return new Promise(function(resolve) {
resolve();
});
}
但是如上述方式引入@babel/polyfill,不仅会污染全局对象,因为其会修改原型方法,而且它的体积过大,无法做到按需引入。因此实际应用中不太建议这种方式。
按需引入
@babel/preset-env提供useBuiltIns配置,当设置为'usage'的时候,就会按需添加代码中需要的Polyfill,另外还需要配置corejs的版本,一般可以设置为2或3,但是这里建议选择3,因为2已经不会再添加新特性。
{
"presets": [
["@babel/presets-env", {
useBuiltIns: 'usage',
corejs: 3
}]
]
}
有了按需添加,这样打包后整体的体积就会小很多。
这里需要补充一点的是,@babel/polyfill包括core-js模块和一个自定义的regenerator-runtime模块,这两个模块都可以在@babel/polyfill的package.json文件的dependencies中找到。
regenerator-runtime
这里需要介绍一下regenerator-runtime,当代码中使用到async/await语法的时候,@babel/preset-env会帮助代码转换为一个regeneratorRuntime的函数,但是转换后的代码仅仅存在这个函数的调用,而这个函数具体的实现,在没有声明useBuiltIns: 'usage'的情况下,是不会引入的!。
很多时候,我们看到
regeneratorRuntime is undefined的报错,也是因为这个regeneratorRuntime函数没有被引入而导致的。
@babel/runtime和@babel/plugin-transform-runtime
文章介绍到这里,我们可以看到@babel/preset-env和@babel/polyfill的配合,似乎已经解决绝大部分的转换和运行环境的问题,但是不是到这里就结束呢?显然答案是否定的。
这里就介绍一下@babel/runtime和@babel/plugin-transform-runtime进一步能优化些什么?
事实上,babel会生成很多小的辅助函数(helpers)去实现类似_createClass的功能,而默认情况下,每个文件都会注入这些辅助函数,这样就导致多个文件都会有重复的辅助函数,这不利于体积的优化,所以@babel/plugin-transform-runtime将会转换这些辅助函数,使其都引用babel/runtime里面的辅助函数:
{
"presets": [
["@babel/presets-env"]
],
"plugins": [
['@babel/plugin-transform-runtime']
]
}
使用上述配置进行代码转换,会发现类似_createClass的辅助函数都会直接引用@babel/runtime/helpers/createClass中的函数。
然而@babel/plugin-transform-runtime的功能不仅仅如此,事实上它可以做如下事情:
- core-js aliasing:这类似于
@babel/polyfill; - helper aliasing;
- regenerator aliasing;
前面我们介绍@babel/polyfill的时候,还提到了它的一个缺点:污染全局方法或者函数,core-js aliasing可以帮助解决这个问题,类似引入辅助函数的方式,从@babel/runtime-corejs3包中按需引入函数,实现这个优化,只需要如下配置,并且安装@babel/runtime-corejs3的依赖包:
{
"presets": [
["@babel/presets-env"]
],
"plugins": [
['@babel/plugin-transform-runtime', {
corejs: 3
}]
]
}
babel-loader 和 rollup-plugin-babel
这两个包分别是Webpack和Rollup两个打包工具上与babel相关的loader和plugin,这里就不多介绍了,大家可以看具体文档,内部实现其实也是使用了babel工具链上的包。
结尾
至此,有关的Babel工具链上一些概念和包已经介绍得差不多了,内容主要是从Babel官网翻译以及自己理解后总结出来的。如有错漏,欢迎指正和讨论哈!