阅读 245

babel polyfill runtime 浅析

前言

babel只能转义ES6语法,比如箭头函数,但是遇到ES6新增的api就无能为力了,比如Promise和includes。对于这些新增的api,需要polyfill去做兼容。babel提供了两个plugin来处理这些api的polyfill。

@babel/preset-env

如果使用preset-env来处理polyfill,需要安装@babel/polyfill。

@babel/preset-env通过配置项,能够智能的帮你处理ES6的语法。它通过读取项目中的browserslist配置,来决定需要对哪些语法进行处理。一个配置的例子

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
           "chrome": "58",
           "ie": "11"
        }
        "useBuiltIns": "entry",
        "modules": false,
        
      }
    ]
  ]
}
复制代码

options.targets

用来配置需要支持的的环境,不仅支持浏览器,还支持node。

如果没有配置targets选项,就会读取项目中的browserslist配置项。

options.loose

默认值是false,如果preset-env中包含的plugin支持loose的设置,那么可以通过这个字段来做统一的设置。

options.modules

"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默认值是auto

用来转换ES6的模块语法。如果使用false,将不会对文件的模块语法进行转化。

如果要使用webpack中的一些新特性,比如tree shaking 和 sideEffects,就需要设置为false,对ES6的模块文件不做转化,因为这些特性只对ES6的模块有效。

options.useBuiltIns

"usage" | "entry" | false,默认值是false

这个配置项主要是用来处理@babel/polyfill。

  • 设置为false时,会把@babel/polyfill这个包引进来,忽略targets配置项和项目里的browserslist配置
  • 设置为entry时,在整个项目里,只需要引入一次@babel/polyfill,它会根据targets和browserslist,然后只加载目标环境不支持的api文件
  • 设置为usage时,babel会在目标环境的基础上,只加载项目里用的那些不支持的api的文件,做到按需加载

其他的一些配置项可以看 官方文档

@babel/plugin-transform-runtime

这个插件可以通过一些helper函数的注入来减少语法转换函数的开销。

const c = {...b};
复制代码

通过babel编译以后,对象的解构语法会被编译为下面的文件

var _extends = Object.assign || function (target) { 
  for (var i = 1; i < arguments.length; i++) { 
    var source = arguments[i]; 
    for (var key in source) { 
      if (Object.prototype.hasOwnProperty.call(source, key)) {
        target[key] = source[key];   
      }
    }
  } 
  return target;
};

var c = _extends({}, b);
复制代码

如果很多文件都用到了解构语法,那么每个文件都会生成一个相同的_extends方法,@babel/plugin-transform-runtime会使用helper函数来处理对应的语法,避免这种重复定义方法的问题,使用这个插件后,会生成下面的文件

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));

var c = (0, _objectSpread2.default)({}, b);
复制代码

对插件进行配置

{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": false,
        "helpers": true,
        "regenerator": true,
        "useESModules": false
      }
    ]
  ]
}
复制代码

babel7相比于babel6,删除了polyfill和useBuiltIns这两个配置。

options.corejs

boolean or number,默认值是false

e.g. ['@babel/plugin-transform-runtime', { corejs: 2 }]

当设置为false时,只对语法进行转换,不对api进行处理

当设置为2的时候,需要安装@babel/runtime-corejs2,这时会对api进行处理。这里需要注意,不能polyfill Array.includes这种需要重写property的api,这种会污染全局变量,需要在项目里使用@babel/polyfill来处理。

options.helpers

默认值是true,用来开启是否使用helper函数来重写语法转换的函数。

options.useESModules

默认值是false,是否对文件使用ES的模块语法,使用ES的模块语法可以减少文件的大小。

// useESModules:false

exports.__esModule = true;

exports.default = function(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
};
复制代码
// useESModules:true

export default function(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}
复制代码

@babel/plugin-transform-runtime默认情况下安装@babel/runtime这个库,即corejs为false时使用;当corejs设置为2时,需要安装使用@babel/runtime-corejs2。runtime和runtime-corejs2这两个库唯一的区别是,corejs2这个库增加了对core-js这个库的依赖,而core-js是用来对ES6各个语法polyfill的库,所以在corejs为false的情况下,只能做语法的转换,并不能polyfill任何api。

@babel/polyfill && @babbel/runtime

polyfill和runtime都可以用来对api进行垫片处理,但是两者还有一定的不同。使用polyfill的时候,会污染全局变量;而runtime的时候,会使用局部变量来处理,不会污染全局变量。这也是为什么runtime无法处理原型上的api的原因,因为要模拟这些api,必须要污染全局变量。

core-js

@babel/polyfill和@babel/runtime-corejs2都使用了core-js(v2)这个库来进行api的处理。

这个库有两个核心的文件夹,分别是library和modules。runtime使用library这个文件夹,polyfill使用modules这个文件夹。

  • library使用helper的方式,局部实现某个api,不会污染全局变量
  • modules以污染全局变量的方法来实现api

library和modules包含的文件基本相同,最大的不同是_export.js这个文件。

core-js/modules/_exports.js文件如下

var global = require('./_global');
var core = require('./_core');
var hide = require('./_hide');
var redefine = require('./_redefine');
var ctx = require('./_ctx');
var PROTOTYPE = 'prototype';

var $export = function (type, name, source) {
  var IS_FORCED = type & $export.F;
  var IS_GLOBAL = type & $export.G;
  var IS_STATIC = type & $export.S;
  var IS_PROTO = type & $export.P;
  var IS_BIND = type & $export.B;
  var target = IS_GLOBAL ? global : IS_STATIC ? global[name] || (global[name] = {}) : (global[name] || {})[PROTOTYPE];
  var exports = IS_GLOBAL ? core : core[name] || (core[name] = {});
  var expProto = exports[PROTOTYPE] || (exports[PROTOTYPE] = {});
  var key, own, out, exp;
  if (IS_GLOBAL) source = name;
  for (key in source) {
    // contains in native
    own = !IS_FORCED && target && target[key] !== undefined;
    // export native or passed
    out = (own ? target : source)[key];
    // bind timers to global for call from export context
    exp = IS_BIND && own ? ctx(out, global) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out;
    // extend global
    if (target) redefine(target, key, out, type & $export.U);
    // export
    if (exports[key] != out) hide(exports, key, exp);
    if (IS_PROTO && expProto[key] != out) expProto[key] = out;
  }
};
global.core = core;
// type bitmap
$export.F = 1;   // forced
$export.G = 2;   // global
$export.S = 4;   // static
$export.P = 8;   // proto
$export.B = 16;  // bind
$export.W = 32;  // wrap
$export.U = 64;  // safe
$export.R = 128; // real proto method for `library`
module.exports = $export;
复制代码

core-js/library/_exports.js文件如下

var global = require('./_global');
var core = require('./_core');
var ctx = require('./_ctx');
var hide = require('./_hide');
var has = require('./_has');
var PROTOTYPE = 'prototype';

var $export = function (type, name, source) {
  var IS_FORCED = type & $export.F;
  var IS_GLOBAL = type & $export.G;
  var IS_STATIC = type & $export.S;
  var IS_PROTO = type & $export.P;
  var IS_BIND = type & $export.B;
  var IS_WRAP = type & $export.W;
  var exports = IS_GLOBAL ? core : core[name] || (core[name] = {});
  var expProto = exports[PROTOTYPE];
  var target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {})[PROTOTYPE];
  var key, own, out;
  if (IS_GLOBAL) source = name;
  for (key in source) {
    // contains in native
    own = !IS_FORCED && target && target[key] !== undefined;
    if (own && has(exports, key)) continue;
    // export native or passed
    out = own ? target[key] : source[key];
    // prevent global pollution for namespaces
    exports[key] = IS_GLOBAL && typeof target[key] != 'function' ? source[key]
    // bind timers to global for call from export context
    : IS_BIND && own ? ctx(out, global)
    // wrap global constructors for prevent change them in library
    : IS_WRAP && target[key] == out ? (function (C) {
      var F = function (a, b, c) {
        if (this instanceof C) {
          switch (arguments.length) {
            case 0: return new C();
            case 1: return new C(a);
            case 2: return new C(a, b);
          } return new C(a, b, c);
        } return C.apply(this, arguments);
      };
      F[PROTOTYPE] = C[PROTOTYPE];
      return F;
    // make static versions for prototype methods
    })(out) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out;
    // export proto methods to core.%CONSTRUCTOR%.methods.%NAME%
    if (IS_PROTO) {
      (exports.virtual || (exports.virtual = {}))[key] = out;
      // export proto methods to core.%CONSTRUCTOR%.prototype.%NAME%
      if (type & $export.R && expProto && !expProto[key]) hide(expProto, key, out);
    }
  }
};
// type bitmap
$export.F = 1;   // forced
$export.G = 2;   // global
$export.S = 4;   // static
$export.P = 8;   // proto
$export.B = 16;  // bind
$export.W = 32;  // wrap
$export.U = 64;  // safe
$export.R = 128; // real proto method for `library`
module.exports = $export;

复制代码

可以看出,library下的这个$export方法,会实现一个wrapper函数,防止污染全局变量。

var p = new Promise();


// @babel/polyfill
require("core-js/modules/es6.promise");
var p = new Promise();


// @babel/runtime-corejs2
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));
var a = new _promise.default();

复制代码

从上面这个例子可以看出,对于Promise这个api,@babel/polyfill引用了core-js/modules中的es6.promise.js文件,因为是对全局变量进行处理,所以赋值语句不用做处理;@babel/runtime-corejs2会生成一个局部变量_promise,然后把Promise都替换成_promise,这样就不会污染全局变量了。

项目和库开发

在项目开发的时候,使用@babel/polyfill;在开发库的时候,使用runtime。

如果在库开发的时候,用到了一些新的api,可以选择不要处理,把选择权留给开发者,让开发者在项目中使用@babel/polyfill去处理。

因为在项目中都会排除node_modules里面的js文件,加快项目的编译速度。出于这个考虑,不建议使用把env的useBuiltIns设置为usage,这样可能会导致node_modules里面的库使用了某些需要polyfill的api,但是我们又没有引入对应的polyfill文件,在某些环境会出现bug。

在项目里,建议使用entry这个配置,并配合browserslist,对于某些已经被目标环境支持的api不用引入对应的polyfill文件,减少项目的文件体积。


关注下面的标签,发现更多相似文章
评论