本文首发于 vivo互联网技术 微信公众号
连接: https://mp.weixin.qq.com/s/plJewhUd0xDXh3Ce4CGpHg
做者:Morrain
在上一节 《CommonJS:不是前端却革命了前端》中,咱们聊到了 ES6 Module,它是 ES6 中对模块的规范,ES6 是 ECMAScript 6.0 的简称,泛指 JavaScript 语言的下一代标准,它的第一个版本 ES2015 已经在 2015 年 6 月正式发布,本文中提到的 ES6 包括 ES201五、ES201六、ES2017等等。在第一节的《Web:一路前行一路忘川》中也提到过,ES2015 从制定到发布历经了十几年,引入了不少的新特性以及新的机制,浏览器对 ES6 的支持进度远远赶不上前端开发小哥哥们使用 ES6 的热情,因而矛盾就日益显著……html
先来看下它在官网上的定义:前端
Babel is a JavaScript compilerwebpack
没错就一句话,Babel 是 JavaScript 的编译器。至于什么是编译器,能够参考the-super-tiny-compiler这个项目,能够找到很好的答案。git
本文是以 Babel 7.9.0 版本进行演示和讲解的,另外建议学习者阅读英文官网,中文官网会比原版网站慢一个版本,而且不少依然是英文的。
Babel 就是一套解决方案,用来把 ES6 的代码转化为浏览器或者其它环境支持的代码。注意个人用词哈,我说的不是转化为 ES5 ,由于不一样类型以及不一样版本的浏览器对 ES6 新特性的支持程度都不同,对于浏览器已经支持的部分,Babel 能够不转化,因此 Babel 会依赖浏览器的版本,后面会讲到。这里能够先参考browerslist项目。es6
在学习任何一门知识前,我都习惯先了解它的历史,这样才能深入理解它存在乎义。github
Babel 的做者是 FaceBook 的工程师 Sebastian McKenzie。他在 2014 年发布了一款 JavaScript 的编译器 6to5。从名字就能看出来,它主要的做用就是将 ES6 转化为 ES5。web
这里的 ES6 指 ES2015,由于当时尚未正式发布, ES2015 的名字还未被正式肯定。
因而不少人评价,6to5 只是 ES6 获得支持前的一个过渡方案,它的做者很是不一样意这个观点,认为 6to5 不光会按照标准逐步完善,依然具有很是大的潜力反过来影响并推动标准的制定。正由于如此 6to5 的团队以为 '6to5' 这个名字并无准确的传达这个项目的目标。加上 ES6 正式发布后,被命名为 ES2015,对于 6to5 来讲更偏离了它的初衷。因而 2015 年 2 月 15 号,6to5 正式改名为 Babel。chrome
(图片来源于网络)express
Babel 是巴比伦文化里的通天塔,用来给 6to5 这个项目命名真得太贴切了!羡慕这些牛逼的人,不光代码写得好,还这么有文化,不像咱们,起个变量名都得憋上半天,吃了没有文化的亏。这也是为何我把这篇文章起名为 《Babel:把 ES6 送上天的通天塔》的缘由。npm
了解了 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 的编译器,任何一个编译器工做流程大体均可以分为以下三步:
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 }
@babel/preset-env 中还有一个很是重要的参数 targets,最先的时候咱们就提过,Babel 转译是按需的,对于环境支持的语法能够不作转换的。就是经过配置 targets 属性,让 Babel 知道目标环境,从而只转译环境不支持的语法。若是没有配置会默认转译全部 ES6 的语法。
// src/index.js const add = (a, b) => a + b // dist/index.js 没有配置targets "use strict"; var add = function add(a, b) { return a + b; };
按以下配置 targets
// 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;
能够看到 const 和箭头函数都没有被转译,由于这个版本的 chrome 已经支持了这些特性。能够根据需求灵活的配置目标环境。
为后方便后续的讲解,把 targets 的配置去掉,让 Babel 默认转译全部语法。
polyfill 直译是垫片的意思,又是 Babel 里一个很是重要的概念。先看下面几行代码:
// src/index.js const add = (a, b) => a + b const arr = [1, 2] const hasThreee = arr.includes(3) new Promise()
按以前的方法,执行 npm run babel 后,咱们惊奇的发现,Array.prototype.includes 和 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();
原来 Babel 把 ES6 的标准分为 syntax 和 built-in 两种类型。syntax 就是语法,像 const、=> 这些默认被 Babel 转译的就是 syntax 的类型。而对于那些能够经过改写覆盖的语法就认为是 built-in,像 includes 和 Promise 这些都属于 built-in。而 Babel 默认只转译 syntax 类型的,对于 built-in 类型的就须要经过 @babel/polyfill 来完成转译。@babel/polyfill 实现的原理也很是简单,就是覆盖那些 ES6 新增的 built-in。示意以下:
Object.defineProperty(Array.prototype, 'includes',function(){ ... })
因为 Babel 在 7.4.0 版本中宣布废弃 @babel/polyfill ,而是经过 core-js 替代,因此本文直接使用 core-js 来说解 polyfill 的用法。
npm install --save core-js
在 @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);
在编译的过程当中,对于 built-in 类型的语法经过 require("core-js/modules/xxxx") polyfill 的方式来兼容,对于 syntax 类型的语法在转译的过程会在当前模块中注入相似 _classCallCheck 和 _defineProperty 的 helper 函数来实现兼容。对于一个模块而言,可能还好,但对于项目中确定是不少模块,每一个模块模块都注入这些 helper 函数,势必会形成代码量变得很大。
而 @babel/plugin-transform-runtime 就是为了复用这些 helper 函数,缩小代码体积而生的。固然除此以外,它还能为编译后的代码提供一个沙箱环境,避免全局污染。
使用 @babel/plugin-transform-runtime
npm install --save-dev @babel/plugin-transform-runtime npm install --save @babel/runtime
其中 @babel/plugin-transform-runtime 是编译时使用的,安装为开发依赖,而 @babel/runtime 其实就是 helper 函数的集合,须要被引入到编译后代码中,因此安装为生产依赖
// 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);
能够看到 Promise 和 arr.includes 的实现已经变成局部变量,并无修改全局上的实现。
截至目前为止,对于 built-in 类型的语法的 polyfill,一共有三种方式:
前两种方式支持设置 targets ,能够根据目标环境来适配。useBuiltIns 设置为 'entry' 会注入目标环境不支持的全部 built-in 类型语法,useBuiltIns 设置为 'usage' 会注入目标环境不支持的全部被用到的 built-in 类型语法。注入的 built-in 类型的语法会污染全局。
第三种方式目前不支持设置 targets,因此不会考虑目标环境是否已经支持,它是经过局部变量的方式实现了全部被用到的 built-in 类型语法,不会污染全局。
针对第三种方式不支持设置 targets 的问题,Babel 正在考虑解决,目前意向的方案是经过 Polyfill provider 来统一 polyfill 的实现:
这个方案实现后,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 }
配置中的 method 值有 'entry-global'、'usage-global'、'usage-pure' 三种。
本文为了讲解方便,都是用 Babel 原生的 @babel/cli 来编译文件,实际使用中,更多的是结合 webpack、rollup 这样第三方的工具来使用的。
因此下一节,咱们聊聊打包工具 webpack。
更多内容敬请关注 vivo 互联网技术 微信公众号
注:转载文章请先与微信号:Labs2020 联系。