Babel常用模块讲解
Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:
1、转换语法
2、通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)
3、源代码转换(codemods)
更多...
上面的内容是引用自babel@7.7.0 官方文档 。babel的用法想必大家都已经很熟悉了,文章主要针对其中几个比较常用的模块,通过简单的使用,阐述其 基本的使用方法 以及它们 在代码转换中所起的作用 。
内容较长,建议跟着讲解一步步实践。
准备工作
开始之前,先做一下准备工作。首先我们创建一个简单的工作目录,目录结构如下:
|——test
|——node_modules
|——src
|——index.js
|——.babelrc
|——package.json
然后我们在
src/index.js
中添加如下代码,都是我们经常使用的ES6中的新的语法和API:
// let声明 & 箭头函数
let fn = () => {
console.log(1)
// 实例方法
[2, 3, 4].includes(1)
// 静态方法
Object.assign({ a: 1, b: 2 }, { c: 3 });
// 新的API
let promise = new Promise((res, rej) => {
setTimeout(res, 1000)
let set = new Set()
let map = new Map()
// 新的语法
class Person {
static get name() {
return 'Person'
say() {
console.log('hello world!')
}
这里我们采用
cli
的方式来对上面的代码进行转换,所以需要先安装以下
dev依赖
:
@babel/cli
@babel/core
然后在
package.json
中添加如下命令,将
src
目录下的源文件转换后输出到
lib
目录下:
"scripts": {
"babel": "babel src --out-dir lib --watch"
}
最后说下,现在我们的
.babelrc
配置文件内容如下:
{}
没错,我们还没有做任何配置。
到这里,我们的准备工作已经都做好了,下面就开始介绍以下几个模块了。
@babel/preset-env
预设其实是Babel插件的组合,而
env
预设目前已经使用了一年多,并且现在已经完全替代了先前
babel
提出并建议的一些预设,比如:
babel-preset-es2015
babel-preset-es2016
babel-preset-es2017
babel-preset-latest
这意味着,不需要我们再去一个一个去手动引入多个插件,一个
@babel/preset-env
就可以cover大部分场景。
ok,现在我们把它引入到我们的项目中来。
使用
- 安装
yarn add @babel/preset-env -D
- 配置
{
"presets": [ "@babel/preset-env"]
}
-
执行
npm run babel
,查看转换结果:
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
// let声明 & 箭头函数
var fn = function fn() {
console.log(1);
// 实例方法
[2, 3, 4].includes(1);
// 静态方法
Object.assign({
a: 1,
// 新的API
var promise = new Promise(function (res, rej) {
setTimeout(res, 1000);
var set = new Set();
var map = new Map();
// 新的语法
var Person =
/*#__PURE__*/
function () {
function Person() {
_classCallCheck(this, Person);
_createClass(Person, [{
key: "say",
value: function say() {
console.log('hello world!');
}], [{
key: "name",
get: function get() {
return 'Person';
return Person;
}();
从转换结果可以看出,babel只转换了下面这三个地方:
- let → var
- 箭头函数语法
- class语法
为什么?
以下内容摘自 Babel学习系列4 【侵删】
Babel 把 Javascript 语法 分为Syntax
和API
1、API
:指那些我们可以通过 函数重新覆盖的语法 ,类似includes
/map
/includes
/Promise
凡是我们能想到重写的都可以归属到API
2、Syntax
:像let
/const
/class
/Arrow Function
/Decorators
等等这些,我们在 Javascript 在运行时无法重写的
而
Babel
默认只是转换
Syntax
层语法,所以需要
@babel/polyfill
来处理
API
兼容
除此之外
刚才我们说,一个
@babel/preset-env
基本上就可以满足我们的需求。但是,官方文档也说了,该预设并不支持
stage-x
相关的
plugin
。也就是说,如果我们在项目中使用了TC39制定的提案阶段的语法,该预设并不能帮我们转换。
It is important to note that @babel/preset-env does not support stage-x plugins.
比如,我们使用了stage-3阶段的 类的静态属性 提案语法:
class Person {
static Version = 1.0
}
如果我们不做其他额外配置,直接运行
npm run babel
,终端会报错:
从Babel v7开始,所有的stage预设都已经弃用了。想要转换上述语法,其实报错信息里提示了,就是需要我们安装一个叫
@babel/plugin-proposal-class-properties
的插件。
我们按照提示安装并配置:
{
"presets": [
"@babel/preset-env"
"plugins": ["@babel/plugin-proposal-class-properties"]
}
然后再次转换就可以成功了,结果如下:
var Person = function Person() {
_classCallCheck(this, Person);
_defineProperty(Person, "Version", 1.0);
配置
有关
@babel/preset-env
的相关配置,这里不多说,在后面介绍
@babel/polyfill
的时候,会着重讲一下
useBuiltIns
配置项,其他的请查看
官方文档
。
@babel/polyfill
上面也提到了
@babel/polyfill
这个模块是用来做什么的。接下来就看一下它的一些简单应用。不过用之前先说明一下:
As of Babel 7.4.0, this package has been deprecated in favor of directly includingcore-js/stable
(to polyfill ECMAScript features) andregenerator-runtime/runtime
(needed to use transpiled generator functions):
从Babel 7.4.0开始,官方就不再推荐使用此模块了,而是让用户按需引入下面这两个模块:
-
core-js/stable
(polyfill的核心,以充实ECMAScript功能) -
regenerator-runtime/runtime
(需要转译生成器函数)
import "core-js/stable";
import "regenerator-runtime/runtime";
其实这两个模块本身也是
@babel/polyfill
的依赖项。
使用
- 安装
需要安装为生产依赖 。因为这是一个运行时依赖,我们在代码运行的时候需要用到它。
yarn add @babel/polyfill
- 配置
@babel/polyfill
的配置方式有很多,不一一叙述,感兴趣的直接去看
官方文档
,写的很清楚。这里只介绍与
@babel/preset-env
配合使用的场景。
首先我们在入口文件中将
@babel/polyfill
引入进来:
import "@babel/polyfill";
// let声明 & 箭头函数
let fn = () => {
console.log(1)
// 下面的代码就不贴了
...
这里用到了
@babel/preset-env
模块的
useBuiltIns
配置项,现在就针对它的三种不同取值来看几个例子:
-
false
:这是默认值。
就是什么也不做:
-
不要为每个文件自动添加
polyfill
-
也不要将
import "core-js"
或import "@babel/polyfill"
转换为单独的polyfill
。
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "false"
"plugins": ["@babel/plugin-proposal-class-properties"]
}
执行
npm run babel
,查看转换结果:
"use strict";
require("@babel/polyfill");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
// let声明 & 箭头函数
var fn = function fn() {
console.log(1);
}; // 实例方法
[2, 3, 4].includes(1); // 静态方法
Object.assign({
a: 1,
}); // 新的API
var promise = new Promise(function (res, rej) {
setTimeout(res, 1000);
var set = new Set();
var map = new Map(); // 新的语法
var Person =
/*#__PURE__*/
function () {
function Person() {
_classCallCheck(this, Person);
_createClass(Person, [{
key: "say",
value: function say() {
console.log('hello world!');
}], [{
key: "name",
get: function get() {
return 'Person';
return Person;
_defineProperty(Person, "Version", 1.0);
可以看到,和一开始的转换结果并没有什么区别,的确什么也没做。
2.
entry
: 就是把全部
@babel/polyfill
全部一次性引入,如果配置了
target
属性,会引入满足
target
属性值的所有
polyfill
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "entry"
"plugins": ["@babel/plugin-proposal-class-properties"]
}
执行
npm run babel
,查看转换结果:
"use strict";
require("core-js/modules/es6.array.copy-within");
require("core-js/modules/es6.array.every");
// 内容太多,就省略了,大家可以自己尝试
require("core-js/modules/web.dom.iterable");
require("regenerator-runtime/runtime");
// let声明 & 箭头函数
var fn = function fn() {
console.log(1);
}; // 实例方法
[2, 3, 4].includes(1); // 静态方法
Object.assign({
a: 1,
}); // 新的API
var promise = new Promise(function (res, rej) {
setTimeout(res, 1000);
var set = new Set();
var map = new Map(); // 新的语法
var Person =
/*#__PURE__*/
function () {
function Person() {
_classCallCheck(this, Person);
_createClass(Person, [{
key: "say",
value: function say() {
console.log('hello world!');
}], [{
key: "name",
get: function get() {
return 'Person';
return Person;
_defineProperty(Person, "Version", 1.0);
可以看到,很多东西,不管我们用不用得到都给我们引入了进来,这肯定不是我们想要的,因为这会让我们项目变得很臃肿。
3.
usage
: 这个选项值实现了
polyfill
的按需加载,解决了咱们刚刚在
entry
部分提到的问题。
不过当我们按照下面的配置来运行时,终端会给我们提示:
意思就是,使用
usage
这个选项时,应当在入口文件中移除对
polyfill
的手动引入。当然你不移除也没什么影响,只不过这个提示看着会难受一些...
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage"
"plugins": ["@babel/plugin-proposal-class-properties"]
}
执行
npm run babel
,查看转换结果:
"use strict";
require("core-js/modules/es6.object.define-property");
require("core-js/modules/es6.map");
require("core-js/modules/web.dom.iterable");
require("core-js/modules/es6.array.iterator");
require("core-js/modules/es6.string.iterator");
require("core-js/modules/es6.set");
require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
require("core-js/modules/es6.object.assign");
require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.string.includes");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
// let声明 & 箭头函数
var fn = function fn() {
console.log(1);
}; // 实例方法
[2, 3, 4].includes(1); // 静态方法
Object.assign({
a: 1,
}); // 新的API
var promise = new Promise(function (res, rej) {
setTimeout(res, 1000);
var set = new Set();
var map = new Map(); // 新的语法
var Person =
/*#__PURE__*/
function () {
function Person() {
_classCallCheck(this, Person);
_createClass(Person, [{
key: "say",
value: function say() {
console.log('hello world!');
}], [{
key: "name",
get: function get() {
return 'Person';
return Person;
_defineProperty(Person, "Version", 1.0);
这次的结果看起来干净了很多,不过,我明明只用了Array实例的includes方法,为啥String实例上的includes方法的polyfill也引入了进来?
其实我们知道, js是一门动态语言,变量的类型只有在运行时才能确定,所以babel在编译阶段并不知道调用includes方法的实例究竟是一个Array实例还是一个String实例,所以只好根据方法名称(未验证)都引入进来了。
@babel/runtime
背景
介绍这个模块之前,咱们需要在
src
目录下新增一个
utils.js
文件,文件内容很简单:
export class Animal {
makeSound() {
console.log('ga~ga~ga~')
}
然后我们在
index.js
中把它文件引入进来:
import { Animal } from './utils'
// ...省略了原来的内容
class Duck extends Animal {}
let duck = new Duck()
duck.makeSound()
babel
配置我们还是沿用的上一节介绍
polyfill
时的
usage
配置,我们看一下此时编译出来的
utils.js
文件内容是什么样的:
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
exports.Animal = void 0;
require("core-js/modules/es6.object.define-property");
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
var Animal =
/*#__PURE__*/
function () {
function Animal() {
_classCallCheck(this, Animal);
_createClass(Animal, [{
key: "makeSound",
value: function makeSound() {
console.log('ga~ga~ga~');
return Animal;
exports.Animal = Animal;
可以看到,与之前
index.js
编译出的文件相比,这两个文件中编译
class
语法时,都使用了相同的辅助方法:
_createClass
、
_classCallCheck
。试想一下,如果我们项目中有一百个文件都用到了class语法,那这些辅助方法就要在每一个文件中出现一次...这显然是一个问题。
使用
那怎么解决这个问题呢?很简单,就是把这些通用的方法提取出来,单独维护成一个公用的模块,而这个模块就是
@babel/runtime
。迫不及待要实践一番了。不过,别着急,这个模块需要和接下来要介绍的
@babel/plugin-transform-runtime
模块配合使用。
@babel/plugin-transform-runtime
使用
刚才已经介绍过背景了,这里我们直接介绍使用:
- 安装
yarn add @babel/plugin-transform-runtime -D
yarn add @babel/runtime
- 配置
{
"presets": [
"@babel/preset-env",
"useBuiltIns": "usage"
"plugins": [
"@babel/plugin-proposal-class-properties",
["@babel/plugin-transform-runtime"]
}
-
执行
npm run babel
,查看转换结果
// index.js
"use strict";
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
// utils.js
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
可以看到相同的辅助方法,已经不在每个文件中单独定义了,而是从
@babel/runtime/helpers
中统一引入。
作用域隔离
看到这里,貌似一切问题都已经得到了很好的解决:
- 转译新的语法、API
- polyfill按需加载
- 用到的辅助方法统一引入
- ...
无论是从代码的兼容性还是代码的体积上都进行了精心的优化,可以说,当前的解决方案已经可以满足大部分项目的需求场景了。但如果我们开发的是一个工具包,未来需要提供给其他人使用的话,做到这样是完全不够的。
If you directly importcore-js
or@babel/polyfill
and the built-ins it provides such asPromise
,Set
andMap
, those will pollute the global scope .
回想一下我们之前在介绍
@babel/polyfill
中的示例,我们在
index.js
中使用了
Promise
这个API,而转译过后的文件中,为了兼容,babel为我们引入了它的polyfill实现:
require("core-js/modules/es6.promise");
这个实现是作用在全局作用域下的。如果某人在项目中使用了你提供的工具包,而他也自定义了一个全局Promise,那后果可想而知。
如何解决
其实
@babel/plugin-transform-runtime
这个插件,除了我们上面提到的作用之外,还有一个功能就是:为你的代码创建一个沙盒环境。
Another purpose of this transformer is to create a sandboxed environment for your code.
这个插件可以接收很多配置参数,具体的内容可以查看
官方文档
。我们这里只说即将用到的
corejs
这个配置项。
可选值与需要安装的模块
① false :npm install --save @babel/runtime
② 2 :npm install --save @babel/runtime-corejs2
③ 3 :npm install --save @babel/runtime-corejs3
看上面的表格可以很清楚的知道,这个配置项接受三个值。值不同,需要配合安装的模块也不同。
-
false
当使用这个选项值时,
@babel/runtime
只提供共用的帮助方法,高级语法和API仍然需要安装
@babel/polyfill
这个生产依赖来转译。
// ====================== .babelrc ====================== //
"presets": [
"@babel/preset-env",
"useBuiltIns": "usage"
"plugins": [
"@babel/plugin-proposal-class-properties",
["@babel/plugin-transform-runtime", {
"corejs": false
// ====================== lib/index.js ====================== //
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
require("core-js/modules/es6.map");
require("core-js/modules/web.dom.iterable");
require("core-js/modules/es6.array.iterator");
require("core-js/modules/es6.string.iterator");
require("core-js/modules/es6.set");
require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
require("core-js/modules/es6.object.assign");
require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.string.includes");
// .... 省略了源代码转译结果部分
-
2
使用这个选项值,需要手动安装
@babel/runtime-corejs2
为生产依赖,而
@babel/runtime
这个依赖就可以去掉了。因为后者的功能前者已经包含了。
而且,除此之外,
@babel/runtime-corejs2
这个模块还包含了部分
polyfill
的功能,这部分功能是
除实例上的方法(如:
'hello world'.includes('llo')
)之外
的其他功能 。这点很重要,因为这也是
core-js@2
与
core-js@3
两个版本的主要不同。
// ====================== .babelrc ====================== //
"presets": [
"@babel/preset-env",
"useBuiltIns": "usage"
"plugins": [
"@babel/plugin-proposal-class-properties",
["@babel/plugin-transform-runtime", {
"corejs": 2
// ====================== lib/index.js ====================== //
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/inherits"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty"));
var _map = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/map"));
var _set = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/set"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));
var _assign = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/assign"));
require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.string.includes");
// .... 省略了源代码转译结果部分
从结果中我们也可以看出来,像源代码中使用的
Map
/
Set
/
Object.assign
/
Promise
等的
polyfill
都已经引用自
@babel/runtime-corejs2
这个模块;而实例上的方法,如
includes
仍然引用自
@babel/polyfill
模块。
-
3
与
2
这个选项值类似,
3
需要安装手动安装
@babel/runtime-corejs3
为生产依赖。而这两者的最大不同就是我们刚才说的,
3
这个版本提供了实例上方法的
polyfill
。这意味着什么?意味着,我们现在连
@babel/polyfill
这个模块也可以去掉了。一个
@babel/runtime-corejs3
即可解决这一切问题。
// ====================== .babelrc ====================== //
"presets": [
"@babel/preset-env"
"plugins": [
"@babel/plugin-proposal-class-properties",
["@babel/plugin-transform-runtime", {
"corejs": 3
// ====================== lib/index.js ====================== //
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/inherits"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/defineProperty"));
var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/map"));
var _set = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set"));
var _setTimeout2 = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set-timeout"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));