重识babel 7

avatar
前端开发工程师 @bigo

file

本文首发于:github.com/bigo-fronte… 欢迎关注、转载。

工欲善其事,必先利其器

作为一个前端开发者,想要使用ECMAScript 2015+新语法,又要兼容旧版的浏览器,babel相关的工具及配置是一个无法绕过去的坎。

前段时间笔者想优化公司内部的一个npm库的size,苦于胸总中无沟壑,只能老老实实看babel的官方文档,写了4个“自认为”是使用babel的最佳配置,分别对应webapp和library的配置,希望对大家日程开发实践和性能优化有一定借鉴意义,如有不足之处,也欢迎大家斧正。

下面我将围绕babel相关的主要npm库,为大家娓娓道来(本文基于最新的babel7)。

@babel/core

babel核心库,使用babel必须要安装的。

在这里我们解释一下babel到底是什么,这里引用官方的定义:

Babel 是一个 JavaScript 编译器

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的特性(通过第三方 polyfill 模块,例如 core-js,实现)
  • 源码转换 (codemods)
// Babel 输入: ES2015 箭头函数
[1, 2, 3].map((n) => n + 1);

// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) {
  return n + 1;
});

@babel/cli:

babel命令行工具,单独使用babel时安装,当babel配合webpackrollup等打包工具一起使用的时候,通常会有相应的loader或着plugin,此时可能并不需要@babel/cli

@babel/preset-env

在解释这个库的作用之前,我们看一下最原始的babel配置是怎样的:

babel.config.js

module.exports = {
  plugins: [
    "@babel/plugin-transform-block-scoping", 
    "@babel/plugin-transform-arrow-functions",
    [
      "babel-plugin-polyfill-corejs3",
      {
        "method": "usage-global"
      }
    ]
  ]
}

input

// index.js
const a = 'hello world'

const set = new Set();

const foo = () => {
  console.log('function')
}

output

require("core-js/modules/es.array.iterator.js");

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

require("core-js/modules/es.set.js");

require("core-js/modules/es.string.iterator.js");

require("core-js/modules/web.dom-collections.iterator.js");

var a = 'hello world';
var set = new Set();

var foo = function foo() {
  console.log('function');
};

在我们的代码中,用到了哪些特性,就需要把对应的babel插件添加进来,后续如果我们还要添加其他的esnext特性,就要这样一个一个的加入各种各样的插件,这对开发这来说非常的不友好。

如果有一个工具可以把常用的plugin都一股脑加进来,开发者并不需要关心自己的代码用了什么新特性,也不关心要安装哪些babel插件,添加plugin的这些工作全都由这个工具去完成,那就轻松很多了。

这时候preset就登场了,我看看官方介绍:

Babel 的预设(preset)可以被看作是一组 Babel 插件和或 options 配置的可共享模块。

preset有很多,官方的preset中,有根据stage的不同,和ECMAScript的版本的不同推出的各种preset,而今天我们的主角是@babel/preset-env,其他的基本都被废弃掉了。

通过官方文档的描述,preset-env主要做的是转换JavaScript最新的语法,而作为可选项 preset-env 也可以转换 JavaScript 最新的 API (指的是比如数组最新的方法includes,Promise等等)

总之,就是把所有的常用插件都汇聚到了一起,省去了自己配置插件的功夫。

这个插件有很多选项可以配置,我们挑几个重要的讲

useBuiltIns

"usage" | "entry" | false, defaults to false.

此选项配置 @babel/preset-env 如何处理 polyfill。

entry

我们需要在代码的入口文件顶部加入两行代码:

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

会在此时 babel 会根据当前targets描述,把需要的所有的 polyfills 全部引入到你的入口文件(注意是全部,不管你是否有用到高级的 API)

usage

无需额外代码,babel 会根据用户代码的使用情况,并根据 targets 自行注入相关 polyfills。

false

这种方式下,不会引入 polyfills,你需要人为在入口文件处import '@babel/polyfill' 全量引入。或者手工引入对应模块的polyfill。

corejs

string |{ version: string, proposals: boolean },defaults "2.0"

此选项仅在与 useBuiltIns: usage 或 useBuiltIns: entry 一起使用时才有效,并确保 @babel/preset-env 注入core-js版本支持的polyfill。

选项需要安装对应的corejs版本

npm install core-js@3 --save

# or

npm install core-js@2 --save

可能的配置如下

// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: { version: '3.16', proposals: true }, // 实际的corejs版本
      }
    ]
  ]
};

targets

运行代码的目标浏览器。

亦可以使用browserslist代替该选项。

loose

boolean, defaults to false.

优化编译的产物,如果设置为true,则会生成性能更高的转译代码,但可能不太符合ES规范。具体查看assumptions

modules

"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false, defaults to "auto".

启用 ES 模块语法到另一种模块类型的转换。

如果是配合打包工具rollup或是webpack,则false即可。

@babel/plugin-transform-runtime

一个插件,可以重用 Babel 注入的辅助代码以节省代码大小。

helpers

boolean, defaults to true.

切换是否将内联的 Babel 助手(classCallCheck、extends 等)替换为对 moduleName 的调用。

corejs

false, 2, 3 or { version: 2 | 3, proposals: boolean }, defaults to false.

@babel/preset-env添加的polyfill都是污染全局的,对于webapp来说是可以接受,而作为library的开发者,并不希望污染全局。

默认情况下,@babel/plugin-transform-runtime 不polyfill提案阶段的api。如果使用的是 corejs: 3,可以通过使用 proposal: true 选项来启用它。

需要的依赖项如下

corejs optionInstall command
falsenpm install --save @babel/runtime
2npm install --save @babel/runtime-corejs2
3npm install --save @babel/runtime-corejs3

webapp最佳实践1

这套针对webapp的配置,最大程度的增加对目标浏览器(运行环境)的支持,即便是项目依赖里使用的某个依赖库里使用了某些高级api,代码亦可正常运行。因为他会根据targets去引入目标targets浏览器所需的polyfill,而不管你代码中是否使用了该特性。当然,这种方式的缺点就是打包后的包体积会比较大,有很大可能会包含一些并未用到的polyfill。

npm install -D @babel/core @babel/preset-env @babel/plugin-transform-runtime
npm install -S core-js@3
// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'entry',
        targets: 'Android 4.0, IOS 7', // .browserslistrc
        corejs: { version: '3.16', proposals: true }, // 实际的corejs版本
        loose: true,
        modules: false
      }
    ]
  ],
  plugins: [['@babel/plugin-transform-runtime', { helpers: true }]]
};

// index.js
import "core-js/stable"
import "regenerator-runtime/runtime"
// other code

webapp最佳实践2

这个webapp的配置,则仅针对代码中使用到的api添加polyfill,最大程度的减小打包体积。然而,由于babel不会再对依赖库中的产物进行编译,因此babel便无法检测到依赖库里的代码,一旦某个依赖库需要依赖某些polyfill,则可能最终类库会无法运行。

npm install -D @babel/core @babel/preset-env @babel/plugin-transform-runtime
npm install -S core-js@3
// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        targets: 'Android 4.0, IOS 7', // .browserslistrc
        corejs: { version: '3.16', proposals: true }, // 实际的corejs版本
        loose: true,
        modules: false
      }
    ]
  ],
  plugins: [['@babel/plugin-transform-runtime', { helpers: true }]]
};

library最佳实践1

这个是针对"不关心类库体积大小"的场景下的一个类库开发最佳实践。使用如下的babel配置时,polyfill不会污染全局,同时又能让类库自己能正常运行,不至于代码运行在低版本浏览器里直接就报错。

然而,由于类库自身添加了局部的polyfill,会使你打包后的体积膨胀。如果类库提供给一个C端应用使用的话,那么应用自身的全局polyfill和类库自身的局部polyfill必然会存在冗余,这样无形又增大了应用的体积,反而降低应用的性能。

因此,这个实践,只适用于"不关注性能"的场景。

npm install -D @babel/core @babel/preset-env @babel/plugin-transform-runtime
npm install -S @babel/runtime-corejs3
// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env', 
      { 
        loose: true, 
        modules: false
      }
    ]
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime', 
      { 
        helpers: true, 
        corejs: {
          version: 3,
          proposals: true
        }
      }
    ]
  ]
};

library最佳实践2

以下配置,比较适合 "公司内自有C端业务" 或 "开源类库产品"。通常这些产品对性能有着极致的要求。因此在代码体积方面 "寸土寸金"。

基于此,我们可以直接将类库的corejs配置设置为false,也就是类库自身不添加任何polyfill。这就要求我们在开发类库项目时,要做到如下2点任选其一:

1、直接放弃使用ES5+的新特性,使用原生API语法来编写代码(在社区中可以看到很多类库作者是这样做的)

2、可以使用ES6语法,但是要通过文档告诉宿主环境,也就是告诉我们的调用者,让他注意要主动手工引入对应的polyfill。

// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env', 
      { 
        loose: true, 
        modules: false
      }
    ]
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime', 
      { 
        helpers: true, 
        corejs: false
      }
    ]
  ]
};

总结

从上面babel实践和 vue/babel-preset-app 的一些实践总结来看的话,可以将library和webapp最佳实践做如下的总结:

  1. 对于类库开发来说,比如我们要给公司或者github上开发一个开源类库。可能我们大部分"要照顾性能的场景下"最好就把polyfill设置为false, 只把helper设置为true。 然后编码时只用es5语法写类库,或者使用es6但要通过文档告诉调用者。

  2. 如果主web项目的依赖库是以ES5的形式释出的,同时依赖库若使用了ES6+特性。此时,要看该依赖库的作者是否"在文档中声明了其依赖的polyfill"。

  • 若作者声明了依赖polyfill列表。那么我们可以在主项目中使用useBuiltIns:'usage',且需要预先引入类库所需的polyfills。一般项目的引入方法可以使用import语法在入口文件引入,具体模块需参考corejs文档;而若是使用了vue/babel-preset-app的项目,则可以直接通过其polyfill选项配置来指定。
  • 若类库作者并没有声明所依赖的polyfill。则我们为了保险起见,则可以将主项目的babel配置为useBuiltIns:'entry'。从而尽最大可能保证我们主项目引入的全局polyfill能覆盖类库所需。
  1. 若主项目依赖库是用ES6+语法来写的,且使用了目标浏览器不支持的API特性。那么我们可以在主项目中使用useBuiltIns:'usage'的配置。然后在主项目代码中的babel和webpack配置里将对应的依赖库设置为include编译包含,这样的话babel编译则会将该依赖库按照主项目的配置进行编译并遵循useBuiltIns:'usage'配置进行polyfill。(若你主项目是使用vue/babel-preset-app,则请参考其文档进行对应项的配置)

参考自: www.npmjs.com/package/@vu…

相关库

以下为在babel配置实践过程中用到的相关库及版本

  • "@babel/cli": "^7.14.8"
  • "@babel/core": "^7.14.8"
  • "@babel/plugin-transform-runtime": "^7.15.0"
  • "@babel/preset-env": "^7.15.0"
  • "@babel/runtime-corejs3": "^7.15.3"
  • "core-js": "^3.16.1"

link

欢迎大家留言讨论,祝工作顺利、生活愉快!

我是bigo前端,下期见。