【记】深入理解Babel、polyfill、runtime和core-js

Babel解决的痛点主要有两方面: 一.是支持我们提前编写较新的es语法,Babel来负责转译为运行环境支持的语法。二.是新标准引入的新的原生对象,部分原生对象新增的原型方法,新增的API等(如Proxy、Set等)。这些Babel不转译,需要用户自行引入polyfill(打补丁)来解决,Babel对此也提供了一些封装和支持如babel-polyfill,babel-runtime。

Ⅰ.Babel转译功能

babel是一个转译器,感觉相对于编译器compiler,叫转译器transpiler更准确,因为它只是把同种语言的高版本规则翻译成低版本规则,而不像编译器那样,输出的是另一种更低级的语言代码。但是和编译器类似,babel的转译过程也分为三个阶段:parsing、transforming、generating,以ES6代码转译为ES5代码为例,babel转译的具体过程如下:

//parsing
ES6代码输入 ==》 babylon进行解析 ==》 得到AST
//transforming
==》 plugin用babel-traverse对AST树进行遍历转译 ==》 得到新的AST树
//generating
==》 用babel-generator通过AST树生成ES5代码

插件应用于babel的转译过程,尤其是第二个阶段transforming,如果这个阶段不使用任何插件,那么babel会原样输出代码。我们主要关注transforming阶段使用的插件,因为transform插件会自动使用对应的词法插件,所以parsing阶段的插件不需要配置。transforming过程中用到各种插件(plugins),babel都提供了封装。

如果要自行配置转译过程中使用的各类插件,那太痛苦了,所以babel官方帮我们做了一些预设的插件集,称之为preset,这样我们只需要使用对应的preset就可以了。以JS标准为例,babel提供了如下的一些preset:

es2015,es2016,es2017...env。es20xx的preset只转译该年份批准的标准,而env则代指最新的标准,包括了latest和es20xx各年份另外,还有 stage-0到stage-4的标准成形之前的各个阶段,这些都是实验版的preset,建议不要使用。

Ⅱ.polyfill(打补丁)及babel对polyfill的封装

babel对polyfill的封装(babel-polyfill)包只是简单的把core-js和regenerator runtime包装了下,这两个包才是真正的实现代码所在(后文会详细介绍core-js)。使用babel-polyfill会把ES2015+环境整体引入到你的代码环境中,让你的代码可以直接使用新标准所引入的新原生对象,新API等,一般来说单独的应用和页面都可以这样使用。

import "babel-polyfill"
//当然这是babel提供的一种全量打补丁的方式.下面我们也介绍一些其它的打补丁方式和打补丁场景,包括babel也有对应的插件支持。
一.实现方式

polyfill(打补丁)是一个针对ES2015+环境的shim,实现方式有三种:1.手动打补丁 2.根据覆盖率自动打补丁 3.根据浏览器特性,动态打补丁.

//1.手动方式如对ie下,Object.assign的支持补丁
Object.assign = require('object-assign')
// or
// Refer: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
if (typeof Object.assign != 'function') {
  // Must be writable: true, enumerable: false, configurable: true
  Object.defineProperty(Object, "assign", {
    value: function assign(target, varArgs) { // .length of function is 2
      'use strict';
      if (target == null) { // TypeError if undefined or null
        throw new TypeError('Cannot convert undefined or null to object');
      }
      var to = Object(target);
      for (var index = 1; index < arguments.length; index++) {
        var nextSource = arguments[index];
        if (nextSource != null) { // Skip over if undefined or null
          for (var nextKey in nextSource) {
            // Avoid bugs when hasOwnProperty is shadowed
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey];
            }
          }
        }
      }
      return to;
    },
    writable: true,
    configurable: true
  });
}

问题是解决了,但优势和劣势也相当明显:优势是保持最小化引入,不会有额外的冗余代码开销,保证了应用的性能。劣势是手动导入不易管理和维护,对于多样化的 polyfill 和变化多端的 Web 应用维护成本比较大。

//2.根据覆盖率自动打补丁,在node,webpack的加持下,我们可以更现代化的方式打补丁。
presets: [
       [
         '@babel/preset-env',
         {
           // 支持chrome 58+ 及 IE 11+
           targets: {
             chrome: '58',
             ie: '11',
           }
         },
       ],
]

以上两种方法都有一个弊端——补丁的冗余。以 Object#assign 来说,在支持这个特性的浏览器来说,就没必要引入这个补丁,势必造成了一定的补丁冗余,这就有了根据浏览器特性动态打补丁的方案。Polyfill.io 就是实现这个方案的服务,它会根据浏览器的UA不同,返回不一样的补丁。如想要 Promise 补丁,在页面引入:

//3.动态打补丁
<script src="https://polyfill.io/v3/polyfill.js?features=Promise"></script>
//如在高版本的浏览器(Chrome 75)上,打开链接会返回空页面:
/* Polyfill service v3.34.0
 * For detailed credits and licence information see https://github.com/financial-times/polyfill-service.
 * 
 * Features requested: Promise
 *  */
/* No polyfills found for current settings */
如果将浏览器的UA改为IE 11,将会返回相应的polyfill代码
二.应用场景

polyfill(打补丁)在应用场景方面,分为两个场景. 1.给应用的代码打补丁 2.给类库代码打补丁

对于应用或页面等环境在你控制之中的情况来说,并没有什么问题。但是对于在类库中使用polyfill,就变得不可行了。因为类库是供外部使用的,但外部的环境并不在类库的可控范围,而polyfill是会污染原来的全局环境的(因为新的原生对象、API这些都直接由polyfill引入到全局环境)。这样就很容易会发生冲突。

此时就出现了runtime的概念,可以理解为只对运行的地方进行polyfill(打补丁),不影响全局,如下:

// 输入的ES6代码
var sym = Symbol();
// 通过transform-runtime转换后的ES5+runtime代码 
var _symbol = require("babel-runtime/core-js/symbol");
var sym = (0, _symbol.default)();

从上面这个例子可见,原本代码中使用的ES6新原生对象Symbol被transform-runtimec插件转换成了babel-runtime的实现,既保持了Symbol的功能,同时又没有像polyfill那样污染全局环境(因为最终生成的代码中,并没有对Symbol的引用).另外,这里我们也可以隐约发现,babel-runtime其实也不是真正的实现代码所在,真正的代码实现是在core-js中.

babel-plugin-transform-runtime是babel提供的最外层的,runtime打补丁插件,其功能主要为:

1.把代码中的使用到的ES6引入的新原生对象和静态方法用babel-runtime/core-js导出的对象和方法替代
2.当使用generators或async函数时,用babel-runtime/regenerator导出的函数取代(类似polyfill分成regenerator和core-js两个部分)
3.把Babel生成的辅助函数改为用babel-runtime/helpers导出的函数来替代(babel默认会在每个文件顶部放置所需要的辅助函数,如果文件多的话,这些辅助函数就在每个文件中都重复了,通过引用babel-runtime/helpers就可以统一起来,减少代码体积)

上述三点就是transform-runtime插件所做的事情,由此也可见,babel-runtime就是一个提供了regenerator、core-js和helpers的运行时库。 建议不要直接使用babel-runtime,因为transform-runtime依赖babel-runtime,大部分情况下都可以用transform-runtime达成目的。 此外,transform-runtime在.babelrc里配置的时候,还可以设置helpers、polyfill、regenerator这三个开关,以自行决定runtime是否要引入对应的功能。 最后补充一点:由于runtime不会污染全局空间,所以实例方法是无法工作的(因为这必须在原型链上添加这个方法,这是和polyfill最大的不同)

三.底层核心库core-js/regenerator

core-js包才上述的polyfill、runtime的核心,因为polyfill和runtime其实都只是对core-js和regenerator的再封装,方便使用而已。但是polyfill和runtime都是整体引入的,不能做细粒度的调整,如果我们的代码只是用到了小部分ES6而导致需要使用polyfill和runtime的话,会造成代码体积不必要的增大(runtime的影响较小)。所以,按需引入的需求就自然而然产生了,这个时候就得依靠core-js来实现了。

core-js的结构是高度模块化的,它把每个特性都组织到一个小模块里,然后再把这些小模块组合成一个大特性,层层组织。比如:core-js/es6(core-js/library/es6)就包含了全部的ES6特性,而core-js/es6/array(core-js/library/es6/array)则只包含ES6的Array特性,而core-js/fn/array/from(core-js/library/fn/array/from)则只有Array.from这个实现。

我们自己实现对core-js的按需使用,babel-polyfill,babel-runtime其实也是对此的封装:

//类似polyfill,直接把特性添加到全局环境,这种方式体验最完整
require('core-js/fn/set');
require('core-js/fn/array/from');
require('core-js/fn/array/find-index');
Array.from(new Set([1, 2, 3, 2, 1])); // => [1, 2, 3]
[1, 2, NaN, 3, 4].findIndex(isNaN);   // => 2
//类似runtime一样,以库的形式来使用特性,这种方式不会污染全局名字空间,但是不能使用实例方法
var Set       = require('core-js/library/fn/set');
var from      = require('core-js/library/fn/array/from');
var findIndex = require('core-js/library/fn/array/find-index');
from(new Set([1, 2, 3, 2, 1]));      // => [1, 2, 3]
findIndex([1, 2, NaN, 3, 4], isNaN); // => 2
//因为第二种库的形式不能使用prototype方法,所以第三种方式使用了一个小技巧,通过::这个符号而不是.来调用实例方式,从而达到曲线救国的目的。这种方式的使用,路径中都会带有/virtual/
import {fill, findIndex} from 'core-js/library/fn/array/virtual';
Array(10)::fill(0).map((a, b) => b * b)::findIndex(it => it && !(it % 8)); // => 4
// 对比下polyfill的实现 
// Array(10).fill(0).map((a, b) => b * b).findIndex(it => it && !(it % 8));

Babel使用的难点主要在于理解polyfill、runtime和core-js,由上述的打补丁方式和场景,可以总结出Babel具体做了些什么.

Ⅲ.Babel的包构成及配置

理解了上述的Babel转译和polyfill,再看babel涉及的各种包,就很容易理解了.

一.核心包
babel-core:babel转译器本身,提供了babel的转译API,如babel.transform等,用于对代码进行转译。像webpack的babel-loader就是调用这些API来完成转译过程的。
babylon:js的词法解析器
babel-traverse:用于对AST(抽象语法树,想了解的请自行查询编译原理)的遍历,主要给plugin用
babel-generator:根据AST生成代码
二.功能包
babel-types:用于检验、构建和改变AST树的节点
babel-template:辅助函数,用于从字符串形式的代码来构建AST树节点
babel-helpers:一系列预制的babel-template函数,用于提供给一些plugins使用
babel-code-frames:用于生成错误信息,打印出错误点源代码帧以及指出出错位置
babel-plugin-xxx:babel转译过程中使用到的插件,其中babel-plugin-transform-xxx是transform步骤使用的
babel-preset-xxx:transform阶段使用到的一系列的plugin
babel-polyfill:JS标准新增的原生对象和API的shim,实现上仅仅是core-js和regenerator-runtime两个包的封装
babel-runtime:功能类似babel-polyfill,一般用于library或plugin中,因为它不会污染全局作用域
三.工具包
babel-cli:babel的命令行工具,通过命令行对js代码进行转译
babel-register:通过绑定node.js的require来自动转译require引用的js代码文件
四.babel的配置

如果是以命令行方式使用babel,那么babel的设置就以命令行参数的形式带过去;还可以在package.json里在babel字段添加设置;但是建议还是使用一个单独的.babelrc文件,把babel的设置都放置在这里,所有babel API的options(除了回调函数之外)都能够支持.

//常用options字段说明
env:指定在不同环境下使用的配置。比如production和development两个环境使用不同的配置,就可以通过这个字段来配置。env字段的从process.env.BABEL_ENV获取,如果BABEL_ENV不存在,则从process.env.NODE_ENV获取,如果NODE_ENV还是不存在,则取默认值"development"
plugins:要加载和使用的插件列表,插件名前的babel-plugin-可省略;plugin列表按从头到尾的顺序运行
presets:要加载和使用的preset列表,preset名前的babel-preset-可省略;presets列表的preset按从尾到头的逆序运行(为了兼容用户使用习惯)
同时设置了presets和plugins,那么plugins的先运行;每个preset和plugin都可以再配置自己的option

babel会从当前转译的文件所在目录下查找配置文件,如果没有找到,就顺着文档目录树一层层往上查找,一直到.babelrc文件存在或者带babel字段的package.json文件存在为止。

注:部分内容转载自https://www.cnblogs.com/kaicy/p/14696368.html https://zhuanlan.zhihu.com/p/71640183

 欢迎转载:转载时请注明本文出处及文章链接

标签:


仅有 1 条评论

  1. 我真的可以评论么? 我真的可以评论么?

    我真的可以评论么?

添加新评论

captcha
请输入验证码