Babel第一讲

最近在小伙伴的推动下进行了一波工具的升级,包括webpack、babel。这两大工具都是前端工程化的常用工具,我准备依次总结一下这两样工具的使用。

“babel 7”的图片搜索ç"“æžœ

what’s babel

Babel是一个广泛使用的转码器,可以将使用了ES6、ES7或更新的仍然在proposal(提案)阶段语法的代码转为ES5代码,从而使低端运行环境(如浏览器和node)能够认识并执行。严格来说,babel 也可以转化为更低的规范。但以目前情况来说,es5 规范已经足以覆盖绝大部分浏览器,因此常规来说转到 es5 是一个安全且流行的做法。

使用方法

一般存在一下几种方式:

  1. 使用单体文件(standalone script,如使用babel-standalone模块提供的浏览器版本,将其插入网页,但网页中实时将ES6代码转为ES5,对性能会有影响,生产环境一般使用已经转码完成的脚本)。
1
2
3
4
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.4.4/babel.min.js"></script>
<script type="text/babel">
// Your ES6 code
</script>
  1. Babel还提供一个REPL在线编译器,可以在线将ES6代码转为ES5代码。转换后的代码,可以直接作为ES5代码插入网页运行。
  2. 命令行(cli),多见于 package.json 中的 scripts 段落中的某条命令。
  3. 使用构建工具的插件(webpack 的 babel-loader,rollup 的 rollup-plugin-babel)集成到构建工具中。

其中后两种比较常见,我们重点讨论,不过这些方式的差别只是于入口不同而已,调用的 babel 内核,处理方式都是一样的。

运行方式和插件

babel 总共分为三个阶段:解析,转换,生成。

babel 本身不具有任何转化功能,它把转化的功能都分解到一个个 plugin 里面。因此当我们不配置任何插件时,经过 babel 的代码和输入是相同的。

插件总共分为语法插件和转译插件两种:

  • 当我们添加 语法插件 之后,就在解析这一步使得 babel 能够解析更多的语法。(tip:babel 内部使用的解析类库叫做 babylon,并非 babel 自行开发)

举个简单的例子,当我们定义或者调用方法时,最后一个参数之后是不允许增加逗号的,如 callFoo(param1, param2,) 就是非法的。如果源码是这种写法,经过 babel 之后就会提示语法错误。

但最近的JavaScript提案中已经允许了这种新的写法(让代码 diff 更加清晰)。为了避免 babel 报错,就需要增加语法插件 babel-plugin-syntax-trailing-function-commas

  • 当我们添加 转译插件 之后,在转换这一步把源码转换并输出。这也是我们使用 babel 最本质的需求。

比起语法插件,转译插件其实更好理解,比如箭头函数 (a) => a 就会转化为 function (a) {return a}。完成这个工作的插件叫做 babel-plugin-transform-es2015-arrow-functions

同一类语法可能同时存在语法插件版本和转译插件版本。如果我们使用了转译插件,就不用再使用语法插件了。

配置插件

插件的配置方式如下:

  1. 使用 npm install babel-plugin-xxx 进行安装
  2. 将插件的名字增加到配置文件中(根目录下创建 .babelrc 或者 package.json 的 babel 里面,格式相同)

preset

我们平常所说的es6(也就是es2015)其实是一套规范,包含大概十几二十个转译插件。如果每次要开发者一个个添加并安装,首先使用npm install一个个安装就很麻烦,配置文件也会越来越长,跟不用说还有同时使用的其他插件。为了解决这个问题,babel 还提供了一组插件的集合(类似于一个全家桶套餐,也就是preset,意思是预先设置)。因为常用,所以不必重复配置和一个个安装。

preset 分为以下几种:

  • 官方内容,目前包括 env、react、flow、minify 等(tip:这里最重要的是 env,后面会详细介绍)。

  • es201x、latest
    这些是已经纳入到标准规范的语法。例如 es2015 包含 arrow-functions,es2017 包含 syntax-trailing-function-commas。但因为 env 的出现,使得 es2016 和 es2017 都已经废弃。所以我们经常可以看到 es2015 被单独列出来(es2015先于env出现),但极少看到其他两个。
    latest 是 env 的雏形,它是一个每年更新的 preset,目的是包含所有 es201x。但也是因为更加灵活的 env 的出现,已经废弃。

  • stage-x,这里面包含的都是当年最新规范的草案,每年更新(tip:babel 7已经不推荐使用,后面也会讲)。例如 syntax-dynamic-import 就是 stage-2 的内容,transform-object-rest-spread 就是 stage-3 的内容。此外,低一级的 stage 会包含所有高级 stage 的内容,例如 stage-1 会包含 stage-2和stage-3的所有内容。stage-4 是不存在的因为它就是上面的 es2015。

执行顺序

简单的几条原则:

  • Plugin 会运行在 Preset 之前。
  • Plugin 会从前到后顺序执行。
  • Preset 的顺序则 刚好相反(从后向前)。

preset 的逆向顺序主要是为了保证向后兼容,因为大多数用户的编写顺序是 ['es2015', 'stage-0']。这样必须先执行 stage-0 才能确保 babel 不报错。因此我们编排 preset 的时候,也要注意顺序,其实只要按照规范的时间顺序列出即可。

插件和 preset 的配置项

简略情况下,插件和 preset 只要列出字符串格式的名字即可。但如果某个 preset 或者插件需要一些配置项(或者说参数),就需要把自己先变成数组。第一个元素依然是字符串,表示自己的名字;第二个元素是一个对象,即配置对象。

最需要配置的当属 env,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"presets": [
// 带了配置项,自己变成数组
[
// 第一个元素依然是名字
"env",
// 第二个元素是对象,列出配置项
{
"modules": false
// 指将ES6模块语法转换为另一种模块类型。设置为 false 则不会转换模块
// 推荐将 modules 设置为 false,即交由 Webpack 来处理模块化,通过其 TreeShaking 特性将有效减少打包出来的 JS 文件大小
}
],
// 不带配置项,直接列出名字
"stage-2"
]

env(重点)

env 最常用也最重要,所以我们需要重点关注。Babel 的官网上在2017年9月就宣布 ES2015 / ES2016/ ES2017 等等 ES20xx 时代的 presets 通通被废弃(deprecated),取而代之的是 babel-preset-env,并且承诺它将成为“未来不会过时的(future-proof)”解决方案。

在过去,Babel 将 babel-preset-es2015 放在 babel/babel 的主仓库中进行维护,而 babel-preset-env 则独立为一级项目,这从某种程度上也显示出 Babel 官方对这款 preset 的重视程度和更长远的规划。

env 的核心目的是通过配置得知目标环境的特点,然后只做必要的转换。例如目标浏览器支持 es2015,那么 es2015 这个 preset 其实是不需要的,于是代码就可以小一点(一般转化后的代码总是更长),构建时间也可以缩短一些。

如果不写任何配置项,env 等价于 latest,也等价于 es2015 + es2016 + es2017 三个相加(不包含 stage-x 中的插件)。env 包含的插件列表维护在这里

下面列出几种比较常用的配置方法:

1
2
3
4
5
6
7
8
9
{
"presets": [
["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}]
]
}

如上配置将考虑所有浏览器的最新2个版本(safari大于等于7.0的版本)的特性,将必要的代码进行转换。而这些版本已有的功能就不进行转化了。这里的语法可以参考 browserslist

1
2
3
4
5
6
7
8
9
{
"presets": [
["env", {
"targets": {
"node": "6.10"
}
}]
]
}

如上配置将目标设置为 nodejs,并且支持 6.10 及以上的版本。也可以使用 node: 'current' 来支持最新稳定版本。例如箭头函数在 nodejs 6 及以上将不被转化,但如果是 nodejs 0.12 就会被转化了。

另外一个有用的配置项是 modules。它的取值可以是 amd, umd, systemjs, commonjsfalse。这可以让 babel 以特定的模块化格式来输出代码。如果选择 false 就不进行模块化处理。

其他配套工具

以上讨论了 babel 的核心处理机制和配置方法等,不论任何入口调用 babel 都走这一套。不过 package.json 中的一堆 babel-* 还是让人一头雾水。实际上这些 babel-* 大多是不同的入口(方式)来使用 babel,下面来简单介绍一下。

babel-cli

顾名思义,cli 是命令行工具。安装了 babel-cli 就能够在命令行中使用 babel 命令来编译文件。

在开发 npm package 时经常会使用如下模式:

  • babel-cli 安装为 devDependencies
  • 在 package.json 中添加 scripts (比如 prepublish),使用 babel 命令编译文件
  • npm publish

这样既可以使用较新规范的 JS 语法编写源码,同时又能支持旧版环境。因为项目可能不太大或者是个后端工具,用不到构建工具 (webpack 或者 rollup),于是在发布之前用 babel-cli 进行处理。

babel-node

babel-nodebabel-cli 的一部分,它不需要单独安装,而是babel-cli的一个命令,存在于babel-cli/bin/中。

它的作用是在 node 环境中,直接运行 es2015 的代码,而不需要额外进行转码。例如我们有一个 js 文件以 es2015 的语法进行编写(如使用了箭头函数)。我们可以直接使用 babel-node es2015.js 进行执行,而不用再进行转码了。

或者说babel-node = babel-polyfill + babel-register。接下来介绍这两个工具。

babel-register

babel-register 模块改写 require 命令,为它加上一个钩子。此后,每当使用 require 加载 .js.jsx.es.es6 后缀名的文件,就会先用 babel 进行转码。

使用时,必须首先加载 require('babel-register')

需要注意的是,babel-register 只会对 require 命令加载的文件转码,而 不会对当前文件转码

另外,由于它是实时转码,所以 只适合在开发环境使用

babel-polyfill

babel 默认只转换 js 语法,而不转换新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转码。

举例来说,es2015 在 Array 对象上新增了 Array.from 方法。babel 就不会转码这个方法。如果想让这个方法运行,必须使用 babel-polyfill。(内部集成了 core-jsregenerator

使用时,在所有代码运行之前增加 require('babel-polyfill')。或者更常规的操作是在 webpack.config.js 中将 babel-polyfill 作为第一个 entry。因此必须把 babel-polyfill 作为 dependencies 而不是 devDependencies

babel-polyfill 主要有两个缺点:

  1. 使用 babel-polyfill 会导致打出来的包非常大,因为 babel-polyfill 是一个整体,把所有方法都加到原型链上。比如我们只使用了 Array.from,但它把 Object.defineProperty也给加上了,这就是一种浪费了。这个问题可以通过单独使用 core-js 的某个类库来解决,core-js 都是分开的(core-js 是babel-polyfill 的底层依赖,通过各种奇技淫巧,用 ES3 实现了大部分的 ES2017 原生标准库)。
  2. babel-polyfill 会污染全局变量,给很多类的原型链上都作了修改,如果我们开发的也是一个类库供其他开发者使用,这种情况就会变得非常不可控。

因此在实际使用中,如果我们无法忍受这两个缺点(尤其是第二个,比如要写一个npm包,如果引入导致污染了使用者的全局变量是不太好的,可能对使用者项目带来不确定性),通常我们会倾向于使用 babel-plugin-transform-runtime

但如果代码中包含高版本 js 中类型的实例方法(例如 [1,2,3].includes(1)),这还是要使用 polyfill。

babel-runtime 和 babel-plugin-transform-runtime

我们时常在项目中看到 .babelrc 中使用 babel-plugin-transform-runtime,而 package.json 中的 dependencies(注意不是 devDependencies) 又包含了 babel-runtime,那这两个的关系和作用分别是什么呢?是不是要成套使用呢?

先说 babel-plugin-transform-runtime

babel 会转换 js 语法,之前已经提过了。以 async/await 举例,如果不使用这个 plugin(但要使用env或者stage-3,因为async/await语法属于es2016,也就是es7),转换后的代码大概是:

1
2
3
4
5
6
7
// babel 添加一个方法,把 async 转化为 generator
function _asyncToGenerator(fn) { return function () {....}} // 很长很长一段

// 具体使用处
var _ref = _asyncToGenerator(function* (arg1, arg2) {
yield (0, something)(arg1, arg2);
});

不用过于纠结具体的语法,只需看到,这个 _asyncToGenerator 在当前文件被定义,然后被使用了,以替换源代码的 await。但每个被转化的文件都会插入一段 _asyncToGenerator 这就导致重复和浪费了。

在使用了 babel-plugin-transform-runtime 了之后,转化后的代码会变成:

1
2
3
4
5
6
7
8
// 从直接定义改为引用,这样就不会重复定义了。
var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');
var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);

// 具体使用处是一样的
var _ref = _asyncToGenerator3(function* (arg1, arg2) {
yield (0, something)(arg1, arg2);
});

从定义方法改成引用,那重复定义就变成了引用,就不存在代码重复的问题了。

但在这里,我们也发现 babel-runtime 出场了,它就是这些方法的集合处,也因此,在使用 babel-plugin-transform-runtime 的时候必须把 babel-runtime 当做依赖。

再说 babel-runtime,它内部集成了:

  1. core-js:转换一些内置类 (Promise, Symbols等等) 和静态方法 (Array.from 等)。绝大部分转换是这里做的。自动引入。
  2. regenerator:作为 core-js 的拾遗补漏,主要是 generator/yieldasync/await两组的支持。当代码中有使用 generators/async 时自动引入。
  3. helpers:如上面的 asyncToGenerator 就是其中之一,其他还有如 jsx, classCallCheck等等,可以查看 babel-helpers。在代码中有内置的 helpers 使用时(如上面的第一段代码)移除定义,并插入引用(于是就变成了第二段代码)。

babel-plugin-transform-runtime 不支持 实例方法(例如 [1,2,3].includes(1)

此外补充一点,把 helpers 抽离并统一起来,避免重复代码的工作还有一个 plugin 也能做,叫做 babel-plugin-external-helpers。但因为我们使用的 transform-runtime 已经包含了这个功能,因此不必重复使用。而且 babel 的作者们也已经开始讨论这两个插件过于类似,正在讨论在 babel 7 中把 external-helpers 删除,讨论在 issue#5699 中。

babel-loader

前面提过 babel 的三种使用方法,并且已经介绍过了 babel-cli。但一些大型的项目都会有构建工具(如 webpack 或 rollup)来进行代码构建和压缩(uglify)。理论上来说,我们也可以对压缩后的代码进行 babel 处理,但那会非常慢。因此如果在 uglify 之前就加入 babel 处理,岂不完美?

所以就有了 babel 插入到构建工具内部这样的需求。以 webpack 为例,webpack 有 loader 的概念,因此就出现了 babel-loader

babel-cli 一样,babel-loader 也会读取 .babelrc 或者 package.json 中的 babel 段作为自己的配置,之后的内核处理也是相同。唯一比 babel-cli 复杂的是,它需要和 webpack 交互,因此需要在 webpack 这边进行配置。比较常见的如下:

1
2
3
4
5
6
7
8
9
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel-loader'
}
]
}

如果想在这里传入 babel 的配置项,也可以把改成:

1
2
3
4
5
6
7
// loader: 'babel-loader' 改成如下:
use: {
loader: 'babel-loader',
options: {
// 配置项在这里
}
}

这里的配置项优先级是最高的。但我认为放到单独的配置文件中更加清晰合理,可读性强一些。

babel-eslint

许多工具需要Babel进行前置转码,这里举两个例子:ESLint和Mocha。

ESLint 用于静态检查代码的语法和风格,为了使用新语法我们需要安装 babel-eslint 作为parser(解释器)。使用方法是在eslint的配置文件.eslint中,加入parser字段:

1
2
3
4
5
6
{
"parser": "babel-eslint",
"rules": {
...
}
}

再在package.json之中,加入相应的scripts脚本。

1
2
3
4
5
6
7
8
9
10
{
"name": "my-module",
"scripts": {
"lint": "eslint my-files.js"
},
"devDependencies": {
"babel-eslint": "...",
"eslint": "..."
}
}

Mocha 则是一个测试框架,如果需要执行使用ES6语法的测试脚本,可以修改package.jsonscripts.test

1
2
3
"scripts": {
"test": "mocha --ui qunit --compilers js:babel-core/register"
}

上面命令中,--compilers参数指定脚本的转码器,规定后缀名为js的文件,都需要使用babel-core/register先转码。

babel-core

我们看到上面使用 Mocha 时候,选择了babel-core/register作为转码器。那么 babel-core 是什么呢?

简单的说,如果你需要以编程的方式来使用 Babel,使用 babel-core 这个包,可以在代码里调用Babel的API。相当于是编程的方式去使用 babel,是把代码以字符串的形式从文件或者网络请求等读入,调用它的 transform 方法,转化为新的字符串,再写回文件或者网络返回。

下面是一个例子。

1
2
3
4
5
6
var es6Code = 'let x = n => n + 1';
var es5Code = require('babel-core')
.transform(es6Code, {
presets: ['es2015']
})
.code;

babel-core 可以把 js 代码分析成 AST ,方便各个插件分析语法进行相应的处理。有些新语法在低版本 js 中是不存在的,如箭头函数,rest 参数,函数默认值等,这种语言层面的不兼容只能通过将代码转为 AST,分析其语法后再转为低版本 js。

总结

包名 作用 备注
babel-cli 命令行使用bable命令 img
babel-node 命令行直接转译 + 执行node文件 babel-cli的一部分
babel-node=babel-polyfill+babel-register
babel-register 改写require命令,为其加载的文件进行转码,但是不会对当前文件转码 image-20181113224837212
只适用于开发环境
babel-polyfill 填充内建对象,缺失的为所有API增加兼容方法 image-20181113224808773
需要在所有代码之前require,而且体积较大
babel-runtime 和 babel-plugin-transform-runtime 让 babel 自动且按需的进行 polyfill(不污染全局变量和prototype) image-20181113230350432
image-20181113224911651
babel-core 在代码里使用 babel img
babel-preset-env 整合了 es201x,并自动根据目标平台分析需要用哪些插件 plugin太多,不截图了,把除了babel开头的截一下
image-20181120224948380
babel-loader 使用webpack时作为一个loader,在代码混淆前进行转码 babel-loader和babel-core是Peer Dependencies关系,不装会报UNMET PEER DEPENDENCY
babel-eslint 使用eslint时,进行前置转码 babel-eslint和eslint也是Peer Dependencies关系,不装会报UNMET PEER DEPENDENCY

参考链接

一口(很长的)气了解 babel

再见,babel-preset-2015