什么是Babel?
Babel 是一个工具链,主要用于在旧的浏览器或环境中将 ECMAScript 2015+ 代码转换为向后兼容版本的 JavaScript 代码。
主要做的事情:
- 语法转换
- 实现目标环境缺少的功能(es2015+)
- 源代码转换 (codemods)
- 还有更多!(点开这些视频看看)
用法
在这里会介绍如何将用es2015+写的JavaScript代码转换为能在当前浏览器正常执行的代码。包括两方面:语法转换、功能补充(这里暂时叫这个名字,之后会相信介绍)。
- 安装这些必要的包
npm install --save-dev @babel/core @babel/cli @babel/preset-env
npm install --save @babel/polyfill
- 在根目录创建一个babel.config.js的配置文件:
const presets = [
[
"@babel/env",
{
targets: {
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1",
},
useBuiltIns: "usage",
},
],
];
module.exports = { presets };
- targets:表示编译出的代码想要支持的浏览器版本
- useBuiltIns:之后详细解释
- 执行命令
./node_modules/.bin/babel src --out-dir lib
然后你用es2015+编写的代码就被转化为能在目标浏览器正常运行的代码了。
如何运行的?
所有你用到的babel包都是被单独发布在@babel作用域下(v7开始),比如@babel/preset-env、@babel/core、 @babel/cli,因为babel是插拔式的,所以用到什么安装什么,每个包各司其职。
@babel/core
其中最核心的包就是@babel/core,它主要的作用就是编译:
npm install --save-dev @babel/core
然后你可以在代码里直接使用:
const babel = require("@babel/core");
babel.transform("code", optionsObject);
这里的optionsObject就和之前的babel.config.js是一样的,如何编译代码,编译成什么样子什么标准用什么东西都在这里配置。
@babel/cli
为什么我们能在命令行里直接使用:
./node_modules/.bin/babel src --out-dir lib
光有core是无法在命令行使用这些功能的,@babel/cli支持你直接在命令行中编译代码。
这句话会编译你src目录下的所有js代码,并编译成你想要的那样(babel.config.js配置的),并输出到lib目录下。
如果我们没有配置babel.config.js,那么执行这句话之后src会被原封不动的搬到lib下(格式除外)。
--out-dir 代表输出到哪个目录下,你可试试--help看其他的用法,如果在这里我们没有配置babel.config.js,我们可以通过--plugins 或者 --presets告诉 代码应该编译成什么样子。
Plugins & Presets
plugins顾名思义就是组件,一个小型的js代码程序告诉Babel
如何转换你的源码,你可以自己写plugins也可以在github上使用别人写好的。来看如何使用一个插件:@babel/plugin-transform-arrow-functions
:
npm install --save-dev @babel/plugin-transform-arrow-functions
./node_modules/.bin/babel src --out-dir lib --plugins=@babel/plugin-transform-arrow-functions
@babel/plugin-transform-arrow-functions
组件的作用就是将es2015的箭头函数转换成普通函数:
// src/foo.js:
const fn = () => 1;
// converted to
// lib/foo.js:
var fn = function fn() {
return 1;
};
当然,es2015有这么多新的语法,我们不可能一一的去引用每个plugins来编译我们的代码吧,于是就又了presets,顾名思义——预设,它包含了一组我们需要的plugins。就像plugin一样,你也可以编写一组你最需要的plugins成为一个preset。
目前这里有一个非常优秀的preset叫env —— @babel/preset-env。
npm install --save-dev @babel/preset-env
./node_modules/.bin/babel src --out-dir lib --presets=@babel/env
不需要任何配置,这个preset包含了所有现代js(es2015 es2016等)的所有新特性,你也可以传递一些配置给env,精准实现你想要的编译效果。
配置
更具你的需求,配置肯定是不一样的,这里贴一个官方推荐配置:
const presets = [
[
"@babel/env",
{
targets: {
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1",
},
},
],
];
module.exports = { presets };
这个配置只配置了prsets,其实还可以配置plugins。
Polyfill
中文翻译是垫片,之前没有详细了解babel之前,我也很迷茫这个polyfill是啥,因为语法不都给你转换好了,还需要这个东西干啥,后来仔细想了一下,要适应新特性应该从两方面入手:
- 语法转换:
() => {};
for (let i of items) {};
比如箭头函数、for...of,在不支持这些语法的环境下,直接会报语法错误,因为编译器根本不知道 =>
这些是什么鬼符号,要做到让编译器识别,那就要将这样的语法转换成浏览器能识别的代码,那么就需要语法转换。
然后这里回到我们最开始安装包那里:
npm install --save-dev @babel/core @babel/cli @babel/preset-env
npm install --save @babel/polyfill
仔细看我们安装core cli env都是 --save-dev,这是因为我们发布的代码都是编译好的代码,这些都只是开发依赖,发布的代码不需要依赖这些包。
- 功能补充
'foo'.includes('f');
es2015里不仅只有新的语法,还有实例的扩展,比如String,其实这里只是调用了String实例的一个方法,我们无论怎么语法转换也没有什么用吧,如果我们在不支持String.prototype.includes的编译器里跑这些代码,会得到 'foo'.includes is not a function. 这样的一个报错,而不是语法报错。
Polyfill提供的就是一个这样功能的补充,实现了Array、Object等上的新方法,实现了Promise、Symbol这样的新Class等。到这里应该能明白了,为什么安装@babel/polyfill
没有-dev,因为就算代码发布后,编译后的代码依然会依赖这些新特性来实现功能。
虽然@babel/polyfill提供了我们想要的所有新方法新类,但是这里依然存在一些问题:
- 体积太大:比如我只用了String的新特性,但是我把整个包都引进来了,,这不是徒增了很多无用的代码。
- 污染全局环境:如果你引用了
@babel/polyfill
,那么像Promise这样的新类就是挂载在全局上的,这样就会污染了全局命名空间。可能在一个团建建立的项目问题不太大,但是如果你是一个工具的开发者,你把全局环境污染了,别人用你的工具,就有可能把别人给坑了。
一个解决方案就是引入transform runtime 来替代 @babel/polyfill
。
幸运的是,我们有env这个preset,它又一个useBuiltIns选项,如果设置成"usage"
,那么将会自动检测语法帮你require你代码中使用到的功能。
const presets = [
[
"@babel/env",
{
targets: {
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1",
},
useBuiltIns: "usage",
},
],
];
module.exports = { presets };
比如我在代码中:
Promise.resolve().finally();
如果在edge17不支持这个特性的环境里运行,将会帮你编译成:
require("core-js/modules/es.promise.finally");
Promise.resolve().finally();
@babel/plugin-transform-runtime
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
主要功能:
- 避免多次编译出helper函数:
Babel转移后的代码想要实现和原来代码一样的功能需要借助一些帮助函数,比如:
class Person {}
会被转换为:
"use strict";
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Person = function Person() {
_classCallCheck(this, Person);
};
这里_classCallCheck
就是一个helper函数,试想下如果我们很多文件里都声明了类,那么就会产生很多个这样的helper函数,积少成多增大了代码体积。
这里的@babel/runtime
包就声明了所有需要用到的帮助函数,而@babel/plugin-transform-runtime
的作用就是将所有需要helper函数的文件,依赖@babel/runtime
包:
"use strict";
var _classCallCheck2 = require("@babel/runtime/helpers/classCallCheck");
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
var Person = function Person() {
(0, _classCallCheck3.default)(this, Person);
};
这里就没有再编译出helper函数classCallCheck了,而是直接引用了@babel/runtime
中的helpers/classCallCheck。
- 解决
@babel/polyfill
提供的类或者实例方法污染全局作用域的情况。
@babel/plugin-transform-runtime
会为代码创建一个沙盒环境,为core-js
这里内建的实例提供假名,你可以无缝的使用这些新特性,而不需要使用require polyfill。
"foobar".includes("foo")
,这样的实例方法仍然是不能正常执行的,因为他在挂载在String.prototype上的,如果需要使用这样的实例方法,还是不得不require('@babel/polyfill')
比如:
var sym = Symbol();
var promise = new Promise();
console.log(arr[Symbol.iterator]());
会被编译成:
"use strict";
var _getIterator2 = require("@babel/runtime-corejs2/core-js/get-iterator");
var _getIterator3 = _interopRequireDefault(_getIterator2);
var _promise = require("@babel/runtime-corejs2/core-js/promise");
var _promise2 = _interopRequireDefault(_promise);
var _symbol = require("@babel/runtime-corejs2/core-js/symbol");
var _symbol2 = _interopRequireDefault(_symbol);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
var sym = (0, _symbol2.default)();
var promise = new _promise2.default();
console.log((0, _getIterator3.default)(arr));
这样像Symbol、Promise这样的新类也不会污染全局环境了。
用法:
配置在配置文件里,以.babelrc
举例:
无选项配置:
{
"plugins": ["@babel/plugin-transform-runtime"]
}
有选项配置(默认值):
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
这个插件的默认配置默认用户已经提供了所有polyfillable APIs,因此想要无缝使用不污染全局环境的内建功能需要特别标明corejs。
可选项:
corejs
:默认false,或者数字:{ corejs: 2 }
,代表需要使用corejs的版本。helpers
:默认是true,是否替换helpers。polyfill
:v7无该属性regenerator
:默认true,generator是否被转译成用regenerator runtime包装不污染全局作用域的代码。useESModules
:默认false,如果是true将不会用@babel/plugin-transform-modules-commonjs
进行转译,这样会减小打包体积,因为不需要保持语义。false:
exports.__esModule = true;
exports.default = function(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
- true:
export default function(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
babel-polyfill vs babel-runtime
以下出自:github -了解Babel 6生态
babel-polyfill
和 babel-runtime
是达成同一种功能(模拟ES2015环境,包括global keywords,prototype methods,都基于core-js提供的一组polyfill和一个generator runtime)的两种方式:
babel-polyfill通过向全局对象和内置对象的prototype上添加方法来达成目的。这意味着你一旦引入babel-polyfill,像Map,Array.prototype.find这些就已经存在了——全局空间被污染。
babel-runtime不会污染全局空间和内置对象原型。事实上babel-runtime是一个模块,你可以把它作为依赖来达成ES2015的支持。
比如当前环境不支持Promise,你可以通过require(‘babel-runtime/core-js/promise’)来获取Promise。这很有用但不方便。幸运的是,babel-runtime并不是设计来直接使用的——它是和babel-plugin-transform-runtime一起使用的。babel-plugin-transform-runtime会自动重写你使用Promise的代码,转换为使用babel-runtime导出(export)的Promise-like对象。
注意: 所以plugin-transform-runtime一般用于开发(devDependencies),而runtime自身用于部署的代码(dependencies),两者配合来一起工作。
那么我们什么时候用babel-polyfill,什么时候用babel-runtime?
babel-polyfill会污染全局空间,并可能导致不同版本间的冲突,而babel-runtime不会。从这点看应该用babel-runtime。
但记住,babel-runtime有个缺点,它不模拟实例方法,即内置对象原型上的方法,所以类似Array.prototype.find,你通过babel-runtime是无法使用的。
最后,请不要一次引入全部的polyfills(如require('babel-polyfill')),这会导致代码量很大。请按需引用最好。
@babel/preset-env
以下出自:github -Babel 7 及新用法
preset-env
是 JS 中的 autoprefixer
根据环境来应用不同的plugins。支持的plugins超过babel-preset-latest(2015-2017)。
用法:
{
"presets": [
[
"env",
{
"targets": { // 目标环境
"browsers": [ // 浏览器
"last 2 versions",
"ie >= 10"
],
"node": "current" // node
},
"modules": true, // 是否转译module syntax,默认是 commonjs
"debug": true, // 是否输出启用的plugins列表
"spec": false, // 是否允许more spec compliant,但可能转译出的代码更慢
"loose": false, // 是否允许生成更简单es5的代码,但可能不那么完全符合ES6语义
"useBuiltIns": false, // 怎么运用 polyfill
"include": [], // 总是启用的 plugins
"exclude": [], // 强制不启用的 plugins
"forceAllTransforms": false, // 强制使用所有的plugins,用于只能支持ES5的uglify可以正确压缩代码
}
]
],
}
这里主要需要注意的是useBuiltIns用于指定怎么处理polyfill,可以选值"usage" | "entry" | false
,默认是false
。
-
useBuiltIns: 'usage'
:当每个文件里用到(需要polyfill的特性)时,在文件中添加特定的import语句。这可以保证每个polyfill的特性仅load一次。
/// input
var a = new Promise(); // a.js
var b = new Map(); // b.js
/// output
// a.js
import "core-js/modules/es6.promise";
var a = new Promise();
// b.js
import "core-js/modules/es6.map";
var b = new Map();
-
useBuiltIns: 'entry'
:替换import "@babel/polyfill" / require("@babel/polyfill")
语句为独立的(根据环境)需要引入的polyfill特性的import语句。
// input
import "@babel/polyfill";
// output
import "core-js/modules/es7.string.pad-start";
import "core-js/modules/es7.string.pad-end";
需要注意,在整个项目中,"@babel/polyfill"只能require一次,否则报错。建议用独立的entry文件引入。
-
useBuiltIns: false
:对@babel/polyfill
不做任何处理。
ReferenceError: regeneratorRuntime is not defined
需要注意,当你使用async/await
并被preset-env转译后,运行时可能会出现以上错误,这是因为:
plugin-transform-regenerator
使用regenerator
来转译 async/generator
函数,但是它本身不包括regeneratorRuntime
,你需要使用babel-polyfill/regenerator runtime
来使regeneratorRuntime 存在。
通常情况下,加上transform-runtime plugin即可。
参考文献:
- 了解 Babel 6 & 7 生态
- transform-runtime 会自动应用 polyfill,即便没有使用 babel-polyfill
- babel官方文档(usage、@babel/polyfill、@babel/plugin-transform-runtime、@babel/preset-env)