欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Babel文档

程序员文章站 2024-03-21 13:36:10
...

介绍

Babel 是一个通用的多用途 JavaScript 编译器。通过 Babel 你可以使用(并创建)下一代的 JavaScript,以及下一代的 JavaScript 工具。

作为一种语言,JavaScript 在不断发展,新的标准/提案和新的特性层出不穷。 在得到广泛普及之前,Babel 能够让你提前(甚至数年)使用它们。    

Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译
(transpiling,是一个自造合成词,即转换+编译。以下也简称为转译)。      

例如,Babel 能够将新的 ES2015 箭头函数语法:

const square = n => n * n;

转译为:

const square = function square(n) {
  return n * n;
};
不过 Babel 的用途并不止于此,它支持语法扩展,能支持像 React 所用的 JSX 语法,同时还支持用于静态类型检查的流式语法(Flow Syntax)。

更重要的是,Babel 的一切都是简单的插件,谁都可以创建自己的插件,利用 Babel 的全部威力去做任何事情。

再进一步,Babel 自身被分解成了数个核心模块,任何人都可以利用它们来创建下一代的 JavaScript 工具。

已经有很多人都这样做了,围绕着 Babel 涌现出了非常大规模和多样化的生态系统。 在这本手册中,我将介绍如何使用 Babel 的内建工具
以及一些来自于社区的非常有用的东西。

Babel 编译代码的几种方式

由于 JavaScript 社区没有统一的构建工具、框架、平台等等,因此 Babel 正式集成了对所有主流工具的支持。 从 Gulp 到 Browserify,
从 Ember 到 Meteor,不管你的环境设置如何,Babel 都有正式的集成支持。

本手册的目的主要是介绍 Babel 内建方式的安装,不过你可以访问交互式的安装页面来查看其它的整合方式。

注意: 本手册将涉及到一些命令行工具如 node 和 npm。在继续阅读之前请确保你已经熟悉这些工具了。

babel-cli

Babel 的 CLI 是一种在命令行下使用 Babel 编译文件的简单方法。

让我们先全局安装它来学习基础知识。

$ npm install --global babel-cli

我们可以这样来编译我们的第一个文件:

$ babel my-file.js

这将把编译后的结果直接输出至终端。使用 --out-file 或着 -o 可以将结果写入到指定的文件。

$ babel example.js --out-file compiled.js
/* 或 */
$ babel example.js -o compiled.js

如果我们想要把一个目录整个编译成一个新的目录,可以使用 --out-dir 或者 -d

$ babel src --out-dir lib
/* 或 */
$ babel src -d lib
在项目内运行babel-cli

尽管你可以把 babel-cli 全局安装在你的机器上,但是按项目逐个安装在本地会更好。

  • 有两个主要的原因:
    1. 在同一台机器上的不同项目或许会依赖不同版本的 Babel 并允许你有选择的更新。
    2. 这意味着你对工作环境没有隐式依赖,这让你的项目有很好的可移植性并且易于安装。

要在(项目)本地安装 Babel CLI 可以运行:

$ npm install --save-dev babel-cli

安装完成后,你的 package.json 应该如下所示:

{
  "name": "my-project",
  "version": "1.0.0",
  "devDependencies": {
    "babel-cli": "^6.0.0"
  }
}

现在,我们不直接从命令行运行 Babel 了,取而代之我们将把运行命令写在 npm scripts 里,这样可以使用 Babel 的本地版本。

只需将 "scripts" 字段添加到你的 package.json 文件内并且把 babel 命令写成 build 字段。

  {
    "name": "my-project",
    "version": "1.0.0",
    "scripts": {
      "build": "babel src -d lib"
    },
    "devDependencies": {
      "babel-cli": "^6.0.0"
    }
  }

现在可以在终端里运行:

npm run build

这将以与之前同样的方式运行 Babel,但这一次我们使用的是本地副本。

babel-node

babel-cli工具自带一个babel-node命令,提供一个支持ES6的REPL环境。
它支持Node的REPL环境的所有功能,而且可以直接运行ES6代码。

它不用单独安装,而是随babel-cli一起安装。然后,执行babel-node就进入PEPL环境。

$ babel-node
> (x => x * 2)(1)
2

babel-node命令可以直接运行ES6脚本。将上面的代码放入脚本文件es6.js,然后直接运行。

$ babel-node es6.js
2

babel-node也可以安装在项目中。

$ npm install --save-dev babel-cli

然后,改写package.json。

{
  "scripts": {
    "script-name": "babel-node script.js"
  }
}

上面代码中,使用babel-node替代node,这样script.js本身就不用做任何转码处理。

babel-register

babel-register模块改写require命令,为它加上一个钩子。
此后,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用Babel进行转码。

首先安装 babel-register:

$ npm install --save-dev babel-register

使用时,必须首先加载babel-register。

require("babel-register");
require("./index.js");

然后,就不需要手动对index.js转码了。

**注意: **babel-register只会对require命令加载的文件转码,而不会对当前文件转码。
另外,由于它是实时转码,所以只适合在开发环境使用。

babel-core

如果某些代码需要调用Babel的API进行转码,就要使用babel-core模块。

安装命令如下。

$ npm install babel-core --save

然后,在项目中就可以调用babel-core。

var babel = require('babel-core');

/* 字符串转码 */
babel.transform('code();', options);
/* => { code, map, ast } */

/* 文件转码(异步)*/
babel.transformFile('filename.js', options, function(err, result) {
  result; /* => { code, map, ast } */
});

/* 文件转码(同步)*/
babel.transformFileSync('filename.js', options);
/* => { code, map, ast } */

/* Babel AST转码 */
babel.transformFromAst(ast, code, options);
/* => { code, map, ast } */

配置对象options,可以参看官方文档http://babeljs.io/docs/usage/options/。

下面是一个例子。

var es6Code = 'let x = n => n + 1';
var es5Code = require('babel-core')
  .transform(es6Code, {
    presets: ['es2015']
  })
  .code;
// '"use strict";\n\nvar x = function x(n) {\n  return n + 1;\n};'

上面代码中,transform方法的第一个参数是一个字符串,表示需要转换的ES6代码,第二个参数是转换的配置对象。

babel-loader

我们大部分情况下,做前端项目是有一套自己的构建、打包过程的,这个过程会对js进行压缩等处理。而这种情况下,我们要用ES6,就可以顺便把babel加入到这个构建过程当中(岂不是更加灵活咯)。而babel也为webpack这类的工具提供了对应的loader。
loader是webpack里的概念哦,有了babel-loaderwebpack就能在打包过程中加入babel的强大编译功能了)

其实babel除了能支持webpack,也能支持JavaScript社区所有的主流构建工具,这里就不做说明了。

babel-loader的使用方法实际上跟你使用命令CLI或者API的方式都是一模一样的。只是这个调用者变成了webpack,webpack执行时其实类似于你通过babel API来转译你的源码。
所以他们之间的关系是: webpack依赖babel-loader, babel-loader依赖babel编译相关的包(如babel-core), 而babel编译又依赖自身或社区一些插件(如preset-env等)。

看下使用babel-loader时,webpack的配置文件:

{
  module: {
    loaders: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
                {
                    loader: 'babel-loader',
                    options: {
                        presets: ['es2015']
                    }
                }
            ]
          }
    ]
  }
}

由于babel有自己的配置文件,所以上面代码中babel-loader中的options配置可以不写,而是放到独立配置文件当中。

配置Babel

上面呢我们已经学习了调用babel的N种方式。可以说,无论哪种方式调用吗,都离不开babel的配置文件的配置(否则babel什么都不做)。
现在我们学习如何配置babel。Babel的配置文件是.babelrc或者babel.config.js(babel7推荐的),存放在项目的根目录下, rc结尾的文件通常代表运行时自动加载的文件、配置。
使用Babel的第一步,就是配置这个文件。

你可以通过安装**插件(plugins)或预设(presets,也就是一组插件)**来指示 Babel 去做什么事情。

.babelrc

在我们告诉 Babel 该做什么之前,我们需要创建一个配置文件。你需要做的就是在项目的根路径下创建 .babelrc 文件。然后输入以下内容作为开始:

{
  "presets": [],
  "plugins": []
}

这个文件就是用来让 Babel 做你要它做的事情的配置文件。

**注意:**尽管你也可以用其他方式给 Babel 传递选项,但 .babelrc 文件是约定也是最好的方式。

plugins插件

babel6以后,babel自身只能完成基本的核心功能。并不去做转换任何语法特性的事情。
比如 transform-es2015-classes 这个插件就可以让babel转译你代码中的class定义的语法。
比如如果在babel6里想用箭头函数,得装上插件:

npm install babel-plugin-transform-es2015-arrow-functions

然后设置babelrc配置文件:

{
  "plugins": ["transform-es2015-arrow-functions"]
}

babel官方内置插件都在babel的官方仓库package目录下(babel-cli代码也在这): 链接
这里就不列举了。

preset预设

但是这么多插件,写起来非常麻烦。总不能让开发者记住所有插件的功能并且去配置上项目所需要的插件吧。这显然不行,所以有了preset预设。一个预设就包含了很多插件咯。
preset预设是一系列plugin插件的集合,配置了该预设,就不需要配置n个插件了,减少了配置的繁琐。
比如使用 preset-es2015 的预设为什么就可以转换class定义这种语法呢,其实就因为 es2015的预设中已经包含了 transform-es2015-classes 这个插件。
官方的预设还是在babel的这个仓库

babel内置的预设如下:

  • env
  • es2015
  • es2016
  • es2017
  • latest (已经废弃,请用preset-env代替)
  • react
  • flow

还有其他一些非官方的预设,可以在npm上进行搜索: https://www.npmjs.com/search?..

????: babel-preset-stage-x

JavaScript 还有一些提案,正在积极通过 TC39(ECMAScript 标准背后的技术委员会)的流程成为标准的一部分。

这个流程分为 5(0-4)个阶段。 随着提案得到越多的关注就越有可能被标准采纳,于是他们就继续通过各个阶段,最终在阶段 4 被标准正式采纳。

以下是4 个不同阶段的(打包的)预设:

  • babel-preset-stage-0
  • babel-preset-stage-1
  • babel-preset-stage-2
  • babel-preset-stage-3
  • babel-preset-stage-4

注意 stage-4 预设是不存在的因为它就是上面的 es2015 预设。

以上每种预设都依赖于紧随的后期阶段预设。
例如,babel-preset-stage-1 依赖 babel-preset-stage-2,后者又依赖 babel-preset-stage-3

使用的时候只需要安装你想要的阶段就可以了:

$ npm install --save-dev babel-preset-stage-2

然后添加进你的 .babelrc 配置文件。

  {
    "presets": [
      "es2015",
      "react",
+     "stage-2"
    ],
    "plugins": []
  }

preset-env预设

有一个预设叫做 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", "ie >= 10"]
      }
    }]
  ]
}

其中 targets字段可以用来指明目标平台和版本等信息。如果是面向node环境,可以指明node环境版本:

"targets": {
  "node": "6.10"
}

可以看到preset-env的options中,最重要的就是这个targets配置。targets中有2个选项,一个叫 node, 一个叫 browsers。

  • node: node这个key后面可以写一个字符串类型的版本号或者"current", 如果想直接面向其babel运行环境的node版本,则可以改写为这样: "node": "current",此时babel会直接取 process.versions.node 中的版本号

  • browsers: 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还有其他一些配置,如:

  • modules 设置为true可以让babel把你的模块编译为 “amd”, ”“umd"或者"commonjs”. 在配合webpack使用的时候,一般由webpack打包,因此一般将babel的这个配置设置为false

  • include, exclude, 可以让babel加载或者去除指定名称的插件。适用于我们要自定义改动preset-env的情况。

  • useBuiltIns. 这个配置用来给preset-env这个智能预设添加polyfill的。因为babel只转换语法,不转换API(下文会讲),所以代码中很多API需要根据你设置的targets环境进行polyfill处理,而在preset-env中能根据配置的环境进行智能添加polyfill的过程,就需要useBuiltIns的支持。 这也是在开发web应用(非类库时)使用preset-env时的polyfill最佳实践,下文会讲。

复杂语法转换和babel-polyfill

babel只转换语法,不转换API。babel在语言转换方面,只转换各种ES新语法以及jsx等,但不转换ES6提供的新的API功能,例如Promise、Array的新增的原型、静态方法等。这时就需要polyfill垫片。

我们可以分析下,对于ES6转换为ES5这件事情来说。有几种需要做不同实现的转换类型呢?

大概是这样的:

  1. 一种是仅仅是语法糖的区别,比如箭头函数能直接转为ES5的function;
  2. 一种是API方法或类的。比如Array.from是ES6新加的API,ES5没有这方法。babel要想提供只能提前给实现这个方法。
  3. 一种是既是新语法,但ES5也没有能直接对应的语法. babel要想实现这个,就既要做语法变换,又要提前提供一些辅助函数。比如 class类声明以及async这些,你不能简单的转换成一个 ES5 的映射,你需要一些辅助函数配合。

babel是怎么处理这些情况的呢?

  1. 对于第一种,babel是通过上文讲到的插件直接进行代码翻译即可,很容易理解,也很简单; 这是上文讲到的babel+presetEnv预设所完成的。
  2. 对于第二种情况,为了解决这个问题,babel使用一种叫做 Polyfill(代码填充,也可译作兼容性补丁) 的技术。 简单地说,polyfill 即是在当前运行环境中用来复制(意指模拟性的复制,而不是拷贝)尚不存在的原生 api 的代码。 能让你提前使用还不可用的 APIs,Array.from 就是一个例子。Babel 用了优秀的 core-js 用作 polyfill。
  3. 对于第三种情况,babel采用的方法是:编译你代码的过程中如果发现了这种语法,就会把你的语法包装成另一种ES5实现的语法,但是由于实现比较复杂,所以除了对语法进行转换之外,还需要辅助函数的配合,因此你会发现有运行时函数插入到代码的最上方。

我们来看一段代码:

/* 原型方法 */
[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);
});

分析转换结果,我们可以看到:

  1. 对于普通的ES语法如箭头函数,babel会通过你preset-env指定的插件完成语法转换。把箭头函数转为ES5的函数写法
  2. 对于ES5中没有的原型方法和静态方法,babel自身在进行语法转换时,并不关注这一点。这个需要交给polyfill垫片js库来完成。
  3. 对于复杂的语法如class/async,babel的preset-env里面包含了对这类语法的转换。但是这类语法由于其比较复杂,所以会产生辅助函数,而且这些辅助函数的实现代码会注入到转译后的文件里。 如class的实现需要_createClass和classCallback函数,这两个函数就注入到了编译结果代码里。

由此可见,由于默认的preset-env配置是转换所有的ES6语法,所以我们的箭头函数、async、class都被启用了相应的插件进行转换,并且转换成功了。 现在伤脑筋的问题有两个:

  1. 原型方法、静态方法等都无法转换,包括但不限于Array.from Object.assign Array.prototype.includes. 那么,我们上文也说了,这种活应该交给polyfill(比如在页面里引入一个shim.js),那么babel有没有提供相应的polyfill办法呢? 答案是有的,它就是 babel-polyfill.

  2. 复杂的ES语法,经过转换后会生成一坨函数实现代码在文件里。如果只有一个文件/模块还好,如果有 a.js, b.js, c.js 等多个模块文件,babel编译后每个js文件里都有一堆重复的 _createClass函数实现;如果再未来用webpack对他们打包上线,则会导致打包里面每个模块里也包含重复的_createClass函数实现(因为这个函数在每个js文件里相当于是个私有函数)

怎么办呢? 下面我们来分别分析一下这俩伤脑筋的问题如何解决。

如何解决语法API无法被转换的问题

babel自身只转换语法,不负责hack语法的API。这个一般用polyfill代码实现。其实用一个polyfill垫片库最简单的方式就是全量引入了。

要使用 Babel polyfill,首先用 npm 安装它:

$ npm install --save babel-polyfill /* 要作为运行依赖哦,因为polyfill要最终交给浏览器执行 */
$ npm install @babel/polyfill /* babel7 版本的安装方式 */

然后只需要在入口文件最顶部导入 polyfill 就可以了: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的话就会发现,这个包其实就是依靠 core-jsregenerator-runtime 实现了所有的shim/polyfill。所以在babel-polyfill这个npm包里面,只有一个index.js文件,里面直接引用了这两个npm库而已。

babel-polyfill引用了core-js/shim.js, 其实shim.js这个文件就是把core-js包里的所有polyfill的API暴漏出来。

虽然polyfill的使用很简单,甚至跟babel都没有多少关系。可是现在问题来了:

  1. 如果你的代码是要支持chrome的某个较新版本即可,由于chrome已经支持了大部分的ES6能力,可能你只需polyfill该版本chrome尚不支持的少量API即可;结果却引入了一个庞然大物babel-polyfill。能不能根据目标平台的支持情况来精简polyfill呢?
  2. 你的业务代码中可能仅仅使用了一个Object.assign和Promise,结果却要引入一个庞然大物 babel-polyfill。能不能根据代码中用到的API来精简polyfill引入呢?
  3. 尽管polyfill的目的就是能全局hack API,但是有些时候比如你开发的是一个类库。你可能仅仅希望局部去hack一下你用到的这个API就好了,不要影响外部环境。能不能只在局部hack我的Array.from呢?

优化是无止境的,让我们看看怎么解决上面问题呢?

1. 根据目标平台的支持情况引入需要的polyfill

恭喜,这个能力已经被 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。如果设置为 entry,则只加载目标平台不支持的那些特性的polyfill,而不会根据代码使用情况来加载(这在性能上要快一些)。不过,useBuiltIns: 'entry' 是替换import "@babel/polyfill" / require("@babel/polyfill") 语句为独立的(根据环境)需要引入的polyfill特性的import语句,因此你必须在源码中显式声明 imoprt 'babel-polyfill'

2. 根据代码中用到的API来加载polyfill

通过上面的 useBuiltIns 案例我们已经发现,preset-env 开启了usageuseBuiltIns之后,它既能够根据目标平台来选择性的引入polyfill,而且它引入的polyfill是你业务代码中用到的,并不会把所有平台不支持的polyfill都引入。

这一点在 @babel/[email protected] 版本我验证是 OK 的, 在之前的版本中我曾经测试发现preset-env不能实现按需引入。 应该是在7.0版本修复了这个问题。

3.局部hack

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-envtransform-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-transform-runtime // babel7的安装方式
npm install --save @babel/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, /* 只能设置为 undefined,false,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应用 我们一般是希望:

  1. ES6复杂语法转换可以不重复打包。(用transform-runtime配合babel-runtime来实现)
  2. polyfill能够按需引入并全局polyfill(用preset-env配合开启useBuiltIns实现)

因此这种场景下正确的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

  • helper是为了帮助你构造ES6的ES5 class实现和generator实现的辅助函数

  • 垫片是为了实现ES6的ES5版本的API,如Array.from. core-js这个垫片函数库是对某个API的具体实现。

  • regenerator-runtime也是一些垫片函数,只是它特定地用来实现ES6里面的generaotor语法。

  • 垫片库是指的一个调用了各个垫片函数再对页面全局进行污染的垫片库,如babel-polyfill

如此,我们就能明白babel-polyfill只是为实现API垫片为目的的一个库,可以全局污染来垫片。它包含了core-jsregeneraor-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插件来完成。

其他

关于async语法

async generator转换成新的辅助函数后,到底需要依赖哪些东西才能正常运行?

经过我的测试发现,它需要依赖两个polyfill:

  • regenerator-runtime这个polyfill(因为helpers辅助函数是不够的)。
  • promise的polyfill(因为转译后的代码中用到了promise)

我们进行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)里面的。

babel与mocha和lint结合使用

babel不止于ES

现在流行框架,都在使用babel进行框架特有的语法转换。例如除了react,还有Vue2.0的jsx


babel搞了将近两天的时间,后续还会补充与修正^_^