Open closertb opened 4 years ago
本文为这个系列的第二篇,上一篇见:Babel 入门指引?
本文将围绕顶部的图剖析,旨在让你更了解Babel 编译的四大助手和区别:
Babel 编译
在@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;
看了结果就会明白,什么叫仅对语法做转换。因为上面的Promise、Map,arr.flat() 都保持了原样,未做兼容。接下来,我们来搞懂为什么。
Promise
Map
arr.flat()
在第一篇已经讲过,插件是我们依赖Babel做项目打包时极其重要的东西。从使用上来讲,我个人将插件归为三类:
preset-env
@babel/plugin-proposal-decorators
@babel/preset-react
几乎我们每个利用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 及其 全局对象上的方法。
在以前
要使用polyfill,其实是一件非常容易的事,比如在你的入口文件:
// index.js import "@babel/polyfill";
但这种方式在7.4.0以后的版本,不再被官方提倡,取而代之的是:
import "core-js/stable"; import "regenerator-runtime/runtime";
以上这都是官方给的使用示例,我自己并没有这样做,后面会细说。
接着来聊聊polyfill中的具体实现(更准确的说是corejs 中的实现), 以core-js/fn/array/includes为例:
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
usage: 按需导入,仅项目中是否用到的,polyfill文件不需要在入口手动注入,会自动注入,然后你会发现构建后的文件头部多了类似下面的代码:
usage
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.`辅助函数,每个文件中都有相同的实现,造成项目体积变大; ```js // ... function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { // 省略具体实现... } function _asyncToGenerator(fn) { // 省略具体实现... }
2.polyfill 的引入文件,会污染全局变量
2.
// ... require("regenerator-runtime/runtime"); // ...
作为一个组件库,前面说过,引入polyfill会直接在其全局对象上添加ES6+ 新增的静态方法和示例,是带有侵入性的。如果使用这个组件库的人不知情,且某个hack方法和浏览器的实现有差别,那就会带来一些让使用者非常头疼的bug, 这种锅谁背谁脸黑。
bug
那针对上面两点,有没有好的解决方法?
有,@babel/plugin-transform-runtime,其官方文档是这样介绍的:
@babel/plugin-transform-runtime
一个可重用Babel注入的帮助程序代码以节省代码大小的插件。
但需要记住的是@babel/plugin-transform-runtime只是一个插件(或者叫媒介),其并不包含复用的函数和代码,复用代码的其具体实现是存在于@babel/runtime(-corejsx), 使用哪个代码包随着插件配置属性corejs的值变化而变化,其对应关系:
@babel/runtime
x
corejs
@babel/runtime-corejs2
@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
corejs: 2
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
corejs: 3
实例方法
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这个选项,还包含其他的一些属性,点击这里在官网查看更多,可以自己拷贝代码,运行一下加深印象。
前面我们提到过两种兼容到ES5 语法的方式:
哪正对应大多数场景,polyfill 配合useBuiltIns 是否就是最优解呢?
答案:否,这里给个链接:2020 如何优雅的兼容 IE
至此,本文卒!!!!!!
本文为这个系列的第二篇,上一篇见:Babel 入门指引?
本文将围绕顶部的图剖析,旨在让你更了解
Babel 编译
的四大助手和区别:有力的开场白
在@babel/preset-env文档的开头,很隐晦的说了这样一个知识点,中文详细解释就是:只转换新的 JavaScript 句法(syntax),比如let、const、async\await、箭头函数、...、管道运算符等,而不转换新的 API,比如 Set、Map、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign,array.flat ),举个🌰:
加个配置:
执行, 得到的转换结果:
看了结果就会明白,什么叫仅对语法做转换。因为上面的
Promise
、Map
,arr.flat()
都保持了原样,未做兼容。接下来,我们来搞懂为什么。细说插件Plugins
在第一篇已经讲过,插件是我们依赖Babel做项目打包时极其重要的东西。从使用上来讲,我个人将插件归为三类:
preset-env
中包含的插件;@babel/plugin-proposal-decorators
;@babel/preset-react
系列插件;preset-env
几乎我们每个利用Babel编译的项目,都要用到这个预设(preset),其使用方式就像我们开场一样。这个预设包含了所有es6+ 语法插件,我数了数大概有50来个。但是不是每个编译,这些插件都会被用上,这取决于你对这个预设的配置。
就像上面那样直接使用,其传达的信息是兼容所有es6+语法,所有的插件都会被用上。所以如你看到的,例子中的所有ES6+ 语法都被做了转换,但仅仅是
语法
。如果你的目标是只需要兼容Chrome最近的5个版本,你可以这样配置:
再执行一下,你会发现编译输出基本和源文件一致,因为Chrome 对新的ES6语法响应极快。
因为我们上面反复提到过,preset-env只会对语法做兼容,所以其转换后的代码,并不是完全的es5语法,所以为了更好的兼容IE浏览器,
在以前
我们需要借助@babel/polyfill 来实现ES6+ 中新的API 及其 全局对象上的方法。@babel/polyfill
要使用polyfill,其实是一件非常容易的事,比如在你的入口文件:
但这种方式在7.4.0以后的版本,不再被官方提倡,取而代之的是:
以上这都是官方给的使用示例,我自己并没有这样做,后面会细说。
接着来聊聊polyfill中的具体实现(更准确的说是corejs 中的实现), 以
core-js/fn/array/includes
为例:简单来讲,就是以es6 以前的语法来实现includes这个实例方法,并将其添加到Array.prototype原型上。但其具体实现比我说的更严谨一些,可以自行去看源码。
除了直接在入口文件导入,还可以通过配合preset-env的useBuiltIns属性,其默认值为false,即不处理API 和 方法,要使用polyfill,需要将其设置为:
entry
: 全量导入, 即所有API 和 方法,无论项目中是否用到;usage
: 按需导入,仅项目中是否用到的,polyfill文件不需要在入口手动注入,会自动注入,然后你会发现构建后的文件头部多了类似下面的代码: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");
// ...
2.
polyfill 的引入文件,会污染全局变量作为一个组件库,前面说过,引入polyfill会直接在其全局对象上添加ES6+ 新增的静态方法和示例,是带有侵入性的。如果使用这个组件库的人不知情,且某个hack方法和浏览器的实现有差别,那就会带来一些让使用者非常头疼的
bug
, 这种锅谁背谁脸黑。那针对上面两点,有没有好的解决方法?
有,
@babel/plugin-transform-runtime
,其官方文档是这样介绍的:但需要记住的是
@babel/plugin-transform-runtime
只是一个插件(或者叫媒介),其并不包含复用的函数和代码,复用代码的其具体实现是存在于@babel/runtime
(-corejsx
), 使用哪个代码包随着插件配置属性corejs
的值变化而变化,其对应关系:@babel/runtime
@babel/runtime-corejs2
@babel/runtime-corejs3
当引入@babel/plugin-transform-runtime,并将配置改成下面这样:
我们将惊喜的看到,函数复用的功能实现了,其代码编程了下面这样:
可以很明显的看出,其只是将函数的具体实现变成了模块引入,这样就很容易的完成了复用。那corejs为2和3 带来的意义呢?
设置
corejs: 2
:除了false包含的功能,还包含了对新增API 和 全局静态方法(Object.assign, Array.from等)的polyfill设置
corejs: 3
:除了2包含了的代码,其增加了对实例方法
的polyfill到这,你应该就看明白了runtime、runtime-corejs2、runtime-corejs3 三个选项的区别;同时也解决了最前面提到的两个问题,相比polyfill,对于组件库这种实现确实更加灵活,而且不会影响外部应用代码实现。
@babel/plugin-transform-runtime
除了corejs这个选项,还包含其他的一些属性,点击这里在官网查看更多,可以自己拷贝代码,运行一下加深印象。IE 兼容最佳实践
前面我们提到过两种兼容到ES5 语法的方式:
哪正对应大多数场景,polyfill 配合useBuiltIns 是否就是最优解呢?
答案:否,这里给个链接:2020 如何优雅的兼容 IE
至此,本文卒!!!!!!