Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2020年,是时候进阶一下Babel了 #60

Open
closertb opened this issue Aug 17, 2020 · 0 comments
Open

2020年,是时候进阶一下Babel了 #60

closertb opened this issue Aug 17, 2020 · 0 comments

Comments

@closertb
Copy link
Owner

closertb commented Aug 17, 2020

20200816121459

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

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

有力的开场白

20200816125414
在@babel/preset-env文档的开头,很隐晦的说了这样一个知识点,中文详细解释就是:只转换新的 JavaScript 句法(syntax),比如let、const、async\await、箭头函数、...、管道运算符等,而不转换新的 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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant