上一节咱们提到了ES6语法转换插件 babel-loader
, 然而babel-loader只是webpack调用 babel
的一个桥梁。 实际上,babel是一个具备强大语言转换功能的独立程序。它的主要功能是把ES6或者更新的语言语法转换为浏览器可识别的ES5语法。javascript
ES6甚至包括后来出现的ES7都是下一代的JavaScript的语法版本名称。目前chrome已经支持了大部分的ES6语法,而其余一些浏览器仍是大多数支持ES5为主。若是要在纯前端兼容低端浏览器,则须要 es6-shim
之类的前端js库解决polyfill的问题,用babel-standalone.js
解决新的es语法转换的问题。 html
在这里能够看到各个平台对ES6,ES7等的支持状况:http://kangax.github.io/compa...
这个网址很是全,其平台涵盖了全部浏览器、server端平台(包括node),以及各类polyfill对ES语法的支持状况(其实babel-preset-env
这个智能预设就是利用这个对照表进行自动化的插件加载的)。若是要详细看Nodejs全部版本对ES特性的支持状况,在这里能够看到: http://node.green/前端
至于ES6的语法学习,请参考中文的阮一峰的:http://es6.ruanyifeng.com/#do...
或者我写的 es6语法精要vue
为了提早使用更新的JavaScript语法,牛人们就发明了babel。经过babel,能够把咱们写的ES6语法的代码,转换为ES5语法。这样咱们就能够写ES6最终却能够在ES5的浏览器上跑了,岂不快哉。java
Babel 把用最新标准编写的 JavaScript 代码向下编译成能够在今天随处可用的版本。 这一过程叫作“源码到源码”编译, 也被称为转换编译(transpiling,是一个自造合成词,即转换+编译。如下也简称为转译)node
不过 Babel 的用途并不止于此,它支持语法扩展,能支持像 React 所用的 JSX 语法,同时还支持用于静态类型检查的流式语法(Flow Syntax)。更重要的是,Babel 的一切都是简单的插件,谁均可以建立本身的插件,利用 Babel 的所有威力去作任何事情。react
再进一步,Babel 自身被分解成了数个核心模块(babel-core, babel-cli, babel-node),任何人均可以利用它们来建立下一代的 JavaScript 工具。webpack
上面咱们讲了,babel是一种语言转换的技术。那么其实要想在浏览器里运行更新的语言语法,须要解决2个问题。git
对于这种新增的类、方法,咱们很容易想到能够在JavaScript运行以前去hack一个本身实现的类,如本身造一个Promise. 这种方法在业界叫作shim或者polyfill技术。好比,若是你想让你的ES6代码支持低端浏览器,这里是一个shim库: https://github.com/paulmillr/...es6
shim、polyfill所谓的垫片技术,是经过提早加载一个脚本,给宿主环境添加一些额外的功能。从而让宿主拥有更多的能力。例如能够基于JavaScript的原型能力,给Array.prototype增长额外的方法,就能够必定程度上让宿主环境拥有ES6的能力。除了对ES6+以外,咱们还得根据项目状况,添加一些额外的shim或者polyfill。好比fetch、requestAnimationFrame 这种浏览器API,若是咱们须要兼容IE8,还须要添加 ES5 shim来兼容更早的JS语法
然而,有些功能,是经过shim/polyfill技术难以实现的,好比箭头函数 =>
,由于JavaScript自身没法提供这样的能力进行扩展。因此这种能力要想实现,就必然须要进行 语言转换transpile
(我在本文也叫作transform,实际上不太严谨),即将代码中的 =>
箭头预先转换为ES5的 function
函数。
若是要在浏览器里进行transpile,babel为浏览器提供了一个运行时的转换器babel-standlone. 这个版本内置了大量的babel插件,因此能够直接在浏览器中运行并编译特定标签内的代码(而不须要安装额外的预设或插件),用户的ES6脚本放在script标签之中,可是要注明type="text/babel" (具体使用方法可参考其文档); 由于放在这种script标签内的脚本不会被浏览器执行,因此standalone版本的babel能够读取script标签内容并解析转换和执行它。 这种standalone版本主要用在那些非webpack打包的场景,好比说在线的try-out网站,jsfiddler这样的网站,或者一些APP上内嵌一个V8引擎让你REPL执行ES6语法的场景。
独立版本的babel使用方法相似下面这样:
因为babel要在浏览器的运行时对你的js代码从新编译一遍执行,性能必然有所下降,所以不适合线上运行的生产环境站点。不过咱们后面会讲到如何使用babel对代码进行预编译,这样最终运行在浏览器中的代码就是ES5了,就不存在性能问题了。因此正由于有了前端编译的过程,如今babel这种transform才流行起来。
在前端项目,咱们的目的每每是利用babel提早把ES6代码转成ES5代码而后放到浏览器执行,而不是为了当即执行他,这里就要对用babel对代码进行编译成es5的源码,而后再交给浏览器或node平台去执行。下面咱们看下几种不一样的babel使用方式。
在后端node项目中,可能你会须要直接执行ES6编写的代码来进行测试(通常也只用在mocha等测试场景,生产环节仍是建议预编译后再执行)。 基于babel-cli,你是能够实现的直接执行node代码的,由于babel-cli自带了一个babel-node的命令,能够直接执行node.js脚本。
首先安装babel-cli
npm i -g babel-cli
如今babel7以后,babel内置的模块和插件都放在了一个babel的命名空间下。并且通常建议局部安装:
npm install --save-dev @babel/core @babel/cli // babel7的cli安装方式
而后直接执行node脚本:
babel-node es6.js
若是是局部安装的,则可使用npm scripts
{ "scripts": { "start": "babel-node script.js" } }
或者使用这两种方式:
./node_modules/.bin/babel-node ./index.js // 全部node版本都支持 npx babel-node ./index.js // npm@5.2.0以后支持
注意,在执行 babel-node
时,你须要配置本身的 .babelrc
文件 (babel7
里面采用 babel.config.js
),开启babel相关的转换插件。不然你代码中的ES6特性等均可能没法使用。好比你不能使用export和import来定义模块。能够说,凡是使用babel的地方,你都必须对babel进行配置,不然babel什么都不会作。
题外话: 为何学习 webpack 的时候,在没有使用babel的时候,webpack就能转译 esmodule 的模块化语法呢? 答: webpack从版本2以后 确实内置了对 esmodule 的默认转译支持。但不表明它能转换其余语法(如箭头函数)。 因此在webpack中要使用 ES 新特性,仍是要安装并配置babel.
如今咱们来尝试开启下 node 的ES6语法转换,最简单的办法是使用官方的 env
预设(这已是babel7默认的建议预设)。先安装这个预设 babel-preset-env
:
npm i @babel/preset-env --save-dev // babel7 npm install --save-dev babel-preset-env // 老的babel版本
而后在babelrc 或 babel.config.js(babel7的配置文件) 里配置babel:
// babel.config.js const presets = [ ["@babel/env"] ]; module.exports = { presets }; // .babelrc { "presets": ["env"] }
其实跟 babel.config.js 跟 babelrc 的原理是同样的,只是JavaScript文件更灵活,因此babel7建议使用 babel.config.js。
另外,babel-node 默认是加载了 babel-polyfill 的,因此各类新的API 都能用。 (关于babel-polyfill 和 语法转换之间的情感纠葛,咱们后文再讲)
Node中另外一种直接执行ES6代码的方式是使用 babel-register,该库引入后会重写你的require加载器,让你的Node代码中require模块时自动进行ES6转码。例如在你的 index.js 中使用 babel-register:
// index.js require('babel-register') ... require('./abc.js') // abc.js能够用ES6语法编写,require时会自动使用babel编译
另外,须要注意的是,babel-register只会对require命令加载的文件转码,而不会对当前文件转码。因此最好你要设计一个什么都不作的入口让它只作一件事情:就是加载其余模块。另外,因为它是实时转码,因此只适合在开发环境使用。
通常外网要上线的代码,都不会用 babel-node 或 babel-standalone 直接去运行时运行的。所以,上线前必须提早编译为目标平台可支持的语法的代码。
若是你已经安装了 @babel/cli, 那么就有了babel和babel-node命令能够用。babel命令就是对源码进行转译的命令。使用方法以下:
# 编译 example.js 输出到 compiled.js babel example.js -o compiled.js # 或 整个目录转码 # --out-dir 或 -d 参数指定输出目录 $ babel src --out-dir lib # 或者 $ babel src -d lib # -s 参数生成source map文件 $ babel src -d lib -s
看一个例子:
// index.js // Babel Input: ES2015 arrow function [1, 2, 3].map((n) => n + 1); // Babel Output: ES5 equivalent [1, 2, 3].map(function(n) { return n + 1; });
而后安装babel相关模块,并执行编译:
npm i @babel/cli @babel/core --save-dev // 安装babel npx babel ./index.js -d dist // 编译index.js 生成到dist目录下
生成结果以下:
// Babel Input: ES2015 arrow function [1, 2, 3].map(n => n + 1); // Babel Output: ES5 equivalent [1, 2, 3].map(function (n) { return n + 1; });
跟源码同样,为何没有编译呢? 由于咱们并无对babel进行配置,在没有任何配置的状况下,babel什么都不会作。咱们像上文中babel-node那个例子同样简单安装并配置下env这个预设,就获得了编译结果:
"use strict"; // Babel Input: ES2015 arrow function [1, 2, 3].map(function (n) { return n + 1; }); // Babel Output: ES5 equivalent [1, 2, 3].map(function (n) { return n + 1; });
箭头函数已经被编译成了普通函数。
若是想在代码中调用babel API进行转码。则依赖的是babel-core,这时就不用babel-cli了。(理论上,babel-cli也是调用的babel-core而已啦)。咱们先来安装babel-core
npm install babel-core --save-dev // 老版本的babel npm install @babel/core --save-dev // babel7
安装后能够调用babel这个模块的函数进行编译:
var babel = require("@babel/core"); import { transform } from "@babel/core"; import * as babel from "@babel/core"; babel.transform("code();", options, function(err, result) { result.code; result.map; result.ast; });
通常状况下,咱们并不会用API的方式调用babel。这里就很少作讲述了。总之 本质上跟咱们经过其余方式调用babel都是同样的,配置方式也是同样的,只是API方式调用时咱们的babel配置是经过函数传给babel。具体转码API的使用,能够参考官方文档: https://babeljs.io/docs/core-....
咱们大部分状况下,作前端项目是有一套本身的构建、打包过程的,这个过程会对js进行压缩等处理。而这种状况下,咱们要用ES6,就能够顺便把babel加入到这个构建过程中(岂不是更加灵活咯)。而babel也为webpack这类的工具提供了对应的loader。(loader是webpack里的概念哦,有了babel-loader,webpack就能在打包过程当中加入babel的强大编译功能了)
其实babel除了能支持webpack,也能支持JavaScript社区全部的主流构建工具,能够访问这里寻找各类构建工具的集成帮助:
http://babeljs.io/docs/setup
babel-loader的使用方法实际上跟你使用命令CLI或者API的方式都是如出一辙的。只是这个调用者变成了webpack,webpack执行时其实相似于你经过babel API来转译你的源码。因此他们之间的关系是: webpack依赖babel-loader, babel-loader依赖babel编译相关的包(如babel-core), 而babel编译又依赖自身或社区一些插件(如preset-env等)。
babel-loader 是没法独立存在运行的。在babel-loader的package.json里你会发现有个 peerDependencies
,其中就定义了一个依赖项叫作webpack。peerDependencies依赖表示了一个模块所依赖的宿主运行环境(通常各类插件的包内会使用 peerDependencies 来代表本身的宿主依赖)。
看下使用babel-loader时,webpack的配置文件:
{ module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, use: [ { loader: 'babel-loader', options: { presets: ['es2015'] } } ] } ] } }
因为babel有本身的配置文件,因此上面代码中babel-loader中的options配置能够不写,而是放到独立配置文件当中。
OK,上面呢咱们已经学习了调用babel的N种方式。能够说,不管哪一种方式调用吗,都离不开babel的配置文件的配置(不然babel什么都不作)。如今咱们学习如何配置babel。Babel的配置文件是.babelrc或者babel.config.js(babel7推荐的),存放在项目的根目录下, rc结尾的文件一般表明运行时自动加载的文件、配置。使用Babel的第一步,就是配置这个文件。
由于全部babel命令的执行,都会去读这个文件来做为配置,若是没有配置的话,至关于没有预设转码规则,他是什么都不会作的。
你能够经过配置 插件(plugins) 或 预设(presets,也就是一组插件) 来指示 Babel 去作什么事情。
其格式以下:
{ "presets": [], "plugins": [] }
除了放到 .babelrc
中,该配置还可放到package.json中也能够生效, 如:
babel6之后,babel自身只能完成基本的核心功能。并不去作转换任何语法特性的事情。好比 transform-es2015-classes
这个插件就可让babel转译你代码中的class定义的语法。好比若是在babel6里想用箭头函数,得装上插件:npm install babel-plugin-transform-es2015-arrow-functions。而后设置babelrc配置文件:
{ "plugins": ["transform-es2015-arrow-functions"] }
若是要编译react jsx 语法,则能够安装react的插件:
npm install --save-dev @babel/preset-react
babel官方内置插件都在babel的官方仓库package目录下(babel-cli代码也在这): https://github.com/babel/babe...
关于babel6的变化可查看http://jamesknelson.com/the-s...
可是这么多插件,写起来很是麻烦。总不能让开发者记住全部插件的功能而且去配置上项目所须要的插件吧。这显然不行,因此有了preset预设。 一个预设就包含了不少插件咯。preset预设是一系列plugin插件的集合,配置了该预设,就不须要配置n个插件了,减小了配置的繁琐。好比使用 preset-es2015
的预设为何就能够转换class定义这种语法呢,其实就由于 es2015的预设中已经包含了 transform-es2015-classes
这个插件。官方的预设仍是在babel的这个仓库里.
babel内置的预设以下:
还有其余一些非官方的预设,能够在npm上进行搜索: https://www.npmjs.com/search?...
其中,es2015, es2016, es2017分别表明不一样ES标准。react、flow是另外一个领域的,暂且不表。另外还有 stage-0
, stage-1
等预设表明最新标准的提案四个阶段. (stage解释)
$ npm install --save-dev babel-preset-stage-0 $ npm install --save-dev babel-preset-stage-1 $ npm install --save-dev babel-preset-stage-2 $ npm install --save-dev babel-preset-stage-3
例子:
{ "presets": [ "es2015", "react", "stage-2" ], "plugins": [] }
若是要使用某个预设,就先安装它。例如 npm i babel-preset-es2015
。而后.babelrc中加入以下配置, 把包名的最后那个名字加进去便可:
{ "presets": [ "es2015" ], "plugins": [] }
但安装该预设时,须要使用完整的预设名称。
有一个预设叫作 babel-preset-env
, 他是一个高级的预设,能编译 ES2015+ 到 ES5,但它是根据你提供的目标浏览器版本和运行时环境来决定使用哪些插件和polyfills。 这个预设是 babel7 里面惟一推荐使用的预设, babel7建议废弃掉其余全部的预设。preset-env的目标是 make your bundles smaller and your life easier
对于preset-env预设来讲,若是不作任何配置:
{ "presets": ["env"] }
那么preset-env就至关于 babel-preset-latest
这个预设。它包含全部的 babel-preset-es2015, babel-preset-es2016, and babel-preset-es2017 预设。
若是你了解你的目标用户所使用的平台(好比大部分用户都使用了较新的浏览器),那么你大可没必要转译全部的特性。你只须要告诉babel让他转译你目标平台如今不支持的语法便可。
此时你须要配置一个数组写法, 且第二个元素是个对象用来配置preset-env的options:
{ "presets": [ ["env", { "targets": { "browsers": ["last 2 versions", "safari >= 7"] } }] ] }
其中 targets字段能够用来指明目标平台和版本等信息。若是是面向node环境,能够指明node环境版本:
"targets": { "node": "6.10" }
能够看到preset-env的options中,最重要的就是这个targets配置。targets中有2个选项,一个叫 node, 一个叫 browsers。node这个key后面能够写一个字符串类型的版本号或者"current", 若是想直接面向其babel运行环境的node版本,则能够改写为这样: "node": "current"
,此时babel会直接取 process.versions.node
中的版本号。browsers这个字段后面是一个Array类型的字符串数组或者是一个字符串。好比能够是一个字符串:
"targets": { "browsers": "> 5%" }
也能够是个字符串数组:
"targets": { "browsers": ["last 2 versions", "ie >= 7"] }
targets.browsers浏览器版本配置采用了browerslist写法,所以具体写法就去参考这个文档吧。而browserslist的配置是能够配置在多个地方的,其官方建议是配置在package.json中,这也是能够被babel识别的。browserlist的源除了能够配置在package.json中,还能够单独配置在一个叫作.browserslistrc文件中,甚至能够配置在BROWSERSLIST的环境变量中。不过,在babel的 .babelrc
中配置了targets选项时,babel就会忽略其余文件中的browserlist配置. 我我的以为,在使用babel时就配置在babel的配置文件里就行了。
preset-env还有其余一些配置,如:
babel只转换语法,不转换API。babel在语言转换方面,只转换各类ES新语法以及jsx等,但不转换ES6提供的新的API功能,例如Promise、Array的新增的原型、静态方法等。这时就须要polyfill垫片。
咱们能够分析下,对于ES6转换为ES5这件事情来讲。有几种须要作不一样实现的转换类型呢?
大概是这样的:
babel是怎么处理这些状况的呢?
咱们来看一段代码:
// 原型方法 [1, 2, 3].map((n) => n + 1); // 新类型 var a = new Promise(function (resolve, reject) { resolve('123') }) a.then(d => console.log(d)) // 新的class语法 class Foo { method() {} } // 新的async语法 async function testAsyncFn() { var a = await Promise.resolve('ok') return a } testAsyncFn().then(data=>{console.log(data)})
这段代码中包含了上面我提到的3种情形: 新箭头语法、原型/静态方法/新类型、新的复杂语法class/async。 咱们使用 preset-env的默认设置对它进行编译(preset-env预设的默认设置意味着对最新的全部ES特性都进行转换)。 转换结果以下:
"use strict"; function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } 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; } // 原型方法 [1, 2, 3].map(function (n) { return n + 1; }); // 新类型 var a = new Promise(function (resolve, reject) { resolve('123'); }); a.then(function (d) { return console.log(d); }); // 新的class语法 var Foo = /*#__PURE__*/ function () { function Foo() { _classCallCheck(this, Foo); } _createClass(Foo, [{ key: "method", value: function method() {} }]); return Foo; }(); // 新的async语法 function testAsyncFn() { return _testAsyncFn.apply(this, arguments); } function _testAsyncFn() { _testAsyncFn = _asyncToGenerator( /*#__PURE__*/ regeneratorRuntime.mark(function _callee() { var a; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return Promise.resolve('ok'); case 2: a = _context.sent; return _context.abrupt("return", a); case 4: case "end": return _context.stop(); } } }, _callee, this); })); return _testAsyncFn.apply(this, arguments); } testAsyncFn().then(function (data) { console.log(data); });
分析转换结果,咱们能够看到:
因而可知,因为默认的preset-env配置是转换全部的ES6语法,因此咱们的箭头函数、async、class都被启用了相应的插件进行转换,而且转换成功了。 如今伤脑筋的问题有两个:
怎么办呢? 下面咱们来分别分析一下这俩伤脑筋的问题如何解决。
【备注:此小节是3级标题】
babel自身只转换语法,不负责hack语法的API。这个通常用polyfill代码实现。其实用一个polyfill垫片库最简单的方式就是全量引入了。若是你是但愿在执行代码的页面里进行垫片,则在页面中引入babel-polyfill的页面版本便可:
使用 babel-polyfill/dist/polyfill.js
若是但愿在预编译阶段引入到业务代码中,你能够 require 到业务代码的开头;将来打包到bundle.js的时候就能加载polyfill的代码了。步骤以下
$ npm install --save babel-polyfill // 要做为运行依赖哦,由于polyfill要最终交给浏览器执行 $ npm install @babel/polyfill // babel7 版本的安装方式
import "babel-polyfill"
. babel7须要使用 import @babel/polyfill
. 若是是webpack能够做为entry数组的第一项。具体官方文档 示例代码:
// polyfill.js import 'babel-polyfill' console.log([1, 2, 3].includes(2)) console.log(Object.assign({}, {a: 1})) console.log(Array.from([1,2,3])) // babel.config.js const presets = [ ["@babel/env"] ]; module.exports = { presets };
用这个 preset-env 的默认配置进行 npx babel ./polyfill.js -d dist
编译,获得:
"use strict"; require("babel-polyfill"); console.log([1, 2, 3].includes(2)); console.log(Object.assign({}, { a: 1 })); console.log(Array.from([1, 2, 3]));
能够发现,babel编译的过程,除了对js模块代码进行了上文讲述的必要的语法转译外,并无作任何事情。对于此案例,仅仅就是把esmodule语法转译为commonjs语法(由于你源码中写了import这样的es模块引用的代码)。 但实际上,咱们这段代码在通过webpack等工具打包放入页面后,是能够polyfill的,由于打包后 require('babel-polyfill')
这一句会把babel-polyfill的代码打包进来。
因此,能够看出来,垫片这个事情跟babel的转译其实无关。是由于咱们在页面或代码开头引入了一些babel-polyfill的垫片代码,因此才让咱们的业务代码可使用一些新的API特性。babel-polyfill 能够垫片的API包括这些:
仔细研究babel-polyfill的话就会发现,这个包其实就是依靠 core-js
和 regenerator-runtime
实现了全部的shim/polyfill。因此在babel-polyfill这个npm包里面,只有一个index.js文件,里面直接引用了这两个npm库而已。
会看到babel-polyfill引用了core-js/shim.js, 其实shim.js这个文件就是把core-js包里的全部polyfill的API暴漏出来。
虽然polyfill的使用很简单,甚至跟babel都没有多少关系。但是如今问题来了:
优化是无止境的,让咱们看看怎么解决上面问题呢?
【备注,此小节已是4级标题】
恭喜,这个能力已经被 preset-env 这个预设所支持了。只要你打开preset-env预设的这个特性,那么preset-env就能自动根据你配置的env targets,按照目标平台的支持状况引入对应平台所需的polyfill模块。来个例子:
// babel.config.js 配置 const presets = [ ["@babel/env", { targets: { node: '0.10.42', // node: 'current' }, useBuiltIns: 'usage' // 这里是关键,要配置为 usage }] ]; module.exports = { presets };
编译以下源码:
import 'babel-polyfill' console.log([1, 2, 3].includes(2)) console.log(Object.assign({}, {a: 1})) console.log(Array.from([1,2,3])) console.log(new Promise()) console.log(Object.defineProperties()) console.log([1,2,3].flat())
因为目标平台是node的0.10版本,这个版本是不支持Object.assign, Array.from 这些API的。所以编译结果中就引入了该平台所须要的polyfill模块:
"use strict"; require("core-js/modules/es6.promise"); require("core-js/modules/es6.array.from"); require("core-js/modules/es6.object.assign"); require("core-js/modules/es7.array.includes"); require("core-js/modules/es6.string.includes"); require("babel-polyfill"); console.log([1, 2, 3].includes(2)); console.log(Object.assign({}, { a: 1 })); console.log(Array.from([1, 2, 3])); console.log(new Promise()); console.log(Object.defineProperties()); console.log([1, 2, 3].flat());
注意到咱们上面除了preset-env帮咱们按需引入的polyfill以外,还有个 require('babel-polyfill')
的代码。这一行是多余的,所以,当咱们开启了 preset-env 的useBuiltIns能力后,源码中就不要再import babel-polyfill 了。
另外就是发现:这里除了只加载了目标平台支持的,还跟进一步只加载了我代码中用到的。这是由于咱们把UserBuiltIns设置为usage。若是设置为 etnry,则只加载目标平台不支持的那些特性的polyfill,而不会根据代码使用状况来加载(这在性能上要快一些)。不过,useBuiltIns: 'entry' 是替换import "@babel/polyfill" / require("@babel/polyfill") 语句为独立的(根据环境)须要引入的polyfill特性的import语句,所以你必须在源码中显式声明 imoprt 'babel-polyfill'
。
经过上面的 useBuiltIns 案例咱们已经发行,preset-env 开启了useage的 useBuiltIns以后,它既可以根据目标平台来选择性的引入polyfill,并且它引入的polyfill是你业务代码中用到的,并不会把全部平台不支持的polyfill都引入。
这一点在 @babel/preset-env@7.0 版本我验证是 OK 的, 在以前的版本中我曾经测试发现preset-env不能实现按需引入。 应该是在7.0版本修复了这个问题。
babel-polyfill有个缺点,就是污染了宿主全局环境。此时有个babel-runtime的包能够解决局部使用的问题,babel-runtime更像是分散的 polyfill 模块,咱们能够在本身的模块里单独引入,好比 var innerPromise = require(‘babel-runtime/core-js/promise’)
,它们不会在全局环境添加未实现的方法. 这样你在使用Promise的时候就要这样了:
var innerPromise = require(‘babel-runtime/core-js/promise’) var a = new innerPromise(...)
但是,本身去发现并改写业务代码里的API调用未免有点麻烦了. 这里就有个插件来帮忙作这个事情了: babel-transform-runtime
插件。 首先安装它:
npm install --save-dev @babel/plugin-transform-runtime // babel7的安装方式 npm install --save @babel/runtime // 这个要做为运行依赖
而后咱们配置下transform-runtime插件:
// babel.config.js const presets = [ ["@babel/env", { targets: { node: '0.10.42', // node: 'current' }, useBuiltIns: 'usage' }] ]; const plugins = [ ["@babel/plugin-transform-runtime", { "corejs": 2, // 只能设置为 undefined,false,2 "helpers": true, "regenerator": true, "useESModules": false }] ] module.exports = { presets, plugins };
咱们执行编译看下结果:
"use strict"; var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); var _defineProperties = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/define-properties")); var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise")); var _from = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/array/from")); var _assign = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/assign")); require("core-js/modules/es7.array.includes"); require("core-js/modules/es6.string.includes"); console.log([1, 2, 3].includes(2)); console.log((0, _assign.default)({}, { a: 1 })); console.log((0, _from.default)([1, 2, 3])); console.log(new _promise.default()); console.log((0, _defineProperties.default)()); console.log([1, 2, 3].flat());
仿佛很完美的样子, 全部的ES6特性,都被 transform-runtime 编译成了对 corejs2的函数调用,并且是按照实际的使用状况按需引用和改写的。 不过这里有个疑惑点:就是 [1,2,3].includes 这种咱们在网上常常看到资料说 transform-runtime 没法作到的这里也作到了,这是为何呢?
实际上之因此上面编译后出现:
require("core-js/modules/es7.array.includes"); require("core-js/modules/es6.string.includes");
是由于 preset-env 的 useBuiltIns 设置致使的。 咱们知道preset-env的useBuiltIns能够按需在全局进行polyfill,因此才出现了这个垫片。 所以能够说,transform-runtime开启corejs的方案和babel-runtime的方案是互斥的,最好不要同时polyfill。transform-runtime的确没法解决实例的原型方法调用的hack问题。(固然因为transform-runtime常建议用在类库项目里,因此这种实例写法问题不大,只需类库开发者本身文档提醒开发者要在全局作includes的polyfill)
另外要注意的一点是:transform-runtime使用core-js:2的配置进行polyfill时,没法感知你目标平台环境(即不能像preset-env同样感知目标平台)。所以局部polyfill时务必要知道这一点,也就说只要你局部polyfill,你设置的preset-env环境跟你polyfill的效果无关(事实上,preset-env跟transform-runtime原本就是两个东西)
实际上babel-runtime里不止包含了全部ES6的API(即core-js),也包含了ES6语法转换时须要的那些辅助函数helpers, 也包含了async和生成器的实现(即regenerator-runtime)。仔细观察babel-runtime的包依赖也能够证明这一点. 因此 transform-runtime 的方案也不止用来局部hack polyfill,也会用在上文中提到的另一个疑难问题: “复杂语法编译后多文件重复” 的问题。
上文的例子中,咱们看到,代码中使用了ES7的async,babel会使用了定制化的 regenerator 来让 generators(生成器)和 async functions(异步函数)正常工做。 但这个regenerator函数会插入到编译后代码的最上方。若是源码中使用了ES6的class,也会出现相似的 _createClass 等函数的实现代码放在代码模块文件的上方。
此时,若是有多个js模块文件,每一个文件编译后都会有本身文件内的辅助函数插入,很是影响未来的打包合并。(会致使打包后每一个js factory工厂函数模块里都有重复代码)
要解决这个问题,咱们其实能够想到办法:
若是是用本身写代码的思路来看,根据DRY原则,若是每一个js文件里都使用同一个函数如_createClass, 那么咱们最好把他们放到一个单独的文件/模块里,而后须要的时候require它。 这样写的话,最终webpack等工具打包的时候会以模块为粒度打包,你们都依赖的这个模块只会存在一份,不会存在重复。
因此上文讲到的 _classCallback
这些辅助函数其实能够改成 var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
这样从babel-runtime包种引用。 是否是跟上面局部hack polyfill很像啊?
是的,跟局部polyfill的原理同样,咱们可让代码中的复杂ES6语法如class、async,自动引入对应的babel-runtime辅助函数。解决办法:同样是借助 transform-runtime
插件来自动化处理这一切。
步骤:插件安装方式跟上文同样
npm install --save-dev @babel/plugin-ransform-runtime npm install --save @babel/runtime // runtime是运行时依赖
而后修改 babel.config.js
的配置为:
// babel.config.js const presets = [ ["@babel/env", { targets: { node: '0.10.42', // node: 'current' }, useBuiltIns: 'usage' }] ]; const plugins = [ ["@babel/plugin-transform-runtime", { "corejs": 2, "helpers": true, "regenerator": true, "useESModules": false }] ] module.exports = { presets, plugins };
这样再运行babel编译时,这个插件会把这种generator或者class的运行时的定义移到单独的文件里。 咱们看下编译示例:
// 源码 console.log(Object.assign({}, {a: 1})) console.log(new Promise()) // 新的class语法 class Foo { method() {} }
编译结果以下:
// 编译结果 "use strict"; var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass")); var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise")); var _assign = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/assign")); console.log((0, _assign.default)({}, { a: 1 })); console.log(new _promise.default()); // 新的class语法 var Foo = /*#__PURE__*/ function () { function Foo() { (0, _classCallCheck2.default)(this, Foo); } (0, _createClass2.default)(Foo, [{ key: "method", value: function method() {} }]); return Foo; }();
可是 假如咱们这是一个Web应用,咱们发现上面的编译结果是有问题的。因为transform-runtime的存在,致使咱们本该全局polyfill的静态方法变成了局部polyfill。 这个缘由就是transform-runtime致使的。不过幸亏,tranform-runtime是可配置的,咱们能够配置他是否局部hack polyfill, 是否局部hack helpers, 是否局部x修正regenertor。
对于web应用 咱们通常是但愿:
所以这种场景下正确的babel配置应该是这样的:
// babel.config.js const presets = [ ["@babel/env", { targets: { node: '0.10.42', // node: 'current' }, useBuiltIns: 'usage' }] ]; const plugins = [ ["@babel/plugin-transform-runtime", { "corejs": false, "helpers": true, "regenerator": true, "useESModules": false }] ] module.exports = { presets, plugins };
首先,上面讲了那么多polyfill和语法转换的使用和优化方式。咱们能够看到要想正确配置babel须要看咱们的须要和场景。并且,做为babel的使用者,咱们须要理解几个个概念:helper, 垫片函数,一个是垫片库,一个是regenerator-runtime。
如此,咱们就能明白babel-polyfill只是为实现API垫片为目的的一个库,能够全局污染来垫片。它包含了core-js和regeneraor-runtime两个垫片库的实现,core-js垫片用于普通的API垫片实现,regenerator-runtime垫片用于实现generator生成器。
babel-runtime是什么?它不是一个能够直接用的库(它的package.json里都没有main),能够认为它是core-js、regenerator-runtime、helpers函数的集合。它的corejs和regeneratorRuntime能够帮助你局部不污染全局的按需加载polyfill,它的helpers能够帮助你改变babel编译async等语法带来的辅助函数重复问题。固然,在局部利用babel-runtime里的使用某个垫片函数或helpers函数时,通常都不是手工操做,而是经过transform-runtime插件来完成。
对于类库项目来讲,你可使用最新的语法特性,而后用babel+presetEnv进行语法编译后释出一个ES5的dist.js。但你代码中使用的API你不能直接全局给他polyfill掉,哪怕你按需polyfill也很差,由于这会污染全局环境。你在未知你的调用者环境的状况下,你不能污染全局。因此,类库中最好的polyfill方式是局部polyfill(利用transform-runtime或手工引入core-js的module)。在babel官方polyfill文档里有提到这个小细节
If you are looking for something that won't modify globals to be used in a tool/library, checkout the transform-runtime plugin. This means you won't be able to use the instance methods mentioned above like Array.prototype.includes.Depending on what ES2015 methods you actually use, you may not need to use @babel/polyfill or the runtime plugin. You may want to only load the specific polyfills you are using (like Object.assign) or just document that the environment the library is being loaded in should include certain polyfills.
也就是说,若是你是开发一个类库项目,那么你通常是不要污染全局的。若是你不想污染全局,你能够用transform-runtime配合 babel-runtime的方案,可是这个方案 没法解决实例的原型方法的polyfill问题 这个缺点你必需要注意。 而若是你很明显地知道这个类库调用了哪些较新的API(你的客户环境可能会不支持的API),那么你就不要使用 @babel/polyfill 或 babel-runtime方案了,你能够直接手工走core-js来加载它,或者你在你的类库文档里告诉你的开发者说你这个类库须要依赖什么polyfill。
这种项目因为不怕全局polyfill污染,所以通常采用全局polyfill的方式。不过为了提升页面性能,通常也经过 preset-env 配合 useBuiltIns配置的方式实现按需加载polyfill。注意,如今版本的preset-env若是开启了useBuildIns,你就不要本身在代码的开头出引用babel-polyfill了。
至于复杂语法转换带来的辅助函数问题,就靠 transform-runtime来解决了。注意不要开启 core-js选项,从而避免局部polyfill(由于你已经preset-env+useBuiltIns使用了全局polyfill的方式)。
async generator转换成新的辅助函数后,到底须要依赖哪些东西才能正常运行?
通过个人测试发现,它须要依赖两个polyfill:
咱们进行preset-env+useBultIns的全局转换,能够看到结果里面自动引入了须要的polyfill:
require("regenerator-runtime/runtime"); require("core-js/modules/es6.promise"); function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
咱们再试试用transform-runtime来局部polyfill,能够看到结论是同样的:
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator")); var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator"));
只是这个promise和regenerator-runtime的polyfill换成了babel-runtime(或runtime-corejs2)里面的。
用ES6写代码以后,测试有时也但愿使用ES6来编写。并且eslint进行代码检查时也要利用babel进行转换。关于结合mocha的使用将在后面的文章讲解。eslint的使用请参看博文[[实践]-使用ESLINT检查代码规范]()
总之,测试这些环节执行ES6的测试用例代码时就不需走编译步骤了。因为不在意性能,所以能够直接走实时编译执行的模式。
mocha --compilers js:babel-core/register --require babel-polyfill
如今流行框架,都在使用babel进行框架特有的语法转换。例如除了react,还有Vue2.0的jsx。
咱们也能够写本身的babel插件,详情可参考手册: https://github.com/thejamesky...
官方脚手架:https://github.com/babel/gene...
下一节,就用这些知识点真正搭建一个类库开发项目了。
babel-handbook中文
babel 7 教程
babel-preset-env
https://babeljs.io/docs/plugi...
https://github.com/brunoyang/...
你真的会用 Babel 吗?
21 分钟精通前端 Polyfill 方案
https://leanpub.com/setting-u...
babel笔记
测试external-helper
creeperyang的博客
Babel 入门教程(三):babel-plugin- 插件及与 babel-preset- 预设插件的关系
Babel 入门教程(六):babel-polyfill 与 相关插件和包