Babel概述
在使用vue-cli工具生成的项目中,如果选择的是In dedicated config files,则在项目完成后,会有一个babel.config.js文件,项目中的Babel都会在这里配置。
简单介绍一下Babel, Babel是一个工具,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。所以 Babel 在转译的时候,会将源代码分成 语法(syntax) 和 特性(api) 两部分来处理:
- syntax:类似于展开对象、optional chain、let、const 等语法。
- api:类似于 [1,2,3].includes ,map等函数、方法。
@babel/preset-env
对于使用webpack的vue cli项目来说,项目脚手架生成后会自动引入babel-loader来处理代码中使用的新syntax,而对于api相关的转换,则需要引入polyfill 来处理,在babel.config.js文件中,有一个
@babel/preset-env配置,用来告诉 Babel 如何处理 api。
代码如下:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
其中有一个关键配置选项 useBuiltIns,它的取值共有三个:
- false:即不处理 api,不会自动对每个文件的api进行转换,也不会去引入polyfill。
- entry:对全部文件进行api转换,并且需要在入口文件中手动引入
import from @babel/polyfill。 - usage:对api的转换采用按需加载,即用到哪个方法就自动引入对应的转换代码,不会全量的引入polyfill,无需在页面入口手动引入
import from @babel/polyfill。
其中false是默认值,一般用的比较少,而@babel/polyfill在Babel 7.4之后分成了两个库core-js和regenerator-runtime,所以需要引入这两个库来代替,一般情况下使用usage是最合适的,在构建中会自动扫描代码中使用的新api,并引入对应的转换代码,而不是全量引入,从而可以减少资源包的大小,但是需要注意的是:
-
配置
usage可以按需引入转换代码,但是@babel/polyfill依然需要安装。但是引入方式需要修改成core-js和regenerator-runtime。 -
配置
usage可以按需引入转换代码,但是对于node_modules文件夹下的代码,默认是不会转换的(使用vue cli创建的项目,babel-loader默认不会转换这部分代码),所以类似ant-design,element-ui这些使用了新的api的库,在node_modules里是不会被转换的。
对于此,可以再用entry方式来全量引入,确保api转换不会出问题,当然可也以修改vue.config.js配置文件来增加对node_modules下的库转换,代码如下:
module.exports = {
...
transpileDependencies: ['ant-design-vue'],
}
另外,对于@babel/preset-env的配置项中的corejs需要和安装的corejs版本一致。
@babel/plugin-transform-runtime
如果查看babel转换过的代码可以知道,对于includes这种api 直接是 require 了一下,并不是另一种更符合直觉的方式,代码如下:
var includes = require('xxx/includes')
所以 Babel 的 polyfill 机制是,对于例如 Array.from 等静态方法,直接在 global.Array 上添加;对于例如 includes 等实例方法,直接在 global.Array.prototype 上添加。这样直接修改了全局变量的原型,有可能会带来意想不到的问题。这个问题在开发第三方库的时候尤其重要,因为我们开发的第三方库修改了全局变量,有可能和另一个也修改了全局变量的第三方库发生冲突,或者和使用我们的第三方库的使用者发生冲突。公认的较好的编程范式中,也不鼓励直接修改全局变量、全局变量原型。
另外Babel 转译 syntax 时,有时候会使用一些辅助的函数来帮忙转,比如:
// 转换前
class A {
}
typeof a
// 转换后
function _typeof(obj) { "@babel/helpers - typeof"; 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 _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var A = function A() {
_classCallCheck(this, A);
};
typeof a === "undefined" ? "undefined" : _typeof(a);
class 语法中,Babel 自定义了 _classCallCheck这个函数来辅助;typeof 则是直接重写了一遍,自定义了 _typeof 这个函数来辅助。这些函数叫做 helpers。从上图中可以看到,helper 直接在转译后的文件里被定义了一遍。如果一个项目中有100个文件,其中每个文件都写了一个 class,那么这个项目最终打包的产物里就会存在100个 _classCallCheck 函数,他们的长相和功能一模一样,这显然不合理。
@babel/plugin-transform-runtime这个插件的作用就是解决上面提到的两个问题,先执行下面两条命令安装两个库:
npm i @babel/plugin-transform-runtime -D
npm i @babel/runtime-corejs3
其中 @babel/plugin-transform-runtime 的作用是转译代码,转译后的代码中可能会引入 @babel/runtime-corejs3 里面的模块。所以前者运行在编译时,后者运行在运行时。类似 polyfill,后者需要被打包到最终产物里在浏览器中运行。
再修改配置,代码如下:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3 // 指定 runtime-corejs 的版本,目前有 2 3 两个版本
}
]
]
}
引入之后,原先的转换代码编程如下:
// 转换前
class A {
}
typeof a
// 转换后
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var A = function A() {
(0, _classCallCheck2.default)(this, A);
};
typeof a === "undefined" ? "undefined" : (0, _typeof2.default)(a);
可以看到,在引入了 transform-runtime 这个插件后:
- api 从之前的直接修改原型改为了从一个统一的模块中引入,避免了对全局变量及其原型的污染。
- helpers 从之前的原地定义改为了从一个统一的模块中引入,使得打包的结果中每个 helper 只会存在一个。
两者混用
@babel/plugin-transform-runtime的大多数应用场景是在写第三方库时来使用,这样可以避免影响到使用者的JavaScript环境,造成全局污染。
在自己独立的项目中,更多情况下推荐使用@babel/preset-env搭配useBuiltIns即可,而无需再使用@babel/plugin-transform-runtime,参考issues