本文首发于 vivo互联网技术 微信公众号
连接: mp.weixin.qq.com/s/plJewhUd0…
做者:Morrainhtml
在上一节 《CommonJS:不是前端却革命了前端》中,咱们聊到了 ES6 Module,它是 ES6 中对模块的规范,ES6 是 ECMAScript 6.0 的简称,泛指 JavaScript 语言的下一代标准,它的第一个版本 ES2015 已经在 2015 年 6 月正式发布,本文中提到的 ES6 包括 ES201五、ES201六、ES2017等等。在第一节的《Web:一路前行一路忘川》中也提到过,ES2015 从制定到发布历经了十几年,引入了不少的新特性以及新的机制,浏览器对 ES6 的支持进度远远赶不上前端开发小哥哥们使用 ES6 的热情,因而矛盾就日益显著……前端
先来看下它在官网上的定义:webpack
Babel is a JavaScript compilergit
没错就一句话,Babel 是 JavaScript 的编译器。至于什么是编译器,能够参考 the-super-tiny-compiler 这个项目,能够找到很好的答案。es6
本文是以 Babel 7.9.0 版本进行演示和讲解的,另外建议学习者阅读英文官网,中文官网会比原版网站慢一个版本,而且不少依然是英文的。github
Babel 就是一套解决方案,用来把 ES6 的代码转化为浏览器或者其它环境支持的代码。注意个人用词哈,我说的不是转化为 ES5 ,由于不一样类型以及不一样版本的浏览器对 ES6 新特性的支持程度都不同,对于浏览器已经支持的部分,Babel 能够不转化,因此 Babel 会依赖浏览器的版本,后面会讲到。这里能够先参考 browerslist 项目。web
在学习任何一门知识前,我都习惯先了解它的历史,这样才能深入理解它存在乎义。chrome
Babel 的做者是 FaceBook 的工程师 Sebastian McKenzie。他在 2014 年发布了一款 JavaScript 的编译器 6to5。从名字就能看出来,它主要的做用就是将 ES6 转化为 ES5。express
这里的 ES6 指 ES2015,由于当时尚未正式发布, ES2015 的名字还未被正式肯定。npm
因而不少人评价,6to5 只是 ES6 获得支持前的一个过渡方案,它的做者很是不一样意这个观点,认为 6to5 不光会按照标准逐步完善,依然具有很是大的潜力反过来影响并推动标准的制定。正由于如此 6to5 的团队以为 '6to5' 这个名字并无准确的传达这个项目的目标。加上 ES6 正式发布后,被命名为 ES2015,对于 6to5 来讲更偏离了它的初衷。因而 2015 年 2 月 15 号,6to5 正式改名为 Babel。
(图片来源于网络)
Babel 是巴比伦文化里的通天塔,用来给 6to5 这个项目命名真得太贴切了!羡慕这些牛逼的人,不光代码写得好,还这么有文化,不像咱们,起个变量名都得憋上半天,吃了没有文化的亏。这也是为何我把这篇文章起名为 《Babel:把 ES6 送上天的通天塔》的缘由。
了解了 Babel 是什么后,很明显咱们就要开始考虑怎么使用 Babel 来转化 ES6 的代码了,除了 Babel 自己提供的 cli 等工具外,它还支持和其它打包工具配合使用,譬如 webpack、rollup 等等,能够参考官网对不一样平台提供的配置说明。
本文为了感觉 Babel 最原始的用法,不结合其它任何工具,直接使用 Babel 的 cli 来演示。
使用以下命令构建一个 npm 包,并新建 src 目录 和 一个 index.js 文件。
npm init -y复制代码
package.json 内容以下:
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}复制代码
npm install --save-dev @babel/core @babel/cli @babel/preset-env复制代码
后面会介绍这些包的做用,先看用法
增长 babel 命令来编译 src 目录下的文件到 dist 目录:
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"babel": "babel src --out-dir dist",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.0"
}
}复制代码
在工程的根目录添加 babel.config.js 文件,增长 Babel 编译的配置,没有配置是不进行编译的。
const presets = [
[
'@babel/env',
{
debug: true
}
]
]
const plugins = []
module.exports = { presets, plugins }复制代码
上例中 debug 配置是为了打印出 Babel 工做时的日志,能够方便的看来,Babel 转化了哪些语法。
const presets = [
[
'@babel/env',
{
debug: true
}
]
]
const plugins = []
if (process.env["ENV"] === "prod") {
plugins.push(...)
}
module.exports = { presets, plugins }复制代码
配置好后,咱们运行 npm run babel 命令,能够看到 dist 文件夹下生成了 index.js 文件,内容以下所示:
// src/index.js
const add = (a, b) => a + b
// dist/index.js
"use strict";
var add = function add(a, b) {
return a + b;
};复制代码
能够看到,ES6 的 const 被转化为 var ,箭头函数被转化为普通函数。同时打印出来以下日志:
> babel src --out-dir dist
@babel/preset-env: `DEBUG` option
Using targets:
{}
Using modules transform: auto
Using plugins:
proposal-nullish-coalescing-operator {}
proposal-optional-chaining {}
proposal-json-strings {}
proposal-optional-catch-binding {}
transform-parameters {}
proposal-async-generator-functions {}
proposal-object-rest-spread {}
transform-dotall-regex {}
proposal-unicode-property-regex {}
transform-named-capturing-groups-regex {}
transform-async-to-generator {}
transform-exponentiation-operator {}
transform-template-literals {}
transform-literals {}
transform-function-name {}
transform-arrow-functions {}
transform-block-scoped-functions {}
transform-classes {}
transform-object-super {}
transform-shorthand-properties {}
transform-duplicate-keys {}
transform-computed-properties {}
transform-for-of {}
transform-sticky-regex {}
transform-unicode-regex {}
transform-spread {}
transform-destructuring {}
transform-block-scoping {}
transform-typeof-symbol {}
transform-new-target {}
transform-regenerator {}
transform-member-expression-literals {}
transform-property-literals {}
transform-reserved-words {}
transform-modules-commonjs {}
proposal-dynamic-import {}
Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set.
Successfully compiled 1 file with Babel.复制代码
在了解了如何使用后,咱们一块儿来探寻一下编译背后的事情,同时会熟悉 Babel 的组成和进阶用法。
前面提到 Babel 其实就是一个纯粹的 JavaScript 的编译器,任何一个编译器工做流程大体均可以分为以下三步:
Parser 解析源文件
Transfrom 转换
Generator 生成新文件
Babel 也不例外,以下图所示:
(图片来源于网络)
由于 Babel 使用是 acorn 这个引擎来作解析,这个库会先将源码转化为抽象语法树 (AST),再对 AST 做转换,最后将转化后的 AST 输出,便获得了被 Babel 编译后的文件。
那 Babel 是如何知道该怎么转化的呢?答案是经过插件,Babel 为每个新的语法提供了一个插件,在 Babel 的配置中配置了哪些插件,就会把插件对应的语法给转化掉。插件被命名为 @babel/plugin-xxx 的格式。
上面提到过 @babel/preset-* 实际上是转换插件的集合,最经常使用的就是 @babel/preset-env,它包含了 大部分 ES6 的语法,具体包括哪些插件,能够在 Babel 的日志中看到。若是源码中使用了不在 @babel/preset-env 中的语法,会报错,手动在 plugins 中增长便可。
例如 ES6 明确规定,Class 内部只有静态方法,没有静态属性。但如今有一个提案提供了类的静态属性,写法是在实例属性的前面,加上 static 关键字。
// src/index.js
const add = (a, b) => a + b
class Person {
static a = 'a';
static b;
name = 'morrain';
age = 18
}复制代码
根据报错的提示,添加 @babel/plugin-proposal-class-properties 便可。
npm install --save-dev @babel/plugin-proposal-class-properties复制代码
// babel.config.js
const presets = [
[
'@babel/env',
{
debug: true
}
]
]
const plugins = ['@babel/plugin-proposal-class-properties']
module.exports = { presets, plugins }复制代码
// src/index.js
const add = (a, b) => a + b
// dist/index.js 没有配置targets
"use strict";
var add = function add(a, b) {
return a + b;
};复制代码
// babel.config.js
const presets = [
[
'@babel/env',
{
debug: true,
targets: {
chrome: '58'
}
}
]
]
const plugins = ['@babel/plugin-proposal-class-properties']
module.exports = { presets, plugins }复制代码
// src/index.js
const add = (a, b) => a + b
// dist/index.js 配置targets chrome 58
"use strict";
const add = (a, b) => a + b;复制代码
为后方便后续的讲解,把 targets 的配置去掉,让 Babel 默认转译全部语法。
polyfill 直译是垫片的意思,又是 Babel 里一个很是重要的概念。先看下面几行代码:
// src/index.js
const add = (a, b) => a + b
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()复制代码
// dist/index.js
"use strict";
var add = function add(a, b) {
return a + b;
};
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();复制代码
Object.defineProperty(Array.prototype, 'includes',function(){
...
})复制代码
安装 core-js
npm install --save core-js复制代码
配置 useBuiltIns
在 @babel/preset-env 中经过 useBuiltIns 参数来控制 built-in 的注入。它能够设置为 'entry'、'usage' 和 false 。默认值为 false,不注入垫片。
设置为 'entry' 时,只须要在整个项目的入口处,导入 core-js 便可。
// src/index.js
import 'core-js'
const add = (a, b) => a + b
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()
// dist/index.js
"use strict";
require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.promise");
//
// …… 这里还有不少
//
require("regenerator-runtime/runtime");
var add = function add(a, b) {
return a + b;
};
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();复制代码
设置为 'usage' 时,就不用在项目的入口处,导入 core-js了,Babel 会在编译源码的过程当中根据 built-in 的使用状况来选择注入相应的实现。
// src/index.js
const add = (a, b) => a + b
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise()
// dist/index.js
"use strict";
require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
require("core-js/modules/es7.array.includes");
var add = function add(a, b) {
return a + b;
};
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise();复制代码
当 useBuiltIns 设置为 'usage' 或者 'entry' 时,还须要设置 @babel/preset-env 的 corejs 参数,用来指定注入 built-in 的实现时,使用 corejs 的版本。不然 Babel 日志输出会有一个警告。
最终的 Babel 配置以下:
// babel.config.js
const presets = [
[
'@babel/env',
{
debug: true,
useBuiltIns: 'usage',
corejs: 3,
targets: {}
}
]
]
const plugins = ['@babel/plugin-proposal-class-properties']
module.exports = { presets, plugins }复制代码
在介绍 @babel/plugin-transform-runtime 的用途以前,先前一个例子:
// src/index.js
const add = (a, b) => a + b
const arr = [1, 2]
const hasThreee = arr.includes(3)
new Promise(resolve=>resolve(10))
class Person {
static a = 1;
static b;
name = 'morrain';
age = 18
}
// dist/index.js
"use strict";
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.define-property");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var add = function add(a, b) {
return a + b;
};
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise(function (resolve) {
return resolve(10);
});
var Person = function Person() {
_classCallCheck(this, Person);
_defineProperty(this, "name", 'morrain');
_defineProperty(this, "age", 18);
};
_defineProperty(Person, "a", 1);
_defineProperty(Person, "b", void 0);复制代码
而 @babel/plugin-transform-runtime 就是为了复用这些 helper 函数,缩小代码体积而生的。固然除此以外,它还能为编译后的代码提供一个沙箱环境,避免全局污染。
使用 @babel/plugin-transform-runtime
①安装
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime复制代码
②修改 Babel plugins 配置,增长@babel/plugin-transform-runtime
// babel.config.js
const presets = [
[
'@babel/env',
{
debug: true,
useBuiltIns: 'usage',
corejs: 3,
targets: {}
}
]
]
const plugins = [
'@babel/plugin-proposal-class-properties',
[
'@babel/plugin-transform-runtime'
]
]
module.exports = { presets, plugins }复制代码
// dist/index.js
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var add = function add(a, b) {
return a + b;
};
var arr = [1, 2];
var hasThreee = arr.includes(3);
new Promise(function (resolve) {
return resolve(10);
});
var Person = function Person() {
(0, _classCallCheck2["default"])(this, Person);
(0, _defineProperty2["default"])(this, "name", 'morrain');
(0, _defineProperty2["default"])(this, "age", 18);
};
(0, _defineProperty2["default"])(Person, "a", 1);
(0, _defineProperty2["default"])(Person, "b", void 0);复制代码
到目前为止,对于 built-in 类型的语法仍是经过 require("core-js/modules/xxxx") polyfill 的方式来实现的,例如为了支持 Array.prototype.includes 方法,须要 require
("core-js/modules/es.array.includes") 在 Array.prototype 中添加 includes 方法来实现的,但这会致使一个问题,它是直接修改原型的,会形成全局污染。若是你开发的是独立的应用问题不大,但若是开发的是工具库,被其它项目引用,而刚好该项目自身实现了 Array.prototype.includes 方法,这样就出了大问题!而 @babel/plugin-transform-runtime 能够解决这个问题,只须要配置 @babel/plugin-transform-runtime 的参数 corejs。该参数默认为 false,能够设置为 2 或者 3,分别对应 @babel/runtime-corejs2 和 @babel/runtime-corejs3。
把 @babel/plugin-transform-runtime 的 corejs 的值设置为3,把 @babel/runtime 替换为 @babel/runtime-corejs3。
去掉 @babel/preset-env 的 useBuiltIns 和 corejs 的配置,去掉 core-js。由于使用 @babel/runtime-corejs3 来实现对 built-in 类型语法的兼容,不用再使用 useBuiltIns了。
npm uninstall @babel/runtime
npm install --save @babel/runtime-corejs3
npm uninstall core-js复制代码
// babel.config.js
const presets = [
[
'@babel/env',
{
debug: true,
targets: {}
}
]
]
const plugins = [
'@babel/plugin-proposal-class-properties',
[
'@babel/plugin-transform-runtime',
{
corejs: 3
}
]
]
module.exports = { presets, plugins }
// dist/index.js
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/defineProperty"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
var add = function add(a, b) {
return a + b;
};
var arr = [1, 2];
var hasThreee = (0, _includes["default"])(arr).call(arr, 3);
new _promise["default"](function (resolve) {
return resolve(10);
});
var Person = function Person() {
(0, _classCallCheck2["default"])(this, Person);
(0, _defineProperty2["default"])(this, "name", 'morrain');
(0, _defineProperty2["default"])(this, "age", 18);
};
(0, _defineProperty2["default"])(Person, "a", 1);
(0, _defineProperty2["default"])(Person, "b", void 0);复制代码
截至目前为止,对于 built-in 类型的语法的 polyfill,一共有三种方式:
使用 @babel/preset-env ,useBuiltIns 设置为 'entry'
使用 @babel/preset-env ,useBuiltIns 设置为 'usage'
使用 @babel/plugin-transform-runtime
前两种方式支持设置 targets ,能够根据目标环境来适配。useBuiltIns 设置为 'entry' 会注入目标环境不支持的全部 built-in 类型语法,useBuiltIns 设置为 'usage' 会注入目标环境不支持的全部被用到的 built-in 类型语法。注入的 built-in 类型的语法会污染全局。
第三种方式目前不支持设置 targets,因此不会考虑目标环境是否已经支持,它是经过局部变量的方式实现了全部被用到的 built-in 类型语法,不会污染全局。
针对第三种方式不支持设置 targets 的问题,Babel 正在考虑解决,目前意向的方案是经过 Polyfill provider 来统一 polyfill 的实现:
废弃 @babel/preset-env 中 useBuiltIns 和 corejs 两个参数,再也不经过 @babel/preset-env 实现 polyfill。
废弃 @babel/plugin-transform-runtime 中的 corejs 参数,也再也不经过 @babel/plugin-transform-runtime 来实现 polyfill。
增长 polyfills 参数,相似于如今 presets 和 plugins,用来取代如今的 polyfill 方案。
把 @babel/preset-env 中 targets 参数,往上提一层,和 presets、plugins、polyfills 同级别,并由它们共享。
这个方案实现后,Babel 的配置会是下面的样子:
// babel.config.js
const targets = [
'>1%'
]
const presets = [
[
'@babel/env',
{
debug: true
}
]
]
const plugins = [
'@babel/plugin-proposal-class-properties'
]
const polyfills = [
[
'corejs3',
{
method: 'usage-pure'
}
]
]
module.exports = { targets, presets, plugins, polyfills }复制代码
'entry-global' 等价于 @babel/preset-env 中的 useBuiltIns: 'entry'
'usage-global' 等价于 @babel/preset-env 中的 useBuiltIns: 'usage'
'usage-pure' 等价于 @babel/plugin-transform-runtime 中的 corejs
本文为了讲解方便,都是用 Babel 原生的 @babel/cli 来编译文件,实际使用中,更多的是结合 webpack、rollup 这样第三方的工具来使用的。
因此下一节,咱们聊聊打包工具 webpack。
更多内容敬请关注 vivo 互联网技术 微信公众号
注:转载文章请先与微信号:Labs2020 联系。