咱们知道,各个浏览器对 JavaScript 版本的支持度各不相同,有不少优秀的新语法都不能直接在浏览器中运行。为了解决这个“沟通不顺畅”的问题,因此就有了 Babel。Babel是一个工具集,主要用于将ES6版本的JavaScript代码转为ES5等向后兼容的JS代码,从而能够运行在低版本浏览器或其它环境中。所以,在编码过程当中,可使用ES6/7/8编码,而后使用Babel将代码编译为向后兼容的Javascript代码,这样就不用担忧所在环境是否支持了。javascript
简单来讲,Babel的工做就是:css
Babel的原理很简单,首先是将源码转成抽象语法树(AST)
,而后对语法树进行处理生成新的预发树,最后将新语法树生成新的Javascript代码,整个编译过程分为parsing(解析)、transforming(转换)、generating(生成)。Babel只负责编译新标准引入的语法,好比 Arrow function、Class、ES Module 等,它不会编译原生对象新引入的方法和 API,好比 Array.includes,Map,Set 等,这些须要经过 Polyfill 来解决,文章后面会提到。html
Babel7的npm包都是放到Babel域下, 例如@babel/cli、@babel/core等,在Babel6中,安装的包名是babel-core、babel-cli。本文将以Babel7为例进行讲解。前端
一、@babel/clijava
@babel/cli 是 Babel 提供的内建命令行工具。node
安装:npm install i -S @babel/cli
react
二、@babel/corewebpack
@babel/core是咱们使用Bable进行转码的核心npm包,咱们使用的babel-cli、babel-node都依赖这个包。在命令行和webpack进行转码的时候都是经过Node来调用@babel/core相关功能API来进行的。git
安装: npm install --save-dev @babel/core
es6
一般,咱们须要指定Babel的编译规则来编译代码。Babel的配置文件默认会在当前目录寻找文件,有:.babelrc
、.babelrc.js
、babel.config.js
、package.json
,它们的配置项都是同样的,做用也同样,只须要选择一种便可。
一、.babelrc
配置:
{
"presets": ["es2015", "react"],
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
复制代码
二、babel.config.js
和.babelrc.js
配置:
module.exports = {
"presets": ["es2015", "react"],
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
复制代码
三、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(预设)两个数组(固然还有其余的,咱们先只关注这两个)。
插件是用来定义如何转换你的代码的,通常是单独的某个新特性。当在Babel的配置项中填写了须要使用的插件,Babel编译的时候就会去加载node_modules中对应的npm包,而后编译插件的对应的语法。
Class
语法for...of
语法...Babel支持的插件很是多,若是每个新特性转换都须要经过安装插件来解决,那么咱们的开发效率会变得很是低效,Babel配置文件就会变得很是臃肿。因而,Babel推出了懒人包presets
。
预设就是一堆插件包的集合,例如babel-preset-es2015就是全部处理es2015的二十多个Babel插件的集合。这样咱们就不须要写一大堆插件的配置项了,使用一个预设便可。
经常使用的preset包有:
解释下,stage-x,这里面包含的都是当年最新规范的草案,每一年更新。这里面还细分为:
stage 0
- 设想: 只是一个想法,可能有 Babel 插件,stage-0 的功能范围最广大,包含 stage-1 , stage-2 以及 stage-3 的全部功能。stage 1
- 提案: 初步尝试,值得跟进。stage 2
- 初稿: 完成初步规范。stage 3
- 候选: 完成规范和浏览器初步实现。stage 4
- 完成: 将被添加到下一年度发布。全部的预设也都须要安装npm包到node_modules中才可使用。
plugins插件数组和presets预设数组是有顺序要求的。若是两个插件或预设都要处理同一个代码片断,那么会根据插件和预设的顺序来执行。规则以下:
一、插件plugins的执行顺序是从左到右执行的。
{
"plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
复制代码
在上面的示例中,Babel 在进行 AST 遍历的时候会先调用 transform-decorators-legacy 插件中定义的转换方法,而后再调用 transform-class-properties 中的方法。
二、预设presets的执行顺序是从右到左执行的。
{
"presets": [
"a",
"b",
"c"
]
}
复制代码
它的执行顺序是 c、b、a,是否是有点奇怪,这主要是为了确保向后兼容,由于大多数用户将 "es2015" 放在 "stage-0" 以前。
三、插件plugins在预设presets以前执行
在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
。
在默认状况下,@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
自己仍是其组合子包。使用方式有如下几种:
<script src="https://cdn.bootcss.com/babel-polyfill/7.6.0/polyfill.js"></script>
复制代码
import './polyfill.js';
var promise = Promise.resolve('ok');
console.log(promise);
复制代码
1)安装@babel/polyfill
2)修改a.js
内容
import '@babel/polyfill';
var promise = Promise.resolve('ok');
console.log(promise);
复制代码
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);
复制代码
const path = require('path');
module.exports = {
entry: ['./polyfill.js', './a.js'],
output: {
filename: 'b.js',
path: path.resolve(__dirname, '')
},
mode: 'development'
};
复制代码
const path = require('path');
module.exports = {
entry: ['@babel/polyfill', './a.js'],
output: {
filename: 'b.js',
path: path.resolve(__dirname, '')
},
mode: 'development'
};
复制代码
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
可能会污染全局变量,给不少类的原型链上都做了修改,这就有不可控的因素存在。@babel/preset-env
的参数项,数量有10多个,但大部分咱们要么用不到,要么已经或将要弃用。这里建议你们掌握重点的几个参数项,有的放矢。重点要学习的参数项有targets、useBuiltIns、modules和corejs这四个,能掌握这几个参数项的真正含义。
该参数项能够取值为字符串、字符串数组或对象,不设置的时候取默认值空对象{}。
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的。
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
配置。该参数项的取值能够是2或3,没有设置的时候。默认值为2。这个参数项只有useBuiltIns设置为'usage'或'entry'时,才会生效。
须要注意的是,corejs取值为2的时候,须要安装并引入core-js@2版本,或者直接安装并引入polyfill也能够。若是corejs取值为3,必须安装并引入core-js@3版本才能够。
这个参数项的取值能够是"amd"、"umd" 、 "systemjs" 、 "commonjs" 、"cjs" 、"auto" 、false。在不设置的时候,取默认值"auto"。
该项用来设置是否把ES6的模块化语法改为其它模块化语法。
自动移除语法转换后内联的辅助函数(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);
复制代码
当代码里使用了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();
复制代码
当代码里使用了Generator/async函数,对Generator/async进行API转换功能,默认是开启的,不须要咱们设置。
欢迎你们关注个人公众号 -- 《前端Talkking》 😄
若是还有什么疑问或者建议,能够多多交流,原创文章,文笔有限,才疏学浅,文中如有不正之处,万望告知。