ES6 发布至今真的已经很久了,但不管是浏览器环境还是 Node 环境都没有完全支持,想要原汁原味地使用这些新特性,就必须用到转码神器 Babel。这篇文章就来聊一聊如何使用 Babel。
初探
新建一个项目,在其中新建一个 src
文件夹用于存放我们的源码,源码很简单,一个 javascript 文件 a.js
,其内容如下:
let entries = [['name', 'mike'], ['age', 22]];
entries.forEach((item) => {
console.log(item)
});
主要使用了 let
关键字和箭头函数。
安装 @babel/cli
和 @babel/core
:
npm install --save-dev @babel/core @babel/cli
Babel 的核心功能位于 @babel/core
中,所以使用 Babel 时一定要安装它,它的使用方式是:
const babel = require("@babel/core");
babel.transform("code", optionsObject);
可以看出,其使用方式非常不便,因此 Babel 提供了脚手架工具 @babel/cli
,方便用户使用。
在 package.json
的 scripts
中添加命令:
{
"scripts": {
"build": "babel src --out-dir dist"
}
}
此命令用于通过使用 babel
将 src
文件夹中的文件编译后输出到 dist
文件夹。
运行 npm run build
,稍等片刻,打开 dist
文件夹中的 a.js
,其内容如下:
let entries = [['name', 'mike'], ['age', 22]];
entries.forEach(item => {
console.log(item);
});
可以看到,除了少了一行空行外,代码没有任何变化,这是因为,使用 Babel,我们需要对其进行配置,否则它形同虚设。
配置
配置 Babel 的方式有两种(本质上来说其实就一种),一种是使用插件(plugins),另一种是使用预设(presets)。通常我们会将配置写在一个 .babelrc
文件中,我们也在项目的根目录下新建一个 .babelrc
文件备用。
插件
Babel 基本上为每一种新的语法(包括某些流行的非标准语法)都提供了一个对应的语法插件和转换插件,前者用于解析对应语法,如 react-jsx
用于解析 jsx
语法;后者用于转换对应语法,转换插件会自动启动语法插件,因此不需要同时指定两者。
示例中,转换箭头函数需要使用 @babel/plugin-transform-arrow-functions
,首先安装该插件:
npm install --save-dev @babel/plugin-transform-arrow-functions
然后在 .babelrc
中添加该插件:
{
"plugins": [
"@babel/plugin-transform-arrow-functions"
]
}
得到的结果如下:
let entries = [['name', 'mike'], ['age', 22]];
entries.forEach(function (item) {
console.log(item);
});
可以看到,箭头函数已经被转换成了普通函数。
注意前文的措辞,是语法转换插件,不是方法转换插件。什么意思呢?如果我们在代码中使用了新的方法:
let object = Object.fromEntries(entries);
console.log(object);
就需要使用 polyfill(通常译为垫片)来模拟这些新方法,因为这些方法在老环境中是不存在的。Babel 也提供了插件 @babel/polyfill
,但自 Babel7.4.0 之后,官方已经不推荐使用它了,取而代之的是直接在文件中引入 core-js/stable
和 regenerator-runtime/runtime
:
import "core-js/stable";
import "regenerator-runtime/runtime";
运行 npm run build
,稍等片刻,会发现编译的结果除了比源码少几行空行外没有任何区别,但如果将 import
改为 require()
(因为 Node 环境不支持 import
),然后运行 node dist/a.js
,控制台将输出下列内容:
[ 'name', 'mike' ]
[ 'age', 22 ]
{ name: 'mike', age: 22 }
说明 fromEntries
方法正常执行了。
这其实是直接使用 core-js
来进行的方法转换,如果我们有多个 JavaScript 文件,就需要在每个文件中引入上述代码,除非应用程序只有一个入口文件。
插件还有配置项,可以对其功能早进一步设置,详情请查阅相应文档。
从上面的配置方式可以看出,随着使用的新语法的增多,配置步骤会非常繁琐,通常我们并不会这样去进行配置。Babel 也考虑到了这一点,因此提供了预设。
预设
预设其实就是一堆插件的组合,前文中我们每使用一种新特性,就需要手动添加一个相应的插件,这个过程是非常繁琐的,而粗暴地引入所有插件又会增加文件体积,因此 Babel 官方将部分插件组合在一起,形成了各种“预设”,让用户可以直接使用。
官方提供的预设有:
- @babel/preset-env
- @babel/preset-flow
- @babel/preset-react
- @babel/preset-typescript
其中最通用的预设是 @babel/preset-env
,其余的预设从命名上就可以看出是针对特定环境的,本文将不做讲解。
@babel/preset-env
安装 @babel/preset-env
:
npm install --save-dev @babel/preset-env
修改 .babelrc
文件:
{
"presets": [
"@babel/preset-env"
]
}
运行 npm run buidl
,编译的结果如下:
"use strict";
require("core-js/stable");
require("regenerator-runtime/runtime");
var entries = [['name', 'mike'], ['age', 22]];
entries.forEach(function (item) {
console.log(item);
});
var object = Object.fromEntries(entries);
console.log(object);
可以看到,文件头部添加了 use strict
,将 import
转为了 require()
,将 let
转为了 var
,和前文的结果基本相同,但配置就简洁多了。
不过,@babel/preset-env
能做的不只如此,它提供了 useBuiltIns
、corejs
和 targets
配置项来优化编译结果。
useBuiltIns 和 corejs
useBuiltIns
有三个值,分别为 entry
、usage
和 false
,默认值为 false
。
当 useBuiltIns
值不为 false
时,需要配合 corejs
配置项一起使用,它用于指定所使用的 core-js
的版本,通常都设置为 3,因为 2 已不再添加新特性了,需要单独安装一下:
npm install core-js@3 --save
当 useBuiltIns
值为 entry
时:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": 3
}
]
]
}
Babel 会在编译结果中引入所有的 polyfill:
"use strict";
require("core-js/modules/es.symbol");
require("core-js/modules/es.symbol.description");
require("core-js/modules/es.symbol.async-iterator");
require("core-js/modules/es.symbol.has-instance");
//此处略去几百行···
var entries = [['name', 'mike'], ['age', 22]];
entries.forEach(function (item) {
console.log(item);
});
var object = Object.fromEntries(entries);
console.log(object);
这无疑会增加文件体积,因为我们并不一定会使用到所有的新特性。
当 useBuiltIns
值为 usage
时:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
编译的结果如下:
"use strict";
require("core-js/modules/es.array.for-each");
require("core-js/modules/es.array.iterator");
require("core-js/modules/es.object.from-entries");
require("core-js/modules/es.object.to-string");
require("core-js/stable");
require("regenerator-runtime/runtime");
var entries = [['name', 'mike'], ['age', 22]];
entries.forEach(function (item) {
console.log(item);
});
var object = Object.fromEntries(entries);
console.log(object);
可以看到,Babel 只会按需引入。
targets
targets
可以指定项目所需的运行环境,进一步优化编译结果:
{
"targets": {
"chrome": "58",
"ie": "11"
}
}
不过,使用 .browserslistrc
配置文件通常是更好的选择,因为一些其它的工具可能也需要用到这些配置,可以通用。
不足
@babel/preset-env
也有不足的地方,我们在 a.js
中再添加一些新特性:
let arr = [2, 5, 8];
console.log(arr.includes(5));
async function asyncFunc() {
console.log("async2");
}
let symbol = new Symbol()
运行 npm run build
,得到的结果如下:
"use strict";
require("core-js/modules/es.symbol");
require("core-js/modules/es.symbol.description");
require("core-js/modules/es.array.for-each");
require("core-js/modules/es.array.includes");
require("core-js/modules/es.array.iterator");
require("core-js/modules/es.object.from-entries");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
require("regenerator-runtime/runtime");
require("core-js/stable");
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
var entries = [['name', 'mike'], ['age', 22]];
entries.forEach(function (item) {
console.log(item);
});
var object = Object.fromEntries(entries);
console.log(object);
var arr = [2, 5, 8];
console.log(arr.includes(5));
function asyncFunc() {
return _asyncFunc.apply(this, arguments);
}
function _asyncFunc() {
_asyncFunc = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
console.log("async");
case 1:
case "end":
return _context.stop();
}
}
}, _callee);
}));
return _asyncFunc.apply(this, arguments);
}
var symbol = new Symbol();
可以看到第 23-25 行,Babel 注入了两个针对 async
的辅助函数,这无疑会增加文件的体积,尤其是在很多文件都用到了新特性的时候;而第 33 行的 includes
方法是添加在 Array 原型链上的,会对全局环境造成污染。
Babel 也考虑到了这一点,因此提供了 @babel/plugin-transform-runtime
这个插件。
@babel/plugin-transform-runtime
此插件可以创建一个沙盒环境,解决上述问题。
首先安装它:
npm install --save-dev @babel/plugin-transform-runtime
同时还需要安装它的生产版本,因为它本身仅用于开发环境:
npm install --save @babel/runtime
修改 .babelrc
:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage"
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
注意 corejs
所在的位置,运行 npm run build
,编译的结果如下:
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));
require("regenerator-runtime/runtime");
var _symbol = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/symbol"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
var _fromEntries = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/object/from-entries"));
var _forEach = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/for-each"));
require("core-js/stable");
var entries = [['name', 'mike'], ['age', 22]];
(0, _forEach["default"])(entries).call(entries, function (item) {
console.log(item);
});
var object = (0, _fromEntries["default"])(entries);
console.log(object);
var arr = [2, 5, 8];
console.log((0, _includes["default"])(arr).call(arr, 5));
function asyncFunc() {
return _asyncFunc.apply(this, arguments);
}
function _asyncFunc() {
_asyncFunc = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
return _regenerator["default"].wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
console.log("async");
case 1:
case "end":
return _context.stop();
}
}
}, _callee);
}));
return _asyncFunc.apply(this, arguments);
}
var symbol = new _symbol["default"]();
可以看到,原本的辅助函数变成了一个方法引用,这样就解决了上述问题。
注意点
使用插件和预设时,有下列几点需要注意:
- 插件是在预设前运行的,如果将前例中
corejs
配置项改回到presets
中,得到的编译结果会不一样; - 插件的运行顺序是从前往后的;
- 预设的运行顺序是从后往前的。
在配置时需要留意,详情可以查阅官方文档。
结语
通常,如果没有特殊的需求,使用 @babel/preset-env
和 @babel/plugin-transform-runtime
就满足了,而且它们的配置也并不复杂。