Babel,已经是每个项目都必不可少的依赖了。不过大多数同学可能并没有自己配置 Babel 的经验和机会
其实 Babel 配置并没有很难,只要了解了配置文件中的几个参数和所需依赖的作用,基本就等于完全掌握了 Babel 的使用
一、启动 Babel
Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中
简单说就是源码通过 Babel 处理后会得到向后兼容的代码。我们可以按照这个逻辑写出最简单的 Babel 用例
// 安装 babel 核心库
yarn add @babel/core
const babel = require('@babel/core')
babel.transform(`
const fn=()=>console.log(1);
fn();
`, {}, (err, result) => {
console.log(result.code);
});
使用 Node.js 执行这个 js 文件之后得到的输出如下
const fn = () => console.log(1);
fn();
可以发现待转化的源码没有任何变化。因为 @babel/core
的作用只是用来解析源码、把 js 代码分析成 ast,方便各个插件分析语法进行相应处理
而且我们实际使用过程中也不会直接这么用,一般会基于构建工具做一层封装,例如使用 webpack 时的 babel-loader
为了后续调用 babel 时更加直观,我们使用相应的 cli 库 @babel/cli
yarn add @bable/cli
之后随便新建个 js 文件
// src/index.js
const fn = () => console.log(1);
fn();
然后在根目录里执行命令即可
./node_modules/.bin/babel src --out-dir lib
参数的含义是把 src
里的代码用 @babel/core
处理并输出到 lib
文件夹中,所以执行成功我们可以看到输出后的代码
当然这样的执行方式太丑陋了,我们选择在 package.json
里添加 scripts
{
...
"scripts": {
"babel":"babel src --out-dir lib"
},
...
}
yarn babel
由于只是换了一种执行方式,所以输出的 js 当然还是和之前一样,不会有任何转换
// lib/index.js
const fn = () => console.log(1);
fn();
二、@babel/preset-env
2.1 Plugins 插件
@babel/core
我们已经测试过,不会转化任何源码。所以为了能正确转化,我们必须使用对应的 plugins
Babel 推崇功能的单一性,就是每个插件的功能尽可能的单一。比如想使用 ES6 的箭头函数,需要对应的转化插件
yarn add @babel/plugin-transform-arrow-functions
再执行 babel
yarn babel --plugins=@babel/plugin-transform-arrow-functions
终于得到了转化后的代码
// lib/index.js
const fn = function () {
return console.log(1);
};
fn();
不过这么写太丑了,我们直接在根目录新建一个 babel.config.js
// babel.config.js
module.exports = {
plugins: [
"@babel/plugin-transform-arrow-functions"
]
};
执行命令,Babel 会自动找到根目录中的 config 文件并使用。得到的转化文件和之前一样
yarn babel
2.2 Presets 预设
转化箭头函数需要一个 Plugin,而 const 需要另外的。我们不可能一个个的设置所有的 Plugin
这时候就需要 Presets,可以简单理解为它是一堆 Plugin 的组合。常见的 Preset 如下:(前两个已弃用,了解一下即可)
2.2.1 @babel/preset-stage-xxx
@babel/preset-stage-xxx
是 ES 在不同阶段语法提案的转码规则而产生的预设,随着被批准为 ES 新版本的组成部分而进行相应的改变(例如 ES6/ES2015)
提案分为以下几个阶段:
- stage-0 - 设想(Strawman):只是一个想法,可能有 Babel 插件,stage-0 的功能范围最广大,包含stage-1 , stage-2 以及 stage-3 的所有功能
- stage-1 - 建议(Proposal):这是值得跟进的
- stage-2 - 草案(Draft):初始规范
- stage-3 - 候选(Candidate):完成规范并在浏览器上初步实现
- stage-4 - 完成(Finished):将添加到下一个年度版本发布中
2.2.2 @babel/preset-es2015
preset-es2015 是仅包含 ES6 功能的 Babel 预设
实际上在 Babel7 出来后上面提到的这些预设 stage-x,preset-es2015 都可以废弃了,因为 @babel/preset-env
出来一统江湖了
2.2.3 @babel/preset-env
前面两个预设是从 ES 标准的维度来确定转码规则的,而 @babel/preset-env 是根据浏览器的不同版本中缺失的功能确定代码转换规则的,在配置的时候我们只用配置需要支持的浏览器版本就好了,@babel/preset-env 会根据目标浏览器生成对应的插件列表然后进行编译:
// babel.config.js
module.exports = {
presets: [
["@babel/preset-env", {
targets: {
browsers: ["last 10 versions", "ie >= 9"]
}
}],
],
// plugins: ["@babel/plugin-transform-arrow-functions"]
};
执行命令之后,会得到箭头函数和 const 都被转化过的代码
//lib/index.js
"use strict";
var fn = function fn() {
return console.log(1);
};
fn();
2.2.4 @babel/preset-react
用来转化 jsx https://babeljs.io/docs/en/babel-preset-react
2.2.5 @babel/preset-typescript
用来转化 ts https://babeljs.io/docs/en/babel-preset-typescript
2.3 执行顺序
插件的排列顺序很重要。
如果两个转换插件都将处理源码的某个代码片段时,转化将根据 Plugins 或 Presets 的排列顺序依次执行
- Plugins 在 Presets 前运行
- Plugin 顺序从前往后排列
- Preset 顺序是颠倒的(从后往前)
三、@babel/polyfill
看起来我们的 babel 配置好像已经比较完善了。我们在源码上再加两行代码并转化试试
// src/index.js
const fn = () => console.log(1);
fn();
+ const pro = new Promise()
+ const isIncludes = [1, 2, 3].includes(2)
//lib/index.js
"use strict";
var fn = function fn() {
return console.log(1);
};
fn();
var pro = new Promise();
var isIncludes = [1, 2, 3].includes(2);
可以看到 Promise 和 includes 均没有被转化
有理由怀疑可能是 targets 设置过大,不过实际上就算调整到 ie 6,新加的两行代码也不会被转化
3.1 syntax 与 api
这是由于 Babel 把 Javascript 语法 分为 syntax 和 api
syntax 句法
类似箭头函数、let、const、class 等在 JavaScript 运行时无法重写的部分,就是 syntax 句法
api 方法
类似 Promise、includes 等可以通过函数重新覆盖的语法都可以归类为 api 方法。而且方法本身还分为两类
- 方法 - Promise,Object.entries 等可以直接调用的
- 实例方法 -
[1, 2, 3].includes
等绑定在实例上的
而之前我们的源码部分只转化了 syntax 部分。 api 部分则需要借助另外的库
3.2 core-js 与 regenerator-runtime/runtime
转化 api 的思路很简单。在全局上下文下添加一个同名 api 即可
这些 api 都在 @babel/polyfill
中,所以先安装
yarn add @babel/polyfill
修改配置如下并转化源码(配置后续再说明)
// babel.config.js
module.exports = {
"presets": [
["@babel/preset-env", {
+ useBuiltIns: "usage",
+ corejs: 3,
targets: {
browsers: ["last 10 versions", "ie >= 9"]
}
}],
],
// plugins: ["@babel/plugin-transform-arrow-functions"]
};
可以发现我们的代码已经引入了 promise 和 includes 相关的垫片了
// lib/index.js
"use strict";
require("core-js/modules/es.array.includes");
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
// src/index.js
var fn = function fn() {
return console.log(1);
};
fn();
var pro = new Promise();
var isIncludes = [1, 2, 3].includes(2);
如果使用 await/async 语法
// src/index.js
const asyncFn = async () => { }
会发现在转化句法的同时还引入了 regenerator-runtime 库
// dist/index.js
"use strict";
require("core-js/modules/es.object.to-string");
require("core-js/modules/es.promise");
require("regenerator-runtime/runtime");
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); }); }; }
// src/index.js
var asyncFn = /*#__PURE__*/function () {
var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
case "end":
return _context.stop();
}
}
}, _callee);
}));
return function asyncFn() {
return _ref.apply(this, arguments);
};
}();
其实 @babel/polyfill 本质就是集成了这两个库
core-js
core-js 是用于 JavaScript 的组合式标准化库,它包含 es5 (e.g: object.freeze), es6 的 promise,symbols, collections, iterators, typed arrays, es7+提案等等的 polyfills 实现。也就是说,它几乎包含了所有 JavaScript 最新标准的垫片
// 比如,只不过需要单个引用
require('core-js/array/reduce');
require('core-js/object/values');
regenerator-runtime/runtime
它是来自于 facebook 的一个库,链接。主要就是实现了 generator/yeild, async/await。
所以 babel-runtime 是单纯的实现了 core-js 和 regenerator 引入和导出,比如这里是 filter 函数的定义,做了一个中转并处理了 esModule 的兼容。
3.3 Options
// babel.config.js
module.exports = {
"presets": [
["@babel/preset-env", {
useBuiltIns: "usage",
corejs: 3,
targets: {
browsers: ["last 10 versions", "ie >= 9"]
}
}],
],
// plugins: ["@babel/plugin-transform-arrow-functions"]
};
关于 preset-env 的几个常用配置如下
corejs
指定 core-js 库的版本。除非是有历史遗留的项目,否则使用官方推荐的就行,目前是 3
useBuiltIns
最重要的一个配置项,一共有 3 个值 false、usage 和 entry
- false - 默认值。不转化 api,只转化 syntax
- usage - 转化源码里用到的 api
- entry - 转化所有的 api
看起来好像 usage 吊打 entry,其实不然。
真实开发中,我们通常会引用各种第三方库,并且为了编译速度,一般都配置了规则,不会让 babel 处理这些库。
但是第三方库质量良莠不齐,保不齐哪个库里的代码就没做好兼容性转化。一旦特定浏览器运行到这部分代码,轻则报 error,重则页面白屏
所以要是对第三方库没有完善的管理机制,还是使用 entry 更保险
使用 entry 时必须先手动引入 core-js 和 regenerator-runtime/runtime(在以前引入 @babel/polyfill 这个集成库即可,最新版 babel 弃用了这个设定,必须直接引入两个库)
// src/index.js
import 'core-js'
import 'regenerator-runtime/runtime'
const fn = () => console.log(1);
fn();
const pro = new Promise()
const isIncludes = [1, 2, 3].includes(2)
// dist/index.js
"use strict";
require("core-js/modules/es.symbol");
require("core-js/modules/es.symbol.description");
// ... 省略 500+ 行 require
require('regenerator-runtime/runtime');
var fn = function fn() {
return console.log(1);
};
fn();
var pro = new Promise();
var isIncludes = [1, 2, 3].includes(2);
targets
当然直接引入也有很大缺点,由于需要转化的 api 过多,会极大增加打包后的代码体积。因此一定要配合 targets 参数使用,这样 babel 只会转化指定浏览器版本所需要的 api
modules
引入 api 的方式,默认值为 auto
。设置为 false
的话 api 引入方式会变成 import
其他参数见官方文档:https://babeljs.io/docs/en/babel-preset-env#shippedproposals
四、@babel/runtime
现在想象我们开发的不是一个普通项目,而是一个工具库。
有一天,一个同学引用了我们的工具库,本来这个同学的项目经过改写原 api,每次调用 promise 方法时都会发送一个埋点
可是在使用我们的工具库之后,由于工具库里全局引入了 core-js,导致各个 api 都被 core-js 里提供的方法全局覆盖以至于污染了。因此埋点方法彻底失效了!
显然这是不合理的,因此对于类库、工具库中的 api 转化,我们可以选择全局覆盖之外的另一种方法:api 替换
先安装所需要的相关依赖
yarn add @babel/runtime @babel/plugin-transform-runtime @babel/runtime-corejs3
再修改配置(修改的是 plugins 而不是 presets)
// babel.config.js
module.exports = {
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: 3,
},
]
]
};
源码
// src/index.js
const fn = () => console.log(1);
fn();
const pro = new Promise()
const isIncludes = [1, 2, 3].includes(2)
转化后代码
// dis/index.js
import _includesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/includes";
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
var _context;
// src/index.js
const fn = () => console.log(1);
fn();
const pro = new _Promise();
const isIncludes = _includesInstanceProperty(_context = [1, 2, 3]).call(_context, 2);
这样需要转化的 api 使用的就是 @babel/runtime-corejs3 提供的方法,且不会污染全局 api
当然转化后的代码还有几个其他问题,我们依次来解决
4.1 syntax
源码并没有转化句法,因此还是需要使用 @babel/preset-env 来转化句法。(记得设置 useBuiltIns 为 false,以保证不使用 polyfill 来转化 api,不过不设置的情况下也会优先使用 runtime)
// babel.config.js
module.exports = {
"presets": [
["@babel/preset-env", {
useBuiltIns: false,
targets: {
browsers: ["last 10 versions", "ie >= 9"]
}
}],
],
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: 3,
},
]
]
};
转化后代码
// dist/index.js
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var _context;
// src/index.js
var fn = function fn() {
return console.log(1);
};
fn();
var pro = new _promise["default"]();
var isIncludes = (0, _includes["default"])(_context = [1, 2, 3]).call(_context, 2);
可以看到 syntax 也完全转化了
4.2 target
有个坑点是 @babel/plugin-transform-runtime 目前不支持设置 target,也就意味着所有能被转化的 api 都会被转化。不过好像有人正在帮官方增加这个配置
4.3 实例方法
在之前我们提到过 api 有两种,一种是像 Promise 这样可以直接使用的,一种是挂载在实例上的。在 corejs 2 时期,runtime 是无法转化实例方法的。
因为 JavaScript 实在太灵活了,以至于编译阶段 Babel 根本无法分析实例到底是什么
举个极端例子
// src/index.js
function IncludesMock() { }
IncludesMock.prototype.includes = function (val) {
return 'faker includes: ' + val
}
const randomVal = Math.random(1)
// 只有在运行时才知道 dontKnow 到底是 Array 实例 or IncludesMock 实例
const dontKnow = randomVal <= 0.5 ? [1, 2, 3] : new IncludesMock()
console.log(randomVal);
console.log(dontKnow.includes(1));
不过 corejs 3 的现在已经可以处理这个问题了
// dist/index.js
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
function includesMock() {}
includesMock.prototype.includes = function (val) {
return 'faker includes: ' + val;
};
var randomVal = Math.random(1);
var dontKnow = randomVal <= 0.5 ? [1, 2, 3] : new includesMock();
console.log(randomVal);
console.log((0, _includes["default"])(dontKnow).call(dontKnow, 1));
使用 Node.js 直接执行 lib/index.js 的两种输出
0.19043928592202097
true
0.7307718322750261
faker includes: 1
推测应该是只要获取到 'includes'
关键字就会触发转化的逻辑
4.4 特例
再变态一点
// src/index.js
function IncludesMock() { }
IncludesMock.prototype.includes = function (val) {
return 'faker includes: ' + val
}
IncludesMock.prototype.pop = function () {
return 'faker pop'
}
const randomVal = Math.random(1)
const dontKnow = randomVal <= 0.5 ? [1, 2, 3] : new IncludesMock()
const randomVal2 = Math.random(1)
const randomApi = randomVal2 <= 0.5 ? 'includes' : 'pop'
console.log(randomVal);
console.log(dontKnow[randomApi](1));
// dist/index.js
"use strict";
function IncludesMock() {}
IncludesMock.prototype.includes = function (val) {
return 'faker includes: ' + val;
};
IncludesMock.prototype.pop = function () {
return 'faker pop';
};
var randomVal = Math.random(1);
var dontKnow = randomVal <= 0.5 ? [1, 2, 3] : new IncludesMock();
var randomVal2 = Math.random(1);
var randomApi = randomVal2 <= 0.5 ? 'includes' : 'pop';
console.log(randomVal);
console.log(dontKnow[randomApi](1));
可以发现 runtime 已经无能为力了,动态语言就是这么无法预测。不过正常人应该不会这么写代码,所以提醒自己注意,使用 api 的时候不要太非主流即可
其他参数见官方文档:https://babeljs.io/docs/en/babel-plugin-transform-runtime#docsNav
到这里,你因该已经完全了解了该如何配置 Babel,可喜可贺~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。