babel实战--如何在项目中使用babel
概要
目前在使用的三大框架(vue.js, react.js, angular.js)都有相应的脚手架工具已经贴心的帮我们集成了babel的各种配置,因此我们少了很多配置的工作量,一条命令就可以开始开发业务代码了,觉得自己又牛逼闪闪了。显示那是我们的错觉,与大佬之间的距离也许就差一个对babel深度的学习,接下来我们来详细了解一下babel的配置,让你见到这些配置时不再一脸懵逼。
餐前小菜
babel将es6+(指es6及以上版本)分为
- 语法层:
let
、const
、class
、箭头函数
等,这些需要在构建时进行转译,是指在语法层面上的转译,(比如class...将来会被转译成var function...) - api层:
Promise
、includes
、map
等,这些是在全局或者Object、Array等的原型上新增的方法,它们可以由相应es5的方式重新定义babel对这两个分类的转译的做法肯定是不一样的,我们也需要给出相应的配置
准备工作
以windows系统为例:
在你的项目目录下新建文件夹babel-demo, 执行cd babel-demo进入文件夹,执行npm init -y 初始化项目。
执行 npm install --save-dev @babel/core @babel/cli
安装babel必要的依赖。
babel-core
:Babel 的核心,包含各个核心的 API,供 Babel 插件和打包工具使用babel-cli
:命令行对 js 文件进行换码的工具
新建目录src, 在目录下新建index.js, 并写下如下测试代码:
在package.json中配置一条命令--compiler,用于执行babel
"scripts": {
"compiler": "babel src --out-dir lib --watch"
},
然后执行 npm run compiler
理论上babel会将let
都转化成var
,但是你会发现这时候转译出来的代码跟原来一样,这是为啥呢?
Babel 本身不具有任何转化功能,它把转化的功能都分解到一个个 plugin 里面。因此当我们不配置任何插件时,经过 babel 处理的代码和输入是相同的。所以我们需要安装插件。
插件(用于处理语法层)
两种方式使用插件,一种是一个个的安装(比较麻烦),另一种是以preset的方式安装一组插件,我们当然要选省事的preset了。
- 首先安装preset
npm i @babel/preset-env -D
,@babel/preset-env
包含的插件支持所有最新的JS特性(ES2015,ES2016等,不包含 stage 阶段) - 配置
建立 .babelrc文件或者babelconfig.js文件,添加以下代码,babel会自动寻找这个文件
{
"presets": [
[
"@babel/preset-env"
]
]
}
补充说明:如果你用 Vue ,presets 一般是
@vue/app
,这个是把 在@babel/preset-env
包含的 plugins 上又加了很多自己定义的 plugins。
所有的 Vue CLI 应用都使用@vue/babel-preset-app
,它包含了babel-preset-env
、JSX 支持
以及为最小化包体积优化过的配置。@vue/babel-preset-app
已经默认配置了@babel/plugin-transform-runtime
。通过它的文档可以查阅到更多细节和 preset 选项。
执行npm run compiler
命令,这时候就转译成功了,转译结果如下:
"use strict";
var a = 1;
var b = 2;
var c = a + b;
babel还可配置taget
或者提供.browserlist
文件,用于指定目标环境,这样能使你的代码体积保持更小。
//.browserslistrc
> 0.25%
not dead
polyfill(用于处理Api层)
上边只验证了语法层面的转译,接下来我们试试api层面的转译是怎样的
我们修改index.js如下:
// Promise是api层面的,是一个es6中的全局对象
const p = new Promise((resolve, reject) => {
resolve(100);
});
执行npm run compiler
命令,编译结果如下:
"use strict";
var p = new Promise(function (resolve, reject) {
resolve(100);
});
const-->var没有问题,但是Promise并没有任何变化,难道是我被诅咒了吗,其实并不是,这时候就需要Polyfill
这个东西了。
polyfill
的中文意思是垫片,顾名思义就是垫平不同浏览器或者不同环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用。
使用方法如下:
安装 @babel/polyfill
依赖。注意这是一个运行时依赖。所以不要加-dev,npm install --save @babel/polyfill@babel/polyfill
模块包括 core-js
和一个自定义的 regenerator runtime
模块,可以模拟完整的 ES2015+ 环境。然后在index.js中引入。
import '@babel/polyfill';
const p = new Promise((resolve, reject) => {
resolve(100);
});
转译结果如下:
"use strict";
require("@babel/polyfill");
var p = new Promise(function (resolve, reject) {
resolve(100);
});
虽然看起来Promise还是没有转译,但是我们引入的 polyfill
中已经包含了对Promise
的定义,所以这时候代码便可以在低版本浏览器中运行了。
useBuiltIns属性
不知道大家又没有想到这样一个问题,我代码里边只用到了几个es6,却需要引入所有的垫片,这不合情理啊。要优化这一点,就需要用到useBuiltIns这个属性了。useBuiltIns这一配置项,它的值有三种:
false
: 不对polyfills做任何操作entry
: 根据target中浏览器版本的支持,将polyfills拆分引入,仅引入有浏览器不支持的polyfillusage
(新):检测代码中ES6/7/8等的使用情况,仅仅加载代码中用到的polyfills
然后我们修改配置如下:
执行npm run compiler
命令,这是命令行中会出现如下警告:
意思是需要我们指定corejs的版本。修改.babelrc如下:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
目前推荐使用的corejs版本是3版本,新的特性也只会添加在这个版本。此时,转译结果如下:
"use strict";
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
var p = new Promise(function (resolve, reject) {
resolve(100);
});
@babel/plugin-transform-runtime解决代码冗余
代码冗余是出现在转译语法层时出现的问题。修改下我们的index.js,这次我们再写一个语法层面的es6-->class(let,const这些对比不出问题,所以用class),看看babel会把class转化成什么
//index.js
class Student {
constructor(name,age){
this.name = name;
this.age = age
}
}
转换结果如下:
"use strict";
require("core-js/modules/es.function.name");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Student = function Student(name, age) {
_classCallCheck(this, Student);
this.name = name;
this.age = age;
};
从结果看似乎没什么问题,但事实上,如果我们在其他文件中也使用了class,你会发现_classCallCheck在每个文件中都会出现,这就造成了代码冗余(我们示例中只使用了class,可能冗余的不明显,实际项目中这些函数可能是很长的)。
这时候需要用到另外一个插件了@babel/plugin-transform-runtime
。该插件会开启对 Babel 注入的辅助函数(比如上边的_classCallCheck)的复用,以节省代码体积。
这些辅助函数在@babel/runtime
中,所以需要安装@babel/runtime
,当然@babel/runtime
也是运行时依赖。(在对一些语法进行编译的时候,babel需要借助一些辅助函数)
安装@babel/plugin-transform-runtime和@babel/runtime npm i --save-dev @babel/plugin-transform-runtime @babel/runtime
然后修改配置:
{
"presets": [
["@babel/preset-env",{
"useBuiltIns":"usage",
"corejs":3
}]
],
"plugins": [
["@babel/plugin-transform-runtime"]
]
}
转换结果如下:
"use strict";
require("core-js/modules/es.function.name");
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var Student = function Student(name, age) {
(0, _classCallCheck2["default"])(this, Student);
this.name = name;
this.age = age;
};
可以发现,相关的辅助函数是以require的方式引入而不是被直接插入进来的,这样就不会冗余了。
除了解决代码冗余,@babel/plugin-transform-runtime还有另外一个重要的能力——解决全局污染。
解决全局污染
全局污染是出现在转译api层出现的问题
我们这次依然用Promise来做实验
修改index.js
new Promise(function (resolve, reject) {
resolve(100);
});
转译结果如下:
"use strict";
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
new Promise(function (resolve, reject) {
resolve(100);
});
可以看出preset-env在处理例如Promise这种的api时,只是引入了core-js中的相关的js库,这些库重新定义了Promise,然后将其挂载到了全局。
这里的问题就是:必然会造成全局变量污染,同理其他的例如Array.from等会修改这些全局对象的原型prototype,这也会造成全局对象的污染。
解决方式就是:将core-js
交给transform-runtime
处理。添加一个配置即可,非常简单。
{
"presets": [
["@babel/preset-env"]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
可以看出,我们是将core-js
这个属性添加到@babel/plugin-transform-runtime
这个插件的配置下,让这个插件处理,同时也不需要配置useBuiltIns
了,因为在babel7
中已经将其设置为默认值
(transform-runtime是利用plugin自动识别并替换代码中的新特性,检测到需要哪个就用哪个)
这里注意下:有的博客上说无法转译includes等实例方法,其实说错了,官方文档是这样说的:corejs: 2仅支持全局变量(例如Promise)和静态属性(例如Array.from),corejs: 3还支持实例属性(例如[].includes)。转译结果:
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
new _promise["default"](function (resolve, reject) {
resolve(100);
});
可以看出,这时的代码并没有在全局直接添加一个Promise
,而是定义了一个_promise["default"]
方法,这样便不会出现全局变量污染的情况所以综上可得出@babel/plugin-transform-runtime
这个插件的强大之处有以下几点:
实现对辅助函数的复用,解决转译语法层时出现的代码冗余
解决转译api层出现的全局变量污染
但是transform-runtime也有缺点:
每个特性都会经历检测和替换,随着应用增大,可能会造成转译效率不高
webpack中使用babel
安装babel-loader, npm install --save-dev babel-loader
webpack.config.js配置如下:
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
"plugins": [
["@babel/plugin-transform-runtime",{
"corejs":3
}]
]
}
}
}
]
}
使用vue-cli的同学需要注意, 因为babel-loader
很慢,所以webpack官方推荐转译尽可能少的文件(参考),所以vue-cli配置了该loader的exclude
选项,将node_moduls
中的文件排除了,但是这样可能会造成某个依赖出现兼容性问题。所以,如果你的项目中某个依赖出现了兼容性问题,这可能就是原因。解决办法就是在vue.config.js
中配置transpileDependencies
这个选项,Babel就会 显式转译这个依赖。