乐趣区

关于javascript:2020年是时候进阶一下Babel了

本文为这个系列的第二篇,上一篇见:Babel 入门指引?

本文将围绕顶部的图分析,旨在让你更理解 Babel 编译 的四大助手和区别:

  • @babel/preset-env
  • @babel/polyfill
  • @babel/plugin-transform-runtime
  • @babel/runtime

无力的开场白


在 @babel/preset-env 文档的结尾,很费解的说了这样一个知识点,中文具体解释就是:只转换新的 JavaScript 句法(syntax),比方 let、const、asyncawait、箭头函数、…、管道运算符等,而不转换新的 API,比方 Set、Map、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的办法(比方 Object.assign,array.flat),举个????:

// 1:新的语法 const,export, async,箭头函数与? 管道估算符
export default async (input, arr) => {
  const _in = input?.name;
  // 2:新的 API 和 静态方法
  const map = new Map();
  map.set('exp', 'example');

  const mapArr = Array.from(map);
  // 3:新的实例办法
  const _arr = arr.flat();
  const val = await new Promise((res) => {setTimeout(() => {
      res({
        name: _in,
        arr: _arr
      });
    }, 100);
  });
  return val;
};

export class Test {constructor() {this.name = 'test';}
  method() {console.log('name', this.test);
  }
}

加个配置:

{
  "presets": ["@babel/preset-env",],
}

执行, 失去的转换后果:

"use strict";

Object.defineProperty(exports, "__esModule", {value: true});
exports.Test = exports["default"] = void 0;

function _classCallCheck(instance, Constructor) {// 省略具体实现...}
function _defineProperties(target, props) {// 省略具体实现...}
function _createClass(Constructor, protoProps, staticProps) {// 省略具体实现...}
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {// 省略具体实现...}
function _asyncToGenerator(fn) {// 省略具体实现...}

var _default = /*#__PURE__*/function () {var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(input, arr) {
    var _in, _arr, map, mapArr, val;

    return regeneratorRuntime.wrap(function _callee$(_context) {while (1) {switch (_context.prev = _context.next) {
          case 0:
            _in = input === null || input === void 0 ? void 0 : input.name;
            _arr = arr.flat();
            map = new Map();
            mapArr = Array.from(map);
            map.set('exp', 'example');
            _context.next = 6;
            return new Promise(function (res) {setTimeout(function () {
                res({
                  name: _in,
                  arr: _arr
                });
              }, 100);
            });

          case 6:
            val = _context.sent;
            return _context.abrupt("return", val);

          case 8:
          case "end":
            return _context.stop();}
      }
    }, _callee);
  }));

  return function (_x, _x2) {return _ref.apply(this, arguments);
  };
}();

exports["default"] = _default;

var Test = /*#__PURE__*/function () {function Test() {_classCallCheck(this, Test);

    this.name = 'test';
  }

  _createClass(Test, [{
    key: "method",
    value: function method() {console.log('name', this.test);
    }
  }]);

  return Test;
}();

exports.Test = Test;

看了后果就会明确,什么叫仅对语法做转换。因为下面的PromiseMaparr.flat() 都放弃了原样,未做兼容。接下来,咱们来搞懂为什么。

细说插件 Plugins

在第一篇曾经讲过,插件是咱们依赖 Babel 做我的项目打包时极其重要的货色。从应用上来讲,我集体将插件归为三类:

  • 是 ES 规范,但浏览器还未全副实现的,这占大头,比方 preset-env 中蕴含的插件;
  • 已处于提议阶段,暂未成为 ES 规范,比方装璜器语法:@babel/plugin-proposal-decorators;
  • 工具类语法,便于开发;比方 @babel/preset-react 系列插件;

preset-env

简直咱们每个利用 Babel 编译的我的项目,都要用到这个预设(preset),其应用形式就像咱们收场一样。这个预设蕴含了所有 es6+ 语法插件,我数了数大略有 50 来个。然而不是每个编译,这些插件都会被用上,这取决于你对这个预设的配置。

就像下面那样间接应用,其传播的信息是兼容所有 es6+ 语法,所有的插件都会被用上。所以如你看到的,例子中的所有 ES6+ 语法都被做了转换,但仅仅是 语法

如果你的指标是只须要兼容 Chrome 最近的 5 个版本,你能够这样配置:

["@babel/preset-env", { 
    "targets": {"browsers": "last 5 chrome versions"},
}]

再执行一下,你会发现编译输入根本和源文件统一,因为 Chrome 对新的 ES6 语法响应极快。

因为咱们下面重复提到过,preset-env 只会对语法做兼容,所以其转换后的代码,并不是齐全的 es5 语法,所以为了更好的兼容 IE 浏览器,在以前 咱们须要借助 @babel/polyfill 来实现 ES6+ 中新的 API 及其 全局对象上的办法。

@babel/polyfill

要应用 polyfill,其实是一件非常容易的事,比方在你的入口文件:

// index.js
import "@babel/polyfill";

但这种形式在 7.4.0 当前的版本,不再被官网提倡,取而代之的是:

import "core-js/stable";
import "regenerator-runtime/runtime";

以上这都是官网给的应用示例,我本人并没有这样做,前面会细说。

接着来聊聊 polyfill 中的具体实现 (更精确的说是 corejs 中的实现), 以core-js/fn/array/includes 为例:

// _array-includes.js
var toIObject = require('./_to-iobject');
var toLength = require('./_to-length');
var toAbsoluteIndex = require('./_to-absolute-index');
module.exports = function (IS_INCLUDES) {return function ($this, el, fromIndex) {var O = toIObject($this);
    var length = toLength(O.length);
    var index = toAbsoluteIndex(fromIndex, length);
    var value;
    // Array#includes uses SameValueZero equality algorithm
    // eslint-disable-next-line no-self-compare
    if (IS_INCLUDES && el != el) while (length > index) {value = O[index++];
      // eslint-disable-next-line no-self-compare
      if (value != value) return true;
    // Array#indexOf ignores holes, Array#includes - not
    } else for (;length > index; index++) if (IS_INCLUDES || index in O) {if (O[index] === el) return IS_INCLUDES || index || 0;
    } return !IS_INCLUDES && -1;
  };
};

// add to prototype
var $export = require('./_export');
var $includes = require('./_array-includes')(true);

$export($export.P, 'Array', {includes: function includes(el /* , fromIndex = 0 */) {return $includes(this, el, arguments.length > 1 ? arguments[1] : undefined);
  }
});

简略来讲,就是以 es6 以前的语法来实现 includes 这个实例办法,并将其增加到 Array.prototype 原型上。但其具体实现比我说的更谨严一些,能够自行去看源码。

除了间接在入口文件导入,还能够通过配合 preset-env 的 useBuiltIns 属性,其默认值为 false,即不解决 API 和 办法,要应用 polyfill,须要将其设置为:

  • entry: 全量导入, 即所有 API 和 办法,无论我的项目中是否用到;
  • usage: 按需导入,仅我的项目中是否用到的,polyfill 文件不须要在入口手动注入,会主动注入,而后你会发现构建后的文件头部多了相似上面的代码:

    require("core-js/modules/es6.array.iterator");
    
    require("core-js/modules/es6.object.to-string");
    
    require("core-js/modules/es6.string.iterator");
    
    require("core-js/modules/es6.map");
    
    require("core-js/modules/es6.function.name");
    
    require("regenerator-runtime/runtime");
    
    // ...

transform-runtime

如果你的打包场景是组件库,你会发现,在你构建后的每个 js 文件都存在上面两个问题:

1.辅助函数,每个文件中都有雷同的实现,造成我的项目体积变大;

// ...
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {// 省略具体实现...}
function _asyncToGenerator(fn) {// 省略具体实现...}

2.polyfill 的引入文件,会净化全局变量

// ...
require("regenerator-runtime/runtime");
// ...

作为一个组件库,后面说过,引入 polyfill 会间接在其全局对象上增加 ES6+ 新增的静态方法和示例,是带有侵入性的。如果应用这个组件库的人不知情,且某个 hack 办法和浏览器的实现有差异,那就会带来一些让使用者十分头疼的bug, 这种锅谁背谁脸黑。

那针对下面两点,有没有好的解决办法?

有,@babel/plugin-transform-runtime,其官网文档是这样介绍的:

一个可重用 Babel 注入的帮忙程序代码以节俭代码大小的插件。

但须要记住的是 @babel/plugin-transform-runtime 只是一个插件 (或者叫媒介),其并不蕴含复用的函数和代码,复用代码的其具体实现是存在于@babel/runtime(-corejsx), 应用哪个代码包随着插件配置属性corejs 的值变动而变动,其对应关系:

  • false: @babel/runtime
  • 2:@babel/runtime-corejs2
  • 3: @babel/runtime-corejs3

当引入 @babel/plugin-transform-runtime,并将配置改成上面这样:

{
  "presets": ["@babel/preset-env",],
  "plugins": [
    ["@babel/plugin-transform-runtime", {"corejs": false}],
  ]
}

咱们将惊喜的看到,函数复用的性能实现了, 其代码编程了上面这样:

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

Object.defineProperty(exports, "__esModule", {value: true});
exports.Test = exports["default"] = void 0;

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));

// 上面代码与最开始统一

能够很显著的看出,其只是将函数的具体实现变成了模块引入,这样就很容易的实现了复用。那 corejs 为 2 和 3 带来的意义呢?

设置 corejs: 2:除了 false 蕴含的性能,还蕴含了对新增 API 和 全局静态方法(Object.assign, Array.from 等) 的 polyfill

var _from = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/array/from"));
var _map = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/map"));

// ...
_arr = arr.flat();
map = new _map["default"]();
mapArr = (0, _from["default"])(map);
map.set('exp', 'example');

设置 corejs: 3:除了 2 蕴含了的代码,其减少了对 实例办法 的 polyfill

var _flat = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/flat"));

// ...
_arr = (0, _flat["default"])(arr).call(arr);
map = new _map["default"]();

到这,你应该就看明确了 runtime、runtime-corejs2、runtime-corejs3 三个选项的区别;同时也解决了最后面提到的两个问题,相比 polyfill,对于组件库这种实现的确更加灵便,而且不会影响内部利用代码实现。

@babel/plugin-transform-runtime除了 corejs 这个选项,还蕴含其余的一些属性,点击这里在官网查看更多,能够本人拷贝代码,运行一下加深印象。

IE 兼容最佳实际

后面咱们提到过两种兼容到 ES5 语法的形式:

  • polyfill 配合 useBuiltIns,这种形式侧重于 web 利用的构建;
  • transform-runtime 配合 corejs,这种形式侧重于组件办法类库的开发

哪正对应大多数场景,polyfill 配合 useBuiltIns 是否就是最优解呢?

答案:否,这里给个链接:2020 如何优雅的兼容 IE

至此,本文卒!!!!!!

退出移动版