1. 什么是Babel
我们知道,各个浏览器对 JavaScript 版本的支持度各不相同,有很多优秀的新语法都不能直接在浏览器中运行。为了解决这个“沟通不畅”的问题,所以就有了 Babel。Babel是一个工具集,主要用于将ES6版本的JavaScript代码转为ES5等向后兼容的JS代码,从而可以运行在低版本浏览器或其它环境中。因此,在编码过程中,可以使用ES6/7/8编码,然后使用Babel将代码编译为向后兼容的Javascript代码,这样就不用担心所在环境是否支持了。
简单来说,Babel的工作就是:
- 语法转换
- 通过 Polyfill 的方式在目标环境中添加缺失的特性
- Javascript源码转换
2. Babel的基本原理
Babel的原理很简单,首先是将源码转成抽象语法树(AST)
,然后对语法树进行处理生成新的预发树,最后将新语法树生成新的Javascript代码,整个编译过程分为parsing(解析)、transforming(转换)、generating(生成)。Babel只负责编译新标准引入的语法,比如 Arrow function、Class、ES Module 等,它不会编译原生对象新引入的方法和 API,比如 Array.includes,Map,Set 等,这些需要通过 Polyfill 来解决,文章后面会提到。
Babel7的npm包都是放到Babel域下, 例如@babel/cli、@babel/core等,在Babel6中,安装的包名是babel-core、babel-cli。本文将以Babel7为例进行讲解。
3. Babel的使用
3.1 Babel运行所需的基本环境
1、@babel/cli
@babel/cli 是 Babel 提供的内建命令行工具。
安装:npm install i -S @babel/cli
2、@babel/core
@babel/core是我们使用Bable进行转码的核心npm包,我们使用的babel-cli、babel-node都依赖这个包。在命令行和webpack进行转码的时候都是通过Node来调用@babel/core相关功能API来进行的。
安装: npm install --save-dev @babel/core
3.2 配置文件
通常,我们需要指定Babel的编译规则来编译代码。Babel的配置文件默认会在当前目录寻找文件,有:.babelrc
、.babelrc.js
、babel.config.js
、package.json
,它们的配置项都是一样的,作用也一样,只需要选择一种即可。
1、.babelrc
配置:
{
"presets": ["es2015", "react"],
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
2、babel.config.js
和.babelrc.js
配置:
module.exports = {
"presets": ["es2015", "react"],
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
3、package.json
配置:
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"babel": {
"presets": ["es2015", "react"],
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
}
对比分析不同格式的Babel配置文件,总结起来的配置项都是plugins(插件)和presets(预设)两个数组(当然还有其他的,我们先只关注这两个)。
4. 插件plugins和预设presets
4.1 插件plugins
插件是用来定义如何转换你的代码的,一般是单独的某个新特性。当在Babel的配置项中填写了需要使用的插件,Babel编译的时候就会去加载node_modules中对应的npm包,然后编译插件的对应的语法。
- @babel/plugin-transform-arrow-functions: 转换箭头函数的语法
- @babel/plugin-transform-classes: 使用
Class
语法 - @babel/plugin-transform-for-of: 使用
for...of
语法...
Babel支持的插件非常多,如果每一个新特性转换都需要通过安装插件来解决,那么我们的开发效率会变得非常低效,Babel配置文件就会变得非常臃肿。于是,Babel推出了懒人包presets
。
4.2 预设presets
预设就是一堆插件包的集合,例如babel-preset-es2015就是所有处理es2015的二十多个Babel插件的集合。这样我们就不需要写一大堆插件的配置项了,使用一个预设即可。
常用的preset包有:
- @babel/preset-env
- @babel/preset-react
- @babel/preset-typescript
- @babel/preset-stage-0
- @babel/preset-stage-1...
解释下,stage-x,这里面包含的都是当年最新规范的草案,每年更新。这里面还细分为:
stage 0
- 设想: 只是一个想法,可能有 Babel 插件,stage-0 的功能范围最广大,包含 stage-1 , stage-2 以及 stage-3 的所有功能。stage 1
- 提案: 初步尝试,值得跟进。stage 2
- 初稿: 完成初步规范。stage 3
- 候选: 完成规范和浏览器初步实现。stage 4
- 完成: 将被添加到下一年度发布。
所有的预设也都需要安装npm包到node_modules中才可以使用。
4.3 执行顺序
plugins插件数组和presets预设数组是有顺序要求的。如果两个插件或预设都要处理同一个代码片段,那么会根据插件和预设的顺序来执行。规则如下:
1、插件plugins的执行顺序是从左到右执行的。
{
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
在上面的示例中,Babel 在进行 AST 遍历的时候会先调用 transform-decorators-legacy 插件中定义的转换方法,然后再调用 transform-class-properties 中的方法。
2、预设presets的执行顺序是从右到左执行的。
{
"presets": [
"a",
"b",
"c"
]
}
它的执行顺序是 c、b、a,是不是有点奇怪,这主要是为了确保向后兼容,因为大多数用户将 "es2015" 放在 "stage-0" 之前。
3、插件plugins在预设presets之前执行
4.4 @babel/preset-env一统江湖
在Babel6的时代,常见的preset有babel-preset-es2015
、babel-preset-es2016
、babel-preset-es2017
、babel-preset-latest
、babel-preset-stage-0
、babel-preset-stage-1
、babel-preset-stage-2
等。
目前,Babel官方不再推出babel-preset-es2017
以后的年代preset了。
@babel/preset-env
包含了babel-preset-latest
的功能,并对其进行增强,现在@babel/preset-env
完全可以替代babel-preset-latest
。
5. @babel/polyfill
在默认情况下,@babel/preset-env
只会编译Javascript的语法,不会对新方法和新的原生对象进行转译。比如:
var fn = (num) => num + 2;
const arr = [1,2,3]
console.log(arr.includes(1))
转译后:
"use strict";
var fn = function fn(num) {
return num + 2;
};
var arr = [1, 2, 3];
console.log(arr.includes(1));
可以发现,箭头函数被转换了,但是Array.includes方法没有被处理,如果这个时候程序运行在低版本的浏览器上,就会出现includes is not function
的错误。这个时候就需要polyfill了。
polyfill广义上讲是为环境提供不支持的特性的一类文件或库,既有Babel官方的库,也有第三方的。
@babel/polyfill
本质上是由两个npm包core-js与regenerator-runtime组合而成的,所以在使用层面上还可以再细分为是引入@babel/polyfill
本身还是其组合子包。使用方式有以下几种:
1. 在html文件中引入polyfill文件
<script src="https://cdn.bootcss.com/babel-polyfill/7.6.0/polyfill.js"></script>
2. 在前端工程的入口文件里引入polyfill.js
import './polyfill.js';
var promise = Promise.resolve('ok');
console.log(promise);
3. 在前端工程的入口文件里引入@babel/polyfill
1)安装@babel/polyfill
2)修改a.js
内容
import '@babel/polyfill';
var promise = Promise.resolve('ok');
console.log(promise);
4. 在前端工程的入口文件里引入core-js/stable与regenerator-runtime/runtime
1)安装core-js和regenerator-runtime
npm install --save core-js regenerator-runtime
2)修改a.js
内容
import "core-js/stable";
import "regenerator-runtime/runtime";
var promise = Promise.resolve('ok');
console.log(promise);
5. 在前端工程构建工具的配置文件入口项引入polyfill.js
const path = require('path');
module.exports = {
entry: ['./polyfill.js', './a.js'],
output: {
filename: 'b.js',
path: path.resolve(__dirname, '')
},
mode: 'development'
};
6. 在前端工程构建工具的配置文件入口里引入@babel/polyfill
const path = require('path');
module.exports = {
entry: ['@babel/polyfill', './a.js'],
output: {
filename: 'b.js',
path: path.resolve(__dirname, '')
},
mode: 'development'
};
7. 在前端工程构建工具的配置文件入口里引入core-js/stable与regenerator-runtime/runtime
const path = require('path');
module.exports = {
entry: ['core-js/stable', 'regenerator-runtime/runtime', './a.js'],
output: {
filename: 'b.js',
path: path.resolve(__dirname, '')
},
mode: 'development'
};
转译结果:
"use strict";
eval("\nvar toObject = __webpack_require__(/*! ../internals/to-object */ \"./node_modules/core-js/internals/to-object.js\");\nvar toAbsoluteIndex = __webpack_require__(/*! ../internals/to-absolute-index */ \"./node_modules/core-js/internals/to-absolute-index.js\");\nvar toLength = __webpack_require__(/*! ../internals/to-length */ \"./node_modules/core-js/internals/to-length.js\");\n\n// `Array.prototype.fill` method implementation\n// https://tc39.es/ecma262/#sec-array.prototype.fill\nmodule.exports = function fill(value /* , start = 0, end = @length */) {\n var O = toObject(this);\n var length = toLength(O.length);\n var argumentsLength = arguments.length;\n var index = toAbsoluteIndex(argumentsLength > 1 ? arguments[1] : undefined, length);\n var end = argumentsLength > 2 ? arguments[2] : undefined;\n var endPos = end === undefined ? length : toAbsoluteIndex(end, length);\n while (endPos > index) O[index++] = value;\n return O;\n};\n\n\n//# sourceURL=webpack:///./node_modules/core-js/internals/array-fill.js?");
/***/ }),
...
这么多的方法,在实际开发中该选择哪一种呢?从babel7.4版本开始,Babel官方已经不推荐再使用@babel/polyfill
,包括官方的polyfill.js库文件。因此从2019年中开始,我们的新项目都应该使用core-js
和regenerator-runtime
这两个包。也就是说我们应选择方法4与方法7。
但是,@babel/polyfill
主要有两个缺点:
@babel/polyfill
把两个npm包全部都引入到了我们的前端打包后的文件里了,导致打包后的体积过大。@babel-polyfill
可能会污染全局变量,给很多类的原型链上都作了修改,这就有不可控的因素存在。
6. @babel/preset-env
@babel/preset-env
的参数项,数量有10多个,但大部分我们要么用不到,要么已经或将要弃用。这里建议大家掌握重点的几个参数项,有的放矢。重点要学习的参数项有targets、useBuiltIns、modules和corejs这四个,能掌握这几个参数项的真正含义。
6.1 targets
该参数项可以取值为字符串、字符串数组或对象,不设置的时候取默认值空对象{}。
module.exports = {
presets: [["@babel/env", {
targets: {
"chrome": "58",
"ie": "11"
}
}]],
plugins: []
}
如果我们对@babel/preset-env的targets参数项进行了设置,那么就不使用browserslist的配置,而是使用targets的配置。如不设置targets,那么就使用browserslist的配置。如果targets不配置,browserslist也没有配置,那么@babel/preset-env就对所有ES6语法转换成ES5的。
6.2 useBuiltIns
useBuiltIns项取值可以是"usage" 、 "entry" 或 false。默认值为false。
我们来看一个例子:
1)安装npm包
npm install --save-dev @babel/cli @babel/core @babel/preset-env
npm install --save @babel/polyfill
2)修改Babel配置文件
module.exports = {
presets: [["@babel/env", {
useBuiltIns: "entry"
}]],
plugins: []
}
3)修改package.json
的browserslist
"browserslist": [
"firefox 58"
]
4)修改入口文件a.js
import '@babel/polyfill';
var promise = Promise.resolve('ok');
console.log(promise);
5)执行命令:npx babel a.js -o b.js
。转码后的结果:
"use strict";
require("core-js/modules/es7.array.flat-map");
require("core-js/modules/es7.string.trim-left");
require("core-js/modules/es7.string.trim-right");
require("core-js/modules/web.timers");
require("core-js/modules/web.immediate");
require("core-js/modules/web.dom.iterable");
var promise = Promise.resolve('ok');
console.log(promise);
Babel转码后针对火狐58不支持的特性引入了6个core-js的API补齐模块,由于火狐58已经支持了Promise属性,所以没有引入Promise API相关的补齐特性。
6)修改Babel配置文件: useBuiltIns: "usage"
,去掉a.js
中的import '@babel/polyfill';
,运行npx babel a.js -o b.js
,查看转码后的结果:
"use strict";
require("core-js/modules/es6.object.to-string.js");
require("core-js/modules/es6.promise.js");
var promise = Promise.resolve('ok');
console.log(promise);
总结:
useBuiltIns: "usage"
: 不需要额外配置@babel/polyfill
,也不需要事先引入,@babel/polyfill
会自动安装@babel/polyfill
;useBuiltIns: "entry"
: 不需要额外配置@babel/polyfill
,但需要在文件入口引入@babel/polyfill
,使用require
或者import
;useBuiltIns: false
: 不在每一个文件自动添加语法填充,需要额外在配置文件加入@babel/polyfill
配置。
6.3 corejs
该参数项的取值可以是2或3,没有设置的时候。默认值为2。这个参数项只有useBuiltIns设置为'usage'或'entry'时,才会生效。
需要注意的是,corejs取值为2的时候,需要安装并引入core-js@2版本,或者直接安装并引入polyfill也可以。如果corejs取值为3,必须安装并引入core-js@3版本才可以。
6.4 modules
这个参数项的取值可以是"amd"、"umd" 、 "systemjs" 、 "commonjs" 、"cjs" 、"auto" 、false。在不设置的时候,取默认值"auto"。
该项用来设置是否把ES6的模块化语法改成其它模块化语法。
7. @babel/plugin-transform-runtime
7.1 作用1
自动移除语法转换后内联的辅助函数(inline Babel helpers),使用@babel/runtime/helpers里的辅助函数来替代。
@babel/preset-env做语法转换做语法转换:
{
"presets": [
"@babel/env"
],
"plugins": [
]
}
需要转换的代码为:
class Person {
sayname() {
return 'name'
}
}
var john = new Person()
console.log(john)
Babel转码后的内容为:
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
var Person = /*#__PURE__*/function () {
function Person() {
_classCallCheck(this, Person);
}
_createClass(Person, [{
key: "sayname",
value: function sayname() {
return 'name';
}
}]);
return Person;
}();
var john = new Person();
console.log(john);
可以看到转换后的代码上面增加了好几个函数声明,这就是注入的函数,我们称之为辅助函数。@babel/preset-env在做语法转换的时候,注入了这些函数声明,以便语法转换后使用。
但样这做存在一个问题。在我们正常的前端工程开发的时候,少则几十个js文件,多则上千个。如果每个文件里都使用了class类语法,那会导致每个转换后的文件上部都会注入这些相同的函数声明。这会导致我们用构建工具打包出来的包非常大。
那么怎么办?一个思路就是,我们把这些函数声明都放在一个npm包里,需要使用的时候直接从这个包里引入到我们的文件里。这样即使上千个文件,也会从相同的包里引用这些函数。通过webpack这一类的构建工具打包的时候,我们只会把使用到的npm包里的函数引入一次,这样就做到了复用,减少了体积。
借助@babel/plugin-transform-runtime
插件来帮助我们解决这个问题。
module.exports = {
presets: ["@babel/env"],
plugins: ["@babel/plugin-transform-runtime"]
}
1)安装npm包:
npm install --save-dev @babel/cli @babel/core @babel/preset-env @babel/plugin-transform-runtime
2)修改Babel配置文件
{
"presets": [
"@babel/env"
],
"plugins": [
"@babel/plugin-transform-runtime"
]
}
3)执行npx babel a.js -o b.js
命令,得到转换后的内容为:
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var Person = /*#__PURE__*/function () {
function Person() {
(0, _classCallCheck2["default"])(this, Person);
}
(0, _createClass2["default"])(Person, [{
key: "sayname",
value: function sayname() {
return 'name';
}
}]);
return Person;
}();
var john = new Person();
console.log(john);
7.2 作用2
当代码里使用了core-js的API,自动引入@babel/runtime-corejs3/core-js-stable/,以此来替代全局引入的core-js/stable。
1)安装npm包:
npm install --save @babel/runtime-corejs3
npm install --save-dev @babel/cli @babel/core @babel/preset-env @babel/plugin-transform-runtime
2)修改babel配置:
{
"presets": [
"@babel/env"
],
"plugins": [
["@babel/plugin-transform-runtime", {
"corejs": 3
}]
]
}
3)修改a.js
内容
var obj = Promise.resolve();
4)执行npx babel a.js -o b.js
命令
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var obj = _promise["default"].resolve();
7.3 作用3
当代码里使用了Generator/async函数,对Generator/async进行API转换功能,默认是开启的,不需要我们设置。
8. 最后
欢迎大家关注我的公众号 -- 《前端Talkking》 😄
如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。