搭建测试环境
在根目录下npm init -y
,生成基本的package.json
,创建src
,并在src
创建index.js
文件,用于等会balel要编译的文件。然后在根目录创建.babelrc
文件,作为babel
的配置文件。
安装依赖
按照官网的流程,安装babel
的一些依赖模块
npm install --save-dev @babel/core @babel/cli @babel/preset-env
npm install --save @babel/polyfill
配置.babelrc
babel
编译会自动读取更目录下的.babelrc
文件去读取配置
{
"presets": [
[
"@babel/env",
{
"targets": {
"chrome": "43"
},
"useBuiltIns": "usage",
"corejs": {
"version": 2
}
}
]
],
"plugins": []
}
配置文件里可能会有preset、plugins这两个字段,简单介绍下:
-
plugins:用于将代码由高版本语法转为低版本插件集合,如:@babel/plugin-transform-arrow-functions,可以将箭头函数转换为es5。
-
preset:由于我们一个项目中可能会使用到很多bable插件,如果每次都要一个一个添加,很是麻烦。为了解决这个问题,babel提供了插件组合,也就是perset。
测试es6语法
let a = 1;
const foo = () => {
console.log("箭头函数")
}
运行下面命令
// 编译src下的文件并输出到lib文件夹下
npx babel src --out-dir lib
// 编译结果
"use strict";
var a = 1;
var foo = function foo() {
console.log("箭头函数");
};
可以看出已经被打包成了es5语法。
测试高版本api
includes
是es7新出的api,测试下会被打包成什么?
let arr = [1, 2, 3];
console.log(arr.includes(1))
打包完后
"use strict";
require("core-js/modules/es7.array.includes");
var arr = [1, 2, 3];
console.log(arr.includes(1));
打包完成发现会新增一个require
,这个文件会重写Array的includes
方法,使用低版本语法实现es7新增的includes
方法。
babel-polyfill
其实上面的高版本语法并不是由babel
进行转换的,而是通过babel-polyfill
来进行转,说到这我们有必要好好讲讲babel-polyfill
这个包了。先放上官网地址。
先简单解释一下:babel默认只会转义js语法,但对于一些新的API
是不会做转换的, 像include
、Array.from
等方法。babel-polyfill
做的事情就是帮你兼容这些高版本语法。
组成:babel-polyfill
包含了core-js
和regenerator-runtime
这两个包。
core-js: 一些高版本语法转低版本就是由这个库来实现的。
regenerator-runtime:对async await
提供转换的库。
但很奇怪🤔,我们并没有在项目中的任何地方引入babel-polyfill
,怎么就生效了呢😳😳。其实这得益于我们在.babelrc
中配置的预设@babel/env
,这个具体是干嘛用的呢。
@babel/env
env
是我们在babel中最常用的,env 的核心目的是通过配置得知目标环境的特点,然后只做必要的转换。例如目标浏览器支持 es2015,那么 es2015 这个 preset 其实是不需要的,于是代码就可以小一点(一般转化后的代码总是更长),构建时间也可以缩短一些。
如果不写任何配置项,env 等价于 latest,也等价于 es2015 + es2016 + es2017 三个相加(不包含 stage-x 中的插件)。env 包含的插件列表维护在这里。
那我们现在来看下,为什么上面没有引入polyfill
,它就直接生效了呢?不用想肯定也能猜到,是env
这个预设去引入了。我们可以看下关于babel/env的里关于useBuiltIns
的介绍
"usage"
|"entry"
|false
, defaults tofalse
.This option configures how
@babel/preset-env
handles polyfills.When either the
usage
orentry
options are used,@babel-preset-env
will add direct references tocore-js
modules as bare imports (or requires). This meanscore-js
will be resolved relative to the file itself and needs to be accessible.
当useBuiltIns = entry
,需要在代码顶部去引一下polyfill
// 输入
import "@babel/polyfill"
let arr = [1, 2, 3];
console.log(arr.includes(1))
// 编译结果
"use strict";
...
require("core-js/modules/es6.array.copy-within.js");
require("core-js/modules/es6.array.fill.js");
require("core-js/modules/es7.array.includes.js");
...
var arr = [1, 2, 3];
console.log(arr.includes(1));
可以看到,这种模式下,babel 会将chrome 43
不支持的所有的内容全部引入,这样会导致结果的包大小非常大,而我们这里仅仅需要 includes 一个方法而已。
当useBuiltIns = usage
,会帮我们按需加载,且不需要我们手动引入
// 输入
let arr = [1, 2, 3];
console.log(arr.includes(1))
// 编译结果
"use strict";
require("core-js/modules/es7.array.includes.js");
var arr = [1, 2, 3];
console.log(arr.includes(1));
这个好像就牛逼了,能分析到是否调用了Array.includs
方法再去引用。但真的是这样的吗?
// 输入
const Foo = function () {};
Foo.prototype.includes = function () { };
new Foo().includes();
// 编译结果
"use strict";
require("core-js/modules/es7.array.includes.js");
var Foo = function Foo() {};
Foo.prototype.includes = function () {};
new Foo().includes();
可以看到我并没有调用Array.includes,但是它却还是引入了,所以它应该通过方法名来判断是否需要引入。
当useBuiltIns = false
,即代表不处理api。
弊端
但是上面两种方式有两个的弊端,
-
它们是通过直接修改全局构造函数的原型对象上的方法来实现
polyfill
,这样会造成全局污染,有可能会照成意想不到的bug。 -
babel 转译时,有时候会使用一些辅助的函数来帮忙,比如:
// 输入 class Test {} // 编译结果 "use strict"; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Test = function Test() { _classCallCheck(this, Test); };
class 语法中,babel 自定义了 _classCallCheck这个函数来辅助,如果一个项目中有多个文件,其中每个文件都写了一个 class,那么这个项目最终打包的产物里就会存在100个 _classCallCheck 函数,这显然不合理。
如果想避免这两种情况,我们可以使用@babel/plugin-transform-runtime
插件。
@babel/plugin-transform-runtime
该插件需要依赖@babel/runtime-corejs2
或@babel/runtime-corejs3
这两个包,这两个包你可以理解成是对polyfill的实现
npm install @babel/runtime-corejs2 --save
# or
npm install @babel/runtime-corejs3 --save
假设有如下代码:
class Test {}
const set = new Set();
console.log([].includes)
.babelrc
配置
{
"presets": [
[
"@babel/env",
{
"targets": {
"chrome": "43"
},
"useBuiltIns": "usage",
"corejs": {
"version": 2
}
}
]
],
"plugins": []
}
打包结果:
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
var _set = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/set"));
class Test {}
const set = new _set.default();
console.log([].includes);
发现core-js2
只会对Set
进行了处理,但是includes
没有被处理,因为core-js2
会对代码中用到的类/静态方法进行处理,对原型链上的方法不会做处理。
现在把.babelrc
的corejs
改成3,进行编译
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _set = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set"));
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var Test = function Test() {
(0, _classCallCheck2.default)(this, Test);
};
var set = new _set.default();
console.log((0, _includes.default)([]));
core-js3
处理了Set
和includes
了,可以看出core-js3
也会处理原型链上的方法。
通过上面,可以看出使用plugin-transform-runtime
插件,解决了我们上面的两个问题
- 不是直接通过修改对象原型对象上的方法,而是从一个统一模块中引入,避免了对全局变量的污染。
- 辅助函数从一个统一模块引入,避免了代码中存在多份辅助函数代码。