Babel及其使用


前言

对于浏览器来说,包括但不限于 JSXTSES6+ 的代码并不能正常的在所有浏览器上渲染。为了解决这个问题 babel 应运而生。babel 可以解析这些代码,并将其转换为浏览器可兼容的低级 js 代码。

它工作时将根据 browserslist 所指定的兼容浏览器范围进行代码转换。对于 browserslist 的说明,可以参考 browserslist及其使用

环境准备

1
npm i @babel/core @babel/cli @babel/plugin-transform-arrow-functions @babel/plugin-transform-block-scoping -D

上面安装的两个包分别为:

  • @babel/core:babel 的核心库,但除了解析功能以外什么都干不了,想实现功能需要其他插件帮助。
  • @babel/cli:babel 不允许直接在终端中使用,如需使用则需要借助 @babel/cli 工具。
  • @babel/plugin-transform-arrow-functions:对箭头函数代码进行转换。
  • @babel/plugin-transform-block-scoping:将 letconst 转换为 var

更多的包均可在 babel -> package 里找到。

食用方式

这里有 ./src/js/babel.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const foo = () => {
const obj = Object.create( {}, Object.getOwnPropertyDescriptors( {
wdnmd: 123
} ) );

for (let i = 0; i < 10; i++) {
Reflect.set( obj, `handel${ i }`, () => {
console.log( ` I'm handel ${ i } ` );
} );
}

return obj;
}

此时的 .browserslistrc 配置为:

1
2
3
>0.2%
last 8 versions
not dead

转换箭头函数和 let 与 const

下面的语句可以将上面的 js 文件进行解析,解析结果被放在目录 babel-output 中(此处 src/js/babel.js 可以改为目录名称,将会对目录下所有 js 文件进行解析)。

1
npx babel src/js/babel.js --out-dir babel-output --plugins=@babel/plugin-transform-arrow-functions,@babel/plugin-transform-block-scoping

babel-output/babel.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var foo = function () {
var obj = Object.create( {}, Object.getOwnPropertyDescriptors( {
wdnmd: 123
} ) );
var _loop = function ( i ) {
Reflect.set( obj, `handel${i}`, function () {
console.log( ` I'm handel ${i} ` );
} );
};
for ( var i = 0; i < 10; i++ ) {
_loop( i );
}
return obj;
};

可见 let 与箭头函数成功的被转换为了兼容性代码,且并未丢失代码原本意义。

@babel/preset-env

对于上面的示例代码中,似乎还有不少 ES6 的特性未被转换,但每一种转换都要手动添加工具似乎太过繁琐了。因此便有了整合了一些预设的功能插件 @babel/preset-env。而且该插件与上面有所不同的是,他会参考 browserslist 配置进行兼容。

1
npm i @babel/preset-env -D

这里我们对处理指令进行一些修改,不再使用 --plugins 而是改为 --presets

1
npx babel src/js/babel.js --out-dir babel-output --presets=@babel/preset-env

babel-output/babel.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"use strict";

var foo = function foo() {
var obj = Object.create( {}, Object.getOwnPropertyDescriptors( {
wdnmd: 123
} ) );
var _loop = function _loop(i) {
Reflect.set( obj, "handel".concat( i ), function () {
console.log( " I'm handel ".concat( i, " " ) );
});
};
for ( var i = 0; i < 10; i++ ) {
_loop( i );
}
return obj;
};

虽然该预设插件集成了许多转换功能,比如 const 关键字与 箭头函数等。但当遇到 Prmise、generator 生成器、Symbol、Reflect 等深层次语法时,它是无法转换的。这种情况就要使用 Polyfill 了,本文后面将会讲解

webpack 中使用 babel

在 webpack 中,babel 的加载器为 babel-loader,此时仅需要安装 babel-loader,不需要安装 @babel/core@babel/cli

尽管该加载器与 sass-loaderless-loader 类似,在内部依赖一个工具解析,在这里为 @babel/core。但不同的是,babel-loader 已经内部安装了 @babel/core,不需要我们再额外去安装。而 @babel/cli 仅用于以命令方式交互使用 babel, 因此也不需要安装。

1
npm i babel-loader -D

由于 babel 的工作原理是对 css 代码进行解析,得到兼容后的 css 代码,不难理解 babel-loader 该放置的位置。

1
2
3
4
5
6
7
8
9
10
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [ "babel-loader" ]
}
]
}
}

但这样其实是不会起任何作用的。我们知道 babel 除了解析 js 代码以外是什么也做不了的,想实现功能还需要使用插件。下面将会讲解插件的加载方法。

插件的配置方法

插件的种类很多,这里还是使用转换箭头函数和 let 与 const 的插件 @babel/plugin-transform-arrow-functions@babel/plugin-transform-block-scoping

1
npm i @babel/preset-env -D

使用插件后,也就不能像其他 loader 那样简单配置了,而是需要在 options 中进行相应配置。不同的 loader 有不同的配置方法,可以参考各自的官方文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [ {
loader: "babel-loader",
options: {
plugins: [
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-block-scoping"
]
}
} ]
}
]
}
}

配置许多个 plugins 或许有些繁琐,下面提供整合了一些预设的功能插件 @babel/preset-env 的配置方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [ {
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env"
]
}
} ]
}
]
}
}

presets 是默认按照 browserslist 指定的范围进行编译的,同时也可以通过配置 targets 属性来覆盖 browserslist 中的配置,如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [ {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{ targets: "chrome 91" }
]
]
}
} ]
}
]
}
}

babel.config.js

上述配置方式可以发现一个问题,babel-loader 的配置代码相对比较繁琐,嵌套比较深管理起来非常臃肿。

同时,使用交互命令执行时,--plugins--preset 参数每次都要输入也让人厌烦。便有了 babel.config.js 配置文件。

在项目根目录下创建 babel.config.js,输入以下内容。

该配置文件可以为 jsoncjsmjs,在 babel 7 之前还命名为 babelrc.json(js)

1
2
3
module.exports = {
presets: [ "@babel/preset-env" ]
}

配置好后就可以优化交互指令或 webpack 配置文件的内容了。

指令方式

此时可以省略 --plugins--preset 参数。

1
npx babel src/js/babel.js --out-dir babel-output

webpack

此时可以将 babel-loader 改为简写模式。

1
2
3
4
5
6
7
8
9
10
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [ "babel-loader" ]
}
]
}
}

polyfill

上面所讲的 @babel/preset-env 可以帮我们解决一些如 const 关键字、箭头函数等的兼容性问题,但当遇到 Prmise、generator 生成器、Symbol、Reflect 等深层次语法时,它是无法转换的。这时就有了 polyfill。

polyfill 字面意思填充,把一些高级原生语法实现后填充到低版本浏览器上。由于会导致打包后的产出内容过大,webpack 5 后取消内置。

安装

需要注意的是,polyfill 需要安装为生产依赖

babel 7 之前安装方式:

1
npm i @babel/polyfill

babel 7 后不建议使用上述方式安装,因为直接按上面那样安装太大了,改为使用两个核心库,babel 官网 上是这样讲的:

从 Babel 7.4.0 开始,这个包已经被弃用,取而代之的是直接包含core-js/stable(以填充 ECMAScript 特性):

1
import "core-js/stable";

如果您正在将生成器或异步函数编译到 ES5,并且您使用的版本低于 @babel/core@babel/plugin-transform-regenerator 低于 7.18.0,则还必须加载该 regenerator runtime 包。使用 @babel/preset-envuseBuiltIns: "usage"选项或时会自动加载 @babel/plugin-transform-runtime

这里对两个包做一下说明:

  • core-js/stable:加载 js 核心库下的已经形成标准的部分(stable)
  • regenerarator-runtime/runtime:转换 generator 生成器的时候(比如 promise、async/await 底层就是 generator 生成器)

现在安装时按照如下安装方式即可:

1
2
3
npm i core-js
# 低版本并使用到了 generator 生成器
npm i regenerator-runtime

使用

bable.config.jswebpack.config.js 中对 @babel/preset-env 进行配置。

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: 3
}
]
]
}

属性解释:

  • useBuiltIns:存在两个可选值
    • false:默认值,polyfill 不会对当前打包进行处理
    • usage:根据原代码中使用到的新语法,按需进行填充(不会理睬 browserslist 配置)
    • entry: 根据 browserslist 对所有浏览器所需要的新语法进行填充(不会理睬源代码)
  • corejs:指定 corejs 版本,默认为 2。由于现在安装的最新版本 corejs 为 3.x,当 useBuiltIns 不为 false 时,若不手动指定为 3,会引发报错。

当使用 useBuiltIns 设置为 entry 时,还需要在入口文件引入两个包

1
2
3
import "core-js/stable";
// 低版本并使用到了 generator 生成器
import "regenerator-runtime/runtime";

注意事项

由于第三方包在打包过程中可能会存在使用了 polyfill 填充过的情况,此时再使用 polyfill 对其进行处理可能会出现问题。因此往往会在 webpack.config.js 中做如下处理:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [ "babel-loader" ]
}
]
}
}

处理 TypeScript

babel 支持对 TypeScript 进行包括 polyfill 的处理,此时需要使用使用预设 @babel/preset-typescript

1
npm install @babel/preset-typescript -D

该预设仅对 TypeScript 进行编译处理,若进一步进行 polyfill 等的处理,需要依赖 @babel/preset-env 预设。

babel.config.js 进行如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: 3
}
],
[ "@babel/preset-typescript" ]
]
}

@babel/preset-typescript 拥有一些配置选项,性详见官网 @babel/preset-typescript

尽管该预设可以直接解析 TypeScript,但他并不拥有编译时检查语法错误的功能。因此可以尝试通过联合使用命令的方式来达成需求。

package.json 中新增如下命令,使用 noEmit 参数来指定 ts 编译器仅校验,不便宜 js 操作。

1
2
3
4
5
{
"scripts": {
"build": "tsc --noEmit && webpack"
}
}

个人觉得这样操作属实啰嗦,不如联合使用 ts-loaderbabel-loader,见仁见智了。