Babel7知识梳理

简介

一直以来对Babel只是有一个朦胧的认识,今天看到一篇关于Babel7的相关文章。认识一下子清晰了很多。
为防止找不到了,特意将文章搬移过来,原文地址:Babel7 知识梳理

@babel/core

Babel 的核心功能包含在 @babel/core 模块中。不安装 @babel/core,无法使用 babel 进行编译。

@babel/cli

babel 提供的命令行工具,主要是提供 babel 这个命令,适合安装在项目里。

@babel/node 提供了 babel-node 命令,但是 @babel/node 更适合全局安装,不适合安装在项目里。

  • 初始化项目:npm init -y

  • 新建src/index.js文件

const fn = () => { console.log('a') }
  • 安装@babel/core@babel/cli
npm i @babel/core @babel/cli --save-dev
  • 修改package.json文件的scripts字段
{ // ... "scripts": { "compiler": "babel src --out-dir lib --watch" }, // ... }
  • 使用 npm run compiler 来执行编译,现在我们没有配置任何插件,编译前后的代码是完全一样的。

因为 Babel 虽然开箱即用,但是什么动作也不做,如果想要 Babel 做一些实际的工作,就需要为其添加插件(plugin)。

插件

如果插件发布在 npm 上,可以直接填写插件的名称, Babel 会自动检查它是否已经被安装在 node_modules 目录下

  • 在项目目录下新建 .babelrc 文件
{ "plugins": ["@babel/plugin-transform-arrow-functions"] }

也可以指定插件的相对/绝对路径

{ "plugins": ["./node_modules/@babel/plugin-transform-arrow-functions"] }
  • 执行npm run compiler,可以看到箭头函数已经被编译。
// lib/index.js var fn = function fn() { console.log('a'); };

现在,我们仅支持转换箭头函数,如果想将其它的新的JS特性转换成低版本,需要使用其它对应的 plugin
如果我们一个个配置的话,会非常繁琐,因为你可能需要配置几十个插件,这显然非常不便,那么有没有什么办法可以简化这个配置呢?

有!预设!

预设

官方preset

  • @babel/preset-env:最新Babel-7版本出的预设,抛弃老版本繁乱的插件,这个预设通过配置能根据浏览器版本,将代码自动翻译成最适合的版本
  • @babel/preset-flow
  • @babel/preset-react:专门为React做的预设,能很好的将React简便写法翻译成React正经语法
  • @babel/preset-typescript:专门为Typescript做的预设,只包含一个插件
  • Babel v7 开始,所以针对标准提案阶段的功能所编写的预设(stage preset)都已被弃用,官方已经移除了 @babel/preset-stage-x

@babel/preset-env

@babel/preset-env 主要作用是对我们所使用的并且目标浏览器中缺失的功能进行代码转换和加载 polyfill
在不进行任何配置的情况下,@babel/preset-env 所包含的插件将支持所有最新的JS特性(ES2015,ES2016等,不包含 stage 阶段),将其转换成ES5代码。

例如,如果你的代码中使用了可选链(目前,仍在 stage 阶段),那么只配置 @babel/preset-env,转换时会抛出错误,需要另外安装相应的插件。

//.babelrc { "presets": ["@babel/preset-env"] }

@babel/preset-env 会根据你配置的目标环境,生成插件列表来编译。

官方推荐使用 .browserslistrc 文件来指定目标环境。
默认情况下,如果你没有在 Babel 配置文件中(如 .babelrc)设置 targetsignoreBrowserslistConfig@babel/preset-env 会使用 browserslist 配置源。

如果你不是要兼容所有的浏览器和环境,推荐你指定目标环境,这样你的编译代码能够保持最小。

例如,仅包括浏览器市场份额超过0.25%的用户所需的 polyfill 和代码转换(忽略没有安全更新的浏览器,如 IE10 和 BlackBerry)

> 0.25%
not dead

例如,你将 .browserslistrc 的内容配置为: last 2 Chrome versions
然后再执行 npm run compiler,你会发现箭头函数不会被编译成ES5,因为 chrome 的最新2个版本都能够支持箭头函数。
现在,我们将 .browserslistrc 仍然换成之前的配置。

  • 现在我们修改下src/index.js
const isHas = [1,2,3].includes(2); const p = new Promise((resolve, reject) => { resolve(100); });
  • 编译出来的结果为:
"use strict"; var isHas = [1, 2, 3].includes(2); var p = new Promise(function (resolve, reject) { resolve(100); });

这个编译出来的代码在低版本浏览器中使用的话,显然是有问题的,因为低版本浏览器中数组实例上没有 includes 方法,也没有 Promise 构造函数。

这是为什么呢?因为语法转换只是将高版本的语法转换成低版本的,但是新的内置函数、实例方法无法转换
这时,就需要使用 polyfill 上场了,
顾名思义,polyfill的中文意思是垫片,所谓垫片就是垫平不同浏览器或者不同环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用。

Polyfill

@babel/polyfill 模块包括 core-js 和一个自定义的 regenerator runtime 模块,可以模拟完整的 ES2015+ 环境(不包含第4阶段前的提议)。

这意味着可以使用诸如 PromiseWeakMap 之类的新的内置组件、 Array.fromObject.assign 之类的静态方法、Array.prototype.includes 之类的实例方法以及生成器函数(前提是使用了 @babel/plugin-transform-regenerator 插件)。
为了添加这些功能,polyfill 将添加到全局范围和类似 String 这样的内置原型中(会对全局环境造成污染,后面我们会将不污染全局环境的方法)。

  • 安装@babel/polyfill
npm i @babel/polyfill --save

注意:不使用 --save-dev,因为这是一个需要在源码之前运行的垫片。

  • 修改src/index.js文件,将完整的polyfill在代码之前加载
import '@babel/polyfill'; const isHas = [1,2,3].includes(2); const p = new Promise((resolve, reject) => { resolve(100); });

@babel/polyfill 需要在其它代码之前引入,我们也可以在 webpack 中进行配置。例如:

// ... module.exports = { // ... entry: [ require.resolve('./polyfills'), path.resolve('./index') ], // ... }

polyfills.js 文件内容如下:

//当然,还可能有一些其它的 polyfill,例如 stage 4之前的一些 polyfill import '@babel/polyfill';

现在,我们的代码不管在低版本还是高版本浏览器(或node环境)中都能正常运行了。不过,很多时候,我们未必需要完整的 @babel/polyfill,这会导致我们最终构建出的包的体积增大。

我们更期望的是,如果我使用了某个新特性,再引入对应的 polyfill,避免引入无用的代码。

@babel/preset-env 提供了一个 useBuiltIns 参数;

  • useBuiltIns: "usage":不需要在入口引入 @babel/polyfill,根据业务中用到的特性自动加载,但需要安装polyfill作为依赖。
  • useBuiltIns: "entry":的含义是找到入口文件里引入的 @babel/polyfill,并替换为 targets 浏览器环境需要的补丁列表。
  • useBuiltIns: false:默认是false,不引入polyfill

设置值为 usage 时,就只会包含代码需要的 polyfill

注意

  • 配置此参数的值为 usage ,必须要同时设置 corejs (如果不设置,会给出警告,默认使用的是"corejs": 2) ,
  • 这里仍然需要安装 @babel/polyfill(当前 @babel/polyfill版本默认会安装 "corejs": 2):

首先说一下使用 core-js@3 的原因。
core-js@2 分支中已经不会再添加新特性,新特性都会添加到 core-js@3
例如你使用了 Array.prototype.flat(),如果你使用的是 core-js@2,那么其不包含此新特性。
为了可以使用更多的新特性,建议大家使用 core-js@3

  • 安装依赖:npm i core-js@3 --save

  • 修改.babelrc配置文件

const presets = [ [ "@babel/env", { "useBuiltIns": "usage", "corejs": 3 } ] ]
  • src/index.js文件中去掉import '@babel/polyfill'
const isHas = [1,2,3].includes(2); const p = new Promise((resolve, reject) => { resolve(100); });
  • 执行npm run compiler后,编译结果如下
"use strict"; require("core-js/modules/es.array.includes"); require("core-js/modules/es.object.to-string"); require("core-js/modules/es.promise"); var isHas = [1, 2, 3].includes(2); var p = new Promise(function (resolve, reject) { resolve(100); });

前面曾提到,在 useBuiltIns 参数值为 usage 时,仍然需要安装 @babel/polyfill
虽然我们上面的代码转换中看起来并没有使用到,但是,如果我们源码中使用到了 async/await,那么编译出来的代码需要 require("regenerator-runtime/runtime")
@babel/polyfill 的依赖中,当然啦,你也可以只安装 regenerator-runtime/runtime 取代安装 @babel/polyfill

Babel 会使用很小的辅助函数来实现类似 _createClass 等公共方法。默认情况下,它将被添加(inject)到需要它的每个文件中。

  • 假如src/index.js文件中使用class
class Point { constructor(x, y) { this.x = x; this.y = y; }; getX() { return this.x; } } let cp = new Point(25, 8);

编译出来的 lib/index.js,如下所示:

"use strict"; require("core-js/modules/es.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 Point = /*#__PURE__*/ function () { function Point(x, y) { _classCallCheck(this, Point); this.x = x; this.y = y; } _createClass(Point, [{ key: "getX", value: function getX() { return this.x; } }]); return Point; }(); var cp = new Point(25, 8);

看起来,似乎并没有什么问题,但是你想一下,如果你有10个文件中都使用了这个 class,是不是意味着 _classCallCheck_defineProperties_createClass 这些方法被 inject 了10次。
这显然会导致包体积增大,最关键的是,我们并不需要它 inject 多次。

这个时候,就是 @babel/plugin-transform-runtime 插件大显身手的时候了,
使用 @babel/plugin-transform-runtime 插件,所有帮助程序都将引用模块 @babel/runtime,这样就可以避免编译后的代码中出现重复的帮助程序,有效减少包体积。

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime 是一个可以重复使用 Babel 注入的帮助程序,以节省代码大小的插件。

注意
诸如 Array.prototype.flat() 等实例方法将不起作用,因为这需要修改现有的内置函数(可以使用 @babel/polyfill 来解决这个问题)
——> 对此需要说明的是如果你配置的是corejs3core-js@3 现在已经支持原型方法,同时不污染原型

另外,@babel/plugin-transform-runtime 需要和 @babel/runtime 配合使用。

@babel/plugin-transform-runtime 通常仅在开发时使用,但是运行时最终代码需要依赖 @babel/runtime
所以 @babel/runtime 必须要作为生产依赖被安装

  • 安装依赖
npm i @babel/plugin-transform-runtime --save-dev npm i @babel/runtime --save

除了前文所说的,@babel/plugin-transform-runtime 可以减少编译后代码的体积外,我们使用它还有一个好处,它可以为代码创建一个沙盒环境,
如果使用 @babel/polyfill 及其提供的内置程序(例如 PromiseSetMap ),则它们将污染全局范围。
虽然这对于应用程序或命令行工具可能是可以的,但是如果你的代码是要发布供他人使用的库,或者无法完全控制代码运行的环境,则将成为一个问题。

@babel/plugin-transform-runtime 会将这些内置别名作为 core-js 的别名,因此您可以无缝使用它们,而无需 polyfill

  • 修改.babelrc配置文件
{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", "corejs": 3 } ] ], "plugins": [ [ "@babel/plugin-transform-runtime" ] ] }
  • 重新编译 npm run compiler , 编译出来的内容为(lib/index.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 Point = /*#__PURE__*/ function () { function Point(x, y) { (0, _classCallCheck2.default)(this, Point); this.x = x; this.y = y; } (0, _createClass2.default)(Point, [{ key: "getX", value: function getX() { return this.x; } }]); return Point; }(); var cp = new Point(25, 8);

可以看出,帮助函数现在不是直接被 inject 到代码中,而是从 @babel/runtime 中引入。
前文说了使用 @babel/plugin-transform-runtime 可以避免全局污染,我们来看看是如何避免污染的。

  • 修改src/index.js文件
let isHas = [1,2,3].includes(2); new Promise((resolve, reject) => { resolve(100); });
  • 编译出来的代码如下(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"); var isHas = [1, 2, 3].includes(2); new Promise(function (resolve, reject) { resolve(100); });

Array.prototype 上新增了 includes 方法,并且新增了全局的 Promise 方法,污染了全局环境,这跟不使用 @babel/plugin-transform-runtime 没有区别嘛。

如果我们希望 @babel/plugin-transform-runtime 不仅仅处理帮助函数,同时也能加载 polyfill 的话,我们需要给 @babel/plugin-transform-runtime 增加配置信息。

  • 新增依赖@babel/runtime-corejs3
npm i @babel/runtime-corejs3 --save
  • 修改.babelrc配置文件
{ "presets": [ [ "@babel/preset-env", // { // "useBuiltIns": "usage", // "corejs": 3 // } ] ], "plugins": [ [ "@babel/plugin-transform-runtime",{ "corejs": 3 } ] ] }
  • 重新编译下,编译出来的结果(lib/index.js)
"use strict"; var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault"); var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise")); var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes")); var _context; var isHas = (0, _includes.default)(_context = [1, 2, 3]).call(_context, 2); new _promise.default(function (resolve, reject) { resolve(100); });

可以看出,没有直接去修改 Array.prototype,或者是新增 Promise 方法,避免了全局污染。
如果上面 @babel/plugin-transform-runtime 配置的 core-js"2",其中不包含实例的 polyfill 需要单独引入。

备注:

  • 如果我们配置的 corejs3 版本,那么不管是实例方法还是全局方法,都不会再污染全局环境。

看到这里,不知道大家有没有这样一个疑问?
@babel/plugin-transform-runtime 配置 corejs 是如此的完美,既可以将帮助函数变成引用的形式,又可以动态引入 polyfill,并且不会污染全局环境。
何必要给 @babel/preset-env 提供 useBuiltIns 功能呢,看起来似乎不需要呀。

带着这样的疑问,我新建了几个文件(内容简单且基本一致,使用了些新特性),然后使用 webpack 构建,以下是我对比的数据:

序号 .babelrc 配置 webpack mode production
0 不使用 @babel/plugin-transform-runtime 36KB
1 使用 @babel/plugin-transform-runtime,并配置参数 corejs: 3。不会污染全局环境 37KB
2 使用 @babel/plugin-transform-runtime,不配置 corejs 22KB

猜测是 @babel/runtime-corejs3/XXX 的包本身比 core-js/modules/XXX 要大一些~

补充知识

排列顺序

如果两个转换插件都将处理“程序(Program)”的某个代码片段,则将根据转换插件或 preset 的排列顺序依次执行。

  • 插件在 Presets 前运行。
  • 插件顺序从前往后排列。
  • Preset 顺序是颠倒的(从后往前)。
{ "plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-syntax-dynamic-import"] }

插件的顺序是从前往后的:先执行 @babel/plugin-proposal-class-properties,后执行 @babel/plugin-syntax-dynamic-import

{ "presets": ["@babel/preset-env", "@babel/preset-react"] }

预设的顺序是从后往前的:先执行 @babel/preset-react, 后执行 @babel/preset-env

插件参数

  • 插件和 preset 都可以接受参数,参数由插件名和参数对象组成一个数组。preset 设置参数也是这种格式。
{ "plugins": [ [ "@babel/plugin-proposal-class-properties", { "loose": true } ] ] }

插件短名称

  • 如果插件名称为 @babel/plugin-XXX,可以使用短名称@babel/XXX
{ "plugins": [ "@babel/transform-arrow-functions" // 同 "@babel/plugin-transform-arrow-functions" ] }
  • 如果插件名称为 babel-plugin-XXX,可以使用短名称 XXX,该规则同样适用于带有 scope 的插件
{ "plugins": [ "newPlugin", // 同 "babel-plugin-newPlugin" "@scp/myPlugin" // 同 "@scp/babel-plugin-myPlugin" ] }

创建 Preset

  • 可以简单的返回一个插件数组
module.exports = function() { return { plugins: [ "A", "B", "C" ] } }
  • preset 中也可以包含其他的 preset,以及带有参数的插件。
module.exports = function() { return { presets: [ require("@babel/preset-env") ], plugins: [ [require("@babel/plugin-proposal-class-properties"), { loose: true }], require("@babel/plugin-proposal-object-rest-spread") ] } }

配置文件

  • 根目录下babel.config.js文件
module.exports = function(api) { api.cache(true); const presets = [...]; const plugins = [...]; return { presets, plugins }; }
  • 根目录下.babelrc文件
{ "presets": [], "plugins": [] }
  • 可以将 .babelrc 中的配置信息作为 babel 键(key) 添加到 package.json 文件中
{ // ... "babel": { "presets": [], "plugins": [] } }
  • 根目录下.babelrc.js文件
// 可以在其中调用 Node.js 的API const presets = []; const plugins = []; module.exports = { presets, plugins };

创作不易,若本文对你有帮助,欢迎打赏支持作者!

 分享给好友: