阅读 150

一步一步带你认识Babel7

1. 什么是Babel

我们知道,各个浏览器对 JavaScript 版本的支持度各不相同,有很多优秀的新语法都不能直接在浏览器中运行。为了解决这个“沟通不畅”的问题,所以就有了 Babel。Babel是一个工具集,主要用于将ES6版本的JavaScript代码转为ES5等向后兼容的JS代码,从而可以运行在低版本浏览器或其它环境中。因此,在编码过程中,可以使用ES6/7/8编码,然后使用Babel将代码编译为向后兼容的Javascript代码,这样就不用担心所在环境是否支持了。

简单来说,Babel的工作就是:

  • 语法转换
  • 通过 Polyfill 的方式在目标环境中添加缺失的特性
  • Javascript源码转换

2. Babel的基本原理

Babel的原理很简单,首先是将源码转成抽象语法树(AST),然后对语法树进行处理生成新的预发树,最后将新语法树生成新的Javascript代码,整个编译过程分为parsing(解析)、transforming(转换)、generating(生成)。Babel只负责编译新标准引入的语法,比如 Arrow function、Class、ES Module 等,它不会编译原生对象新引入的方法和 API,比如 Array.includes,Map,Set 等,这些需要通过 Polyfill 来解决,文章后面会提到。

Xnip2021-02-24_11-29-09

Babel7的npm包都是放到Babel域下, 例如@babel/cli、@babel/core等,在Babel6中,安装的包名是babel-core、babel-cli。本文将以Babel7为例进行讲解。

3. Babel的使用

3.1 Babel运行所需的基本环境

1、@babel/cli

@babel/cli 是 Babel 提供的内建命令行工具。

安装:npm install i -S @babel/cli

2、@babel/core

@babel/core是我们使用Bable进行转码的核心npm包,我们使用的babel-cli、babel-node都依赖这个包。在命令行和webpack进行转码的时候都是通过Node来调用@babel/core相关功能API来进行的。

安装: npm install --save-dev @babel/core

3.2 配置文件

通常,我们需要指定Babel的编译规则来编译代码。Babel的配置文件默认会在当前目录寻找文件,有:.babelrc.babelrc.jsbabel.config.jspackage.json,它们的配置项都是一样的,作用也一样,只需要选择一种即可。

1、.babelrc配置:

{
    "presets": ["es2015", "react"],
    "plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
复制代码

2、babel.config.js.babelrc.js配置:

module.exports = {
    "presets": ["es2015", "react"],
    "plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
复制代码

3、package.json配置:

{
    "name": "demo",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1"
    },
    "babel": {
      "presets": ["es2015", "react"],
      "plugins": ["transform-decorators-legacy", "transform-class-properties"]
    }
  }
复制代码

对比分析不同格式的Babel配置文件,总结起来的配置项都是plugins(插件)和presets(预设)两个数组(当然还有其他的,我们先只关注这两个)。

4. 插件plugins和预设presets

4.1 插件plugins

插件是用来定义如何转换你的代码的,一般是单独的某个新特性。当在Babel的配置项中填写了需要使用的插件,Babel编译的时候就会去加载node_modules中对应的npm包,然后编译插件的对应的语法。

  • @babel/plugin-transform-arrow-functions: 转换箭头函数的语法
  • @babel/plugin-transform-classes: 使用 Class 语法
  • @babel/plugin-transform-for-of: 使用 for...of 语法...

Babel支持的插件非常多,如果每一个新特性转换都需要通过安装插件来解决,那么我们的开发效率会变得非常低效,Babel配置文件就会变得非常臃肿。于是,Babel推出了懒人包presets

4.2 预设presets

预设就是一堆插件包的集合,例如babel-preset-es2015就是所有处理es2015的二十多个Babel插件的集合。这样我们就不需要写一大堆插件的配置项了,使用一个预设即可。

常用的preset包有:

  • @babel/preset-env
  • @babel/preset-react
  • @babel/preset-typescript
  • @babel/preset-stage-0
  • @babel/preset-stage-1...

解释下,stage-x,这里面包含的都是当年最新规范的草案,每年更新。这里面还细分为:

  • stage 0 - 设想: 只是一个想法,可能有 Babel 插件,stage-0 的功能范围最广大,包含 stage-1 , stage-2 以及 stage-3 的所有功能。
  • stage 1 - 提案: 初步尝试,值得跟进。
  • stage 2 - 初稿: 完成初步规范。
  • stage 3 - 候选: 完成规范和浏览器初步实现。
  • stage 4 - 完成: 将被添加到下一年度发布。

所有的预设也都需要安装npm包到node_modules中才可以使用。

4.3 执行顺序

plugins插件数组和presets预设数组是有顺序要求的。如果两个插件或预设都要处理同一个代码片段,那么会根据插件和预设的顺序来执行。规则如下:

1、插件plugins的执行顺序是从左到右执行的。

{
  "plugins": ["transform-decorators-legacy", "transform-class-properties"]
}
复制代码

在上面的示例中,Babel 在进行 AST 遍历的时候会先调用 transform-decorators-legacy 插件中定义的转换方法,然后再调用 transform-class-properties 中的方法。

2、预设presets的执行顺序是从右到左执行的。

{
  "presets": [
    "a",
    "b",
    "c"
  ]
}
复制代码

它的执行顺序是 c、b、a,是不是有点奇怪,这主要是为了确保向后兼容,因为大多数用户将 "es2015" 放在 "stage-0" 之前。

3、插件plugins在预设presets之前执行

4.4 @babel/preset-env一统江湖

在Babel6的时代,常见的preset有babel-preset-es2015babel-preset-es2016babel-preset-es2017babel-preset-latestbabel-preset-stage-0babel-preset-stage-1babel-preset-stage-2等。

目前,Babel官方不再推出babel-preset-es2017以后的年代preset了。

@babel/preset-env包含了babel-preset-latest的功能,并对其进行增强,现在@babel/preset-env完全可以替代babel-preset-latest

5. @babel/polyfill

在默认情况下,@babel/preset-env只会编译Javascript的语法,不会对新方法和新的原生对象进行转译。比如:

var fn = (num) => num + 2; 
const arr = [1,2,3]
console.log(arr.includes(1))
复制代码

转译后:

"use strict";

var fn = function fn(num) {
  return num + 2;
};

var arr = [1, 2, 3];
console.log(arr.includes(1));
复制代码

可以发现,箭头函数被转换了,但是Array.includes方法没有被处理,如果这个时候程序运行在低版本的浏览器上,就会出现includes is not function的错误。这个时候就需要polyfill了。

polyfill广义上讲是为环境提供不支持的特性的一类文件或库,既有Babel官方的库,也有第三方的。

@babel/polyfill本质上是由两个npm包core-js与regenerator-runtime组合而成的,所以在使用层面上还可以再细分为是引入@babel/polyfill本身还是其组合子包。使用方式有以下几种:

1. 在html文件中引入polyfill文件

<script src="https://cdn.bootcss.com/babel-polyfill/7.6.0/polyfill.js"></script>
复制代码

2. 在前端工程的入口文件里引入polyfill.js

import './polyfill.js';

var promise = Promise.resolve('ok');
console.log(promise);
复制代码

3. 在前端工程的入口文件里引入@babel/polyfill

1)安装@babel/polyfill

2)修改a.js内容

import '@babel/polyfill';

var promise = Promise.resolve('ok');
console.log(promise);
复制代码

4. 在前端工程的入口文件里引入core-js/stable与regenerator-runtime/runtime

1)安装core-js和regenerator-runtime

npm install --save core-js regenerator-runtime
复制代码

2)修改a.js内容

import "core-js/stable";
import "regenerator-runtime/runtime";

var promise = Promise.resolve('ok');
console.log(promise);
复制代码

5. 在前端工程构建工具的配置文件入口项引入polyfill.js

const path = require('path');

module.exports = {
  entry: ['./polyfill.js', './a.js'],
  output: {
    filename: 'b.js',
    path: path.resolve(__dirname, '')
  },
  mode: 'development'
};
复制代码

6. 在前端工程构建工具的配置文件入口里引入@babel/polyfill

const path = require('path');

module.exports = {
  entry: ['@babel/polyfill', './a.js'],
  output: {
    filename: 'b.js',
    path: path.resolve(__dirname, '')
  },
  mode: 'development'
};
复制代码

7. 在前端工程构建工具的配置文件入口里引入core-js/stable与regenerator-runtime/runtime

const path = require('path');

module.exports = {
  entry: ['core-js/stable', 'regenerator-runtime/runtime', './a.js'],
  output: {
    filename: 'b.js',
    path: path.resolve(__dirname, '')
  },
  mode: 'development'
};
复制代码

转译结果:

"use strict";
eval("\nvar toObject = __webpack_require__(/*! ../internals/to-object */ \"./node_modules/core-js/internals/to-object.js\");\nvar toAbsoluteIndex = __webpack_require__(/*! ../internals/to-absolute-index */ \"./node_modules/core-js/internals/to-absolute-index.js\");\nvar toLength = __webpack_require__(/*! ../internals/to-length */ \"./node_modules/core-js/internals/to-length.js\");\n\n// `Array.prototype.fill` method implementation\n// https://tc39.es/ecma262/#sec-array.prototype.fill\nmodule.exports = function fill(value /* , start = 0, end = @length */) {\n  var O = toObject(this);\n  var length = toLength(O.length);\n  var argumentsLength = arguments.length;\n  var index = toAbsoluteIndex(argumentsLength > 1 ? arguments[1] : undefined, length);\n  var end = argumentsLength > 2 ? arguments[2] : undefined;\n  var endPos = end === undefined ? length : toAbsoluteIndex(end, length);\n  while (endPos > index) O[index++] = value;\n  return O;\n};\n\n\n//# sourceURL=webpack:///./node_modules/core-js/internals/array-fill.js?");

/***/ }),

...

复制代码

这么多的方法,在实际开发中该选择哪一种呢?从babel7.4版本开始,Babel官方已经不推荐再使用@babel/polyfill,包括官方的polyfill.js库文件。因此从2019年中开始,我们的新项目都应该使用core-jsregenerator-runtime这两个包。也就是说我们应选择方法4方法7

但是,@babel/polyfill主要有两个缺点:

  1. @babel/polyfill把两个npm包全部都引入到了我们的前端打包后的文件里了,导致打包后的体积过大。
  2. @babel-polyfill 可能会污染全局变量,给很多类的原型链上都作了修改,这就有不可控的因素存在。

6. @babel/preset-env

@babel/preset-env的参数项,数量有10多个,但大部分我们要么用不到,要么已经或将要弃用。这里建议大家掌握重点的几个参数项,有的放矢。重点要学习的参数项有targets、useBuiltIns、modules和corejs这四个,能掌握这几个参数项的真正含义。

6.1 targets

该参数项可以取值为字符串、字符串数组或对象,不设置的时候取默认值空对象{}。

module.exports = {
  presets: [["@babel/env", {
    targets: {
      "chrome": "58",
      "ie": "11"
    }
  }]],
  plugins: []
}
复制代码

如果我们对@babel/preset-env的targets参数项进行了设置,那么就不使用browserslist的配置,而是使用targets的配置。如不设置targets,那么就使用browserslist的配置。如果targets不配置,browserslist也没有配置,那么@babel/preset-env就对所有ES6语法转换成ES5的。

6.2 useBuiltIns

useBuiltIns项取值可以是"usage" 、 "entry" 或 false。默认值为false。

我们来看一个例子:

1)安装npm包

npm install --save-dev @babel/cli @babel/core  @babel/preset-env
npm install --save @babel/polyfill
复制代码

2)修改Babel配置文件

module.exports = {
  presets: [["@babel/env", {
    useBuiltIns: "entry"
  }]],
  plugins: []
}
复制代码

3)修改package.json的browserslist

"browserslist": [
	"firefox 58"
]
复制代码

4)修改入口文件a.js

import '@babel/polyfill';
var promise = Promise.resolve('ok');
console.log(promise);
复制代码

5)执行命令:npx babel a.js -o b.js。转码后的结果:

"use strict";
require("core-js/modules/es7.array.flat-map");
require("core-js/modules/es7.string.trim-left");
require("core-js/modules/es7.string.trim-right");
require("core-js/modules/web.timers");
require("core-js/modules/web.immediate");
require("core-js/modules/web.dom.iterable");
var promise = Promise.resolve('ok');
console.log(promise);
复制代码

Babel转码后针对火狐58不支持的特性引入了6个core-js的API补齐模块,由于火狐58已经支持了Promise属性,所以没有引入Promise API相关的补齐特性。

6)修改Babel配置文件: useBuiltIns: "usage",去掉a.js中的import '@babel/polyfill';,运行npx babel a.js -o b.js,查看转码后的结果:

"use strict";

require("core-js/modules/es6.object.to-string.js");

require("core-js/modules/es6.promise.js");

var promise = Promise.resolve('ok');
console.log(promise);
复制代码

总结:

  • useBuiltIns: "usage": 不需要额外配置 @babel/polyfill,也不需要事先引入,@babel/polyfill 会自动安装 @babel/polyfill;
  • useBuiltIns: "entry": 不需要额外配置 @babel/polyfill,但需要在文件入口引入 @babel/polyfill,使用 require 或者 import;
  • useBuiltIns: false: 不在每一个文件自动添加语法填充,需要额外在配置文件加入 @babel/polyfill 配置。

6.3 corejs

该参数项的取值可以是2或3,没有设置的时候。默认值为2。这个参数项只有useBuiltIns设置为'usage'或'entry'时,才会生效。

需要注意的是,corejs取值为2的时候,需要安装并引入core-js@2版本,或者直接安装并引入polyfill也可以。如果corejs取值为3,必须安装并引入core-js@3版本才可以。

6.4 modules

这个参数项的取值可以是"amd"、"umd" 、 "systemjs" 、 "commonjs" 、"cjs" 、"auto" 、false。在不设置的时候,取默认值"auto"。

该项用来设置是否把ES6的模块化语法改成其它模块化语法。

7. @babel/plugin-transform-runtime

7.1 作用1

自动移除语法转换后内联的辅助函数(inline Babel helpers),使用@babel/runtime/helpers里的辅助函数来替代。

@babel/preset-env做语法转换做语法转换:

{
  "presets": [
    "@babel/env"
  ],
    "plugins": [
    ]
}
复制代码

需要转换的代码为:

class Person {
  sayname() {
    return 'name'
  }
}

var john = new Person()
console.log(john)
复制代码

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; }

var Person = /*#__PURE__*/function () {
  function Person() {
    _classCallCheck(this, Person);
  }

  _createClass(Person, [{
    key: "sayname",
    value: function sayname() {
      return 'name';
    }
  }]);

  return Person;
}();

var john = new Person();
console.log(john);

复制代码

可以看到转换后的代码上面增加了好几个函数声明,这就是注入的函数,我们称之为辅助函数。@babel/preset-env在做语法转换的时候,注入了这些函数声明,以便语法转换后使用。

但样这做存在一个问题。在我们正常的前端工程开发的时候,少则几十个js文件,多则上千个。如果每个文件里都使用了class类语法,那会导致每个转换后的文件上部都会注入这些相同的函数声明。这会导致我们用构建工具打包出来的包非常大。

那么怎么办?一个思路就是,我们把这些函数声明都放在一个npm包里,需要使用的时候直接从这个包里引入到我们的文件里。这样即使上千个文件,也会从相同的包里引用这些函数。通过webpack这一类的构建工具打包的时候,我们只会把使用到的npm包里的函数引入一次,这样就做到了复用,减少了体积。

借助@babel/plugin-transform-runtime插件来帮助我们解决这个问题。

module.exports = {
  presets: ["@babel/env"],
  plugins: ["@babel/plugin-transform-runtime"]
}
复制代码

1)安装npm包:

npm install --save-dev @babel/cli @babel/core  @babel/preset-env @babel/plugin-transform-runtime
复制代码

2)修改Babel配置文件

{
  "presets": [
    "@babel/env"
  ],
  "plugins": [
    "@babel/plugin-transform-runtime"
  ]
}
复制代码

3)执行npx babel a.js -o b.js命令,得到转换后的内容为:

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var Person = /*#__PURE__*/function () {
  function Person() {
    (0, _classCallCheck2["default"])(this, Person);
  }

  (0, _createClass2["default"])(Person, [{
    key: "sayname",
    value: function sayname() {
      return 'name';
    }
  }]);
  return Person;
}();

var john = new Person();
console.log(john);

复制代码

7.2 作用2

当代码里使用了core-js的API,自动引入@babel/runtime-corejs3/core-js-stable/,以此来替代全局引入的core-js/stable。

1)安装npm包:

npm install --save @babel/runtime-corejs3
npm install --save-dev @babel/cli @babel/core  @babel/preset-env @babel/plugin-transform-runtime
复制代码

2)修改babel配置:

{
  "presets": [
    "@babel/env"
  ],
  "plugins": [
     ["@babel/plugin-transform-runtime", {
        "corejs": 3
     }]
  ]
}
复制代码

3)修改a.js内容

var obj = Promise.resolve();
复制代码

4)执行npx babel a.js -o b.js命令

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var obj = _promise["default"].resolve();
复制代码

7.3 作用3

当代码里使用了Generator/async函数,对Generator/async进行API转换功能,默认是开启的,不需要我们设置。

8. 最后

欢迎大家关注我的公众号 -- 《前端Talkking》 😄

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

参考资料

  1. Babel7知识梳理

  2. Babel教程

  3. Show me the code,babel 7 最佳实践

  4. 2020, 再谈 Polyfill 最佳实践

  5. Babel 7 转码的正确姿势

  6. 史上最清晰易懂的babel配置解析

  7. Babel7 使用配置详解

  8. babel工作原理浅析

文章分类
前端
文章标签