Skip to content

带你在 Babel 的世界中畅游

polyfill 相关内容

假设有如下源代码:

js
// import "core-js/stable"

const foo = '1';
const bar = () => {
  console.log('我是箭头函数')
}

let a = 2
a = 4

Promise.resolve().then(() => {
  console.log('hello, promise')
})

class Person {
}

function * abc() {}

usage 或 entry

使用 @babel/preset-env 预设,并设置选项 useBuiltIns 为 usage 或 entry,然后转换该源代码。

js
const fs = require('fs')
const core = require('@babel/core')

const res = core.transformFileSync('./code.js', {
  presets: [
    ['@babel/preset-env', {
      // 默认为 false,表示仅会转换基础语法,不会垫片 polyfill。
      // 当为 usage 时,会根据 targets 或 browserlist 和使用的代码,
      // 按需为源代码添加 polyfill。targets 如果不指定,默认兼容到最老的浏览器。
      useBuiltIns: 'usage',
      // 设置为 entry 时,入口需要导入 `import "core-js/stable"`
      // 'useBuiltIns': 'entry',
      // corejs
      // 默认为 2。因为版本升级的原因,这里必须设置为 3,
      // 否则执行转换后的文件提示抱不到对应依赖。
      corejs: 3,
      targets: undefined
    }]
  ],
  // preset 可能会在模块顶层生成一些辅助函数的定义声明,
  // 这些函数可能在多个模块里重复定义。
  // babel 的插件先与预设执行。
  plugins: [
    ['@babel/plugin-transform-runtime', {
      "absoluteRuntime": false,
      "corejs": false,
      "helpers": true,
      "regenerator": true,
      "version": "7.0.0-beta.0"
    }]
  ]
})

const { code } = res

const output = './code-compiled.js'

if(fs.existsSync(output)) {
  fs.rmSync(output, { recursive: true, force: true })
}
fs.writeFileSync(output, code)

会发现转换后的代码引入了 polyfill。

js
"use strict";

var _regeneratorRuntime2 = require("@babel/runtime/regenerator");
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
var _marked = /*#__PURE__*/_regeneratorRuntime2.mark(abc);
// import "core-js/stable"

var foo = '1';
var bar = function bar() {
  console.log('我是箭头函数');
};
var a = 2;
a = 4;
Promise.resolve().then(function () {
  console.log('hello, promise');
});
var Person = /*#__PURE__*/(0, _createClass2["default"])(function Person() {
  (0, _classCallCheck2["default"])(this, Person);
});
function abc() {
  return _regenerator["default"].wrap(function abc$(_context) {
    while (1) switch (_context.prev = _context.next) {
      case 0:
      case "end":
        return _context.stop();
    }
  }, _marked);
}

引入方式为 require('xxx'),并没有导入变量名。

这种方式本质上是往全局对象/内置对象上挂载属性,会造成全局污染。

设置 useBuintIns: usage 时,preset-env 只能基于单个模块去分析它使用到的 polyfill 从而进入引入。

在 usage 情况下,如果我们存在很多个模块,那么无疑会多出很多冗余代码(require(xxxx))。

  • usage 是在每个模块内部按需引入 polyfill。
  • 而 entry 则会在代码入口中一次性引入。

usageBuintIns 不同参数分别有不同场景的适应度,具体参数使用场景还需要大家结合自己的项目实际情况找到最佳方式。

@babel/runtime

上边我们使用 preset-env 来引入 polyfill,存在污染全局变量的副作用,在实现 polyfill 时 Babel 还提供了另外一种方式去让我们实现这功能,那就是 @babel/runtime。

@babel/runtime 更像是一种按需加载变量的解决方案,比如哪里需要使用到 Promise,@babel/runtime 就会在他的文件顶部添加 import promise from 'babel-runtime/core-js/promise',而不是 require("core-js/modules/es.promise.js");。前者有导入变量声明,后者则没有。

我们在使用 preset-env 的 useBuintIns 配置项时,我们的 polyfill 是 preset-env 帮我们自动引入的。

而 babel-runtime 仅仅包含了垫片的实现方式。如果我们想通过 babel-runtime 按需加载 polyfill,就得自己引入对应的 polyfill。

如何使用 @babel/runtime 呢,只要我们去安装 npm install --save @babel/runtime 后,在需要使用对应的 polyfill 的地方去单独引入就可以了。比如:

js
// a.js 中需要使用 Promise,我们需要手动引入对应的运行时 polyfill
import Promise from '@babel/runtime/core-js/promise'

const promsies = new Promise()

@babel/plugin-transform-runtime

为什么使用它?解决辅助函数重复定义声明,自动引入 @babel/runtime 以创建 polyfill 的沙箱隔离环境,不污染全局空间。

js
{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false, // 用来自定义 @babel/plugin-transform-runtime 引入 @babel/runtime/ 模块的路径规则,取值是布尔值或字符串。没有特殊需求,我们不需要修改,默认为 false 即可。
        "corejs": false, // 是否启用 polyfill
        "helpers": true, // 替换辅助函数
        "regenerator": true, // generator 函数是否使用不污染全局的  regenerator runtime(运行时)
        "version": "7.0.0-beta.0"
      }
    ]
  ]
}

用 node 执行下面的文件

js
const fs = require('fs')
const core = require('@babel/core')

const res = core.transformFileSync('./code.js', {
  presets: [
    // 让 preset 仅仅转换语法
    ['@babel/preset-env']
  ],
  plugins: [
    ['@babel/plugin-transform-runtime', {
      "corejs": 3,
      "helpers": true,
      "regenerator": true,
      "version": "7.0.0-beta.0"
    }]
  ]
})

const { code } = res

const output = './code-compiled-runtime.js'

if(fs.existsSync(output)) {
  fs.rmSync(output, { recursive: true, force: true })
}
fs.writeFileSync(output, code)

得到编译后的代码:

js
"use strict";

var _regeneratorRuntime2 = require("@babel/runtime-corejs3/regenerator");
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var _marked = /*#__PURE__*/_regeneratorRuntime2.mark(abc);
// import "core-js/stable"

var foo = '1';
var bar = function bar() {
  console.log('我是箭头函数');
};
var a = 2;
a = 4;
_promise["default"].resolve().then(function () {
  console.log('hello, promise');
});
var Person = /*#__PURE__*/(0, _createClass2["default"])(function Person() {
  (0, _classCallCheck2["default"])(this, Person);
});
function abc() {
  return _regenerator["default"].wrap(function abc$(_context) {
    while (1) switch (_context.prev = _context.next) {
      case 0:
      case "end":
        return _context.stop();
    }
  }, _marked);
}

插件先于 preset 执行,为什么 plugin-transform-runtime 还能配合 preset 使用呢?

plugin-transform-runtime 的源码

会根据配置来引入 corejs、regenerator 的转换插件,实现 polyfill 注入的功能。

并且还设置了一个 helperGenerator 的函数到全局上下文 file,这样后面 @babel/preset-env 就可以用它来生成 helper 代码。那自然也就是抽离的了。

tips

建议大家不要同时开启两种 polyfill,这两个东西完全是 Babel 提供给不同场景下的不同解决方案。 它不仅会造成重复打包的问题还有可能在某些环境下直接造成异常,具体你可以参考这个 Issue

当然,你同样可以在业务项目中配合 @babel/preset-env 的 polyfill 同时使用 @babel/plugin-transform-runtime 的 helper 参数来解决多个模块内重复定义工具函数造成冗余的问题。

但是切记设置 runtime 的 corejs:false 选项,关闭 runtime 提供的 polyfill 的功能,仅保留一种 polyfill 提供方案。

就像 vue-cli 一样:

Released under the MIT License.