babel主要用于编译JavaScript代码。一般来说,我们使用JavaScript的最新语法特性来编写程序,但为了能够兼容更多的浏览器,会使用babel将兼容性不好的语法特性编译成被各个浏览器广泛支持的形式。
哪些语法特性需要被编译?
默认情况下,babel不会编译任何语法特性,需要通过配置文件依次添加。
babel的配置方法,参考这里。
配置需要编译的语法特性其实就是在babel的配置中添加plugin
。每一个plugin
负责编译一个语法特性,多个plugin
间互不干扰。比如添加@babel/plugin-transform-arrow-functions,可以把箭头函数编译成普通函数。添加@babel/plugin-transform-classes,可以把class
声明的类编译成普通函数的形式。
更方便的管理方式
一个项目中一般会用到很多的plugin
。如果一个个的去添加,会特别耗费时间。而preset
解决了这个问题。一个preset
是一组plugin
的集合。根据功能的不同,plugin
被分类在不同的preset
中。一般来说,项目中只需要配置特定的preset
就可以满足编译需求。
Babel官方提供了一些preset
, 常用的有:
- @babel/preset-env 把一些已经发布的特性编译成被各版本浏览器广泛支持的形式。
- @babel/preset-react 用于编译
jsx
,使用react
时需要使用。
查看更多preset。
更多优化
复用helpers
babel在编译后会在代码加入一些额外的代码,叫做helpers
,用于替换被编译的语法特性。比如:
// 输入
export default class A extends B{
}
// 输出
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
var A =
/*#__PURE__*/
function (_B) {
_inherits(A, _B);
function A() {
_classCallCheck(this, A);
return _possibleConstructorReturn(this, _getPrototypeOf(A).apply(this, arguments));
}
return A;
}(B);
exports["default"] = A;
一般项目中都有很多个需要被编译的文件,编译后添加的helpers
会被添加在每一个文件中。helpers
的代码其实是固定的,这样相同的代码充斥在每一个文件中,无疑是一种浪费,大大增加了最终的代码体积。
这个问题可以通过@babel/plugin-transform-runtime解决。它使编译后的文件从公用包中引用helpers
,而不是直接添加在文件中。这样所有的文件都会公用一份helpers
,减少了文件体积。
// 使用 @babel/plugin-transform-runtime 编译后的文件
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var A =
/*#__PURE__*/
function (_B) {
(0, _inherits2["default"])(A, _B);
function A() {
(0, _classCallCheck2["default"])(this, A);
return (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(A).apply(this, arguments));
}
return A;
}(B);
exports["default"] = A;
按需使用plugin
上文中提到的@babel/preset-env可以把一些语法特性编译成被各版本浏览器广泛支持的形式。同时编译后的文件也会因此多了许多helpers
,极大的增加了文件的体积。
其实,对于很多项目来说,增加的helpers
里有很多是没有必要的。因为这些项目只需要兼容更少的浏览器,使用更少的plugin
,编译更少的语法特性。
针对这一问题,@babel/preset-env支持配置browserslist。根据需要兼容的浏览器版本号,来确定需要使用哪些plugin
。
查看配置方法。
polyfill的添加
要使项目能够兼容更多浏览器,只是编译语法特性是远远不够的。因为老浏览器所不支持的,不只是一些语法特性,还有一些全局对象和某些成员方法,比如Set
,Map
,[].findIndex
等。为了解决这一兼容问题,一般会在项目中引入polyfill,比如在入口文件中引入import '@babel/polyfill'
。
这样的确可以解决兼容问题,但却极大地增加了代码体积,需要继续优化。@babel/preset-env为这一问题提供了两种解决方案。
方案一
根据browserslist,在入口处为指定版本的浏览器添加polyfill。
注意: 入口文件一定要添加代码——import '@babel/polyfill'
。babel无法判断当前处理的文件是否是入口文件,它的处理方式是把import '@babel/polyfill'
替换为特定的polyfill。
配置方法: useBuiltIns: entry
,见配置方法。
// 输入
console.log('llala')
import '@babel/polyfill'
// 输出
"use strict";
require("core-js/modules/web.timers");
require("core-js/modules/web.immediate");
// ... 此处省略很多polyfill
console.log('llala');
方案二
根据browserslist,在当前文件中为已使用的对象或方法提供polyfill。
配置方法: useBuiltIns: usage
,见配置方法。
// 输入
[].findIndex(item => item)
// 输出
"use strict";
require("core-js/modules/es6.array.find-index");
[].findIndex(function (item) {
return item;
});
使用这一方案会有一个小问题,babel可能无法正确识别代码中是否使用了某个需要polyfill的对象或方法。我做了一些测试,如下:
// 可以识别
const key = 'findIndex';
a[key](item => item)
// 不能识别
const key = ['findIndex'].join('')
a[key](item => item)
// 可以识别
new Map()
// 不能识别
new window.Map()
如果你在使用这一方案,需要注意避开一些让babel无法正确识别的写法。
进一步优化
无论你是在使用上述的方案一,或是方案二,都可以通过配置exclude
来避免添加某些polyfill。
例如:
{
"exclude": ["web.dom.iterable"]
}
更多信息,见[文档]。
最后
笔者在写这篇文章时,babel的版本号为7.6.0
。