从零搭建webpack前端类库脚手架[3]-强悍的babel

上一节咱们提到了ES6语法转换插件 babel-loader, 然而babel-loader只是webpack调用 babel的一个桥梁。 实际上,babel是一个具备强大语言转换功能的独立程序。它的主要功能是把ES6或者更新的语言语法转换为浏览器可识别的ES5语法。javascript

了解ES6

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

babel

为了提早使用更新的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

我对shim、polyfill和transpile的理解

上面咱们讲了,babel是一种语言转换的技术。那么其实要想在浏览器里运行更新的语言语法,须要解决2个问题。git

  • 一个是新的语法如箭头函数怎么解析
  • 一个是之前没有的类或方法该怎么hack(伪造),如Promise。

对于这种新增的类、方法,咱们很容易想到能够在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使用方法相似下面这样:

standalone

因为babel要在浏览器的运行时对你的js代码从新编译一遍执行,性能必然有所下降,所以不适合线上运行的生产环境站点。不过咱们后面会讲到如何使用babel对代码进行预编译,这样最终运行在浏览器中的代码就是ES5了,就不存在性能问题了。因此正由于有了前端编译的过程,如今babel这种transform才流行起来。

babel编译代码的几种方式

在前端项目,咱们的目的每每是利用babel提早把ES6代码转成ES5代码而后放到浏览器执行,而不是为了当即执行他,这里就要对用babel对代码进行编译成es5的源码,而后再交给浏览器或node平台去执行。下面咱们看下几种不一样的babel使用方式。

直接执行es6代码

在后端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 和 语法转换之间的情感纠葛,咱们后文再讲)

babel-register

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命令

通常外网要上线的代码,都不会用 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;
});

箭头函数已经被编译成了普通函数。

API调用babel实现源码编译

若是想在代码中调用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-....

经过babel-loader调用babel

咱们大部分状况下,作前端项目是有一套本身的构建、打包过程的,这个过程会对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配置能够不写,而是放到独立配置文件当中。

babel配置详解

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预设。 一个预设就包含了不少插件咯。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?...

其中,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": []
  }

但安装该预设时,须要使用完整的预设名称。

着重介绍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", "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还有其余一些配置,如:

  • 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没法被转换的问题

【备注:此小节是3级标题】

babel自身只转换语法,不负责hack语法的API。这个通常用polyfill代码实现。其实用一个polyfill垫片库最简单的方式就是全量引入了。若是你是但愿在执行代码的页面里进行垫片,则在页面中引入babel-polyfill的页面版本便可:

使用 babel-polyfill/dist/polyfill.js

若是但愿在预编译阶段引入到业务代码中,你能够 require 到业务代码的开头;将来打包到bundle.js的时候就能加载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 能够垫片的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

【备注,此小节已是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'

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

经过上面的 useBuiltIns 案例咱们已经发行,preset-env 开启了useage的 useBuiltIns以后,它既可以根据目标平台来选择性的引入polyfill,并且它引入的polyfill是你业务代码中用到的,并不会把全部平台不支持的polyfill都引入。

这一点在 @babel/preset-env@7.0 版本我验证是 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-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应用 咱们通常是但愿:

  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-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。

Web应用项目

这种项目因为不怕全局polyfill污染,所以通常采用全局polyfill的方式。不过为了提升页面性能,通常也经过 preset-env 配合 useBuiltIns配置的方式实现按需加载polyfill。注意,如今版本的preset-env若是开启了useBuildIns,你就不要本身在代码的开头出引用babel-polyfill了。

至于复杂语法转换带来的辅助函数问题,就靠 transform-runtime来解决了。注意不要开启 core-js选项,从而避免局部polyfill(由于你已经preset-env+useBuiltIns使用了全局polyfill的方式)。

关于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); }); }; }

参考issue:
5085
112

咱们再试试用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结合使用

用ES6写代码以后,测试有时也但愿使用ES6来编写。并且eslint进行代码检查时也要利用babel进行转换。关于结合mocha的使用将在后面的文章讲解。eslint的使用请参看博文[[实践]-使用ESLINT检查代码规范]()

总之,测试这些环节执行ES6的测试用例代码时就不需走编译步骤了。因为不在意性能,所以能够直接走实时编译执行的模式。

mocha --compilers js:babel-core/register --require babel-polyfill

babel不止于ES

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

咱们也能够写本身的babel插件,详情可参考手册: https://github.com/thejamesky...
官方脚手架:https://github.com/babel/gene...

下一节,就用这些知识点真正搭建一个类库开发项目了。

Refer

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 与 相关插件和包

相关文章
相关标签/搜索