babel 浅入了解

293 阅读5分钟

导读

babel是我们能在众多浏览器各种版本中一直使用ECMAscript新特性的工具,我们可以不需要考虑浏览器兼容性的情况下使用新的特性/规范,babel可以在打包的时候将代码转化为我们需要兼容的版本,但由于配置比较复杂,且大部分脚手架都已经帮我们处理了,所以很少人会去了解 babel。

但如果出现由于 babel 配置导致的问题,就很难找到原因。

本文主要是介绍如何使用babel,以及babel的主体结构;

注: 本文使用的babel版本是babel@7.16.12;

应用

其实babel的应用比较复杂,复杂在于需要理解babel的整体架构和调用顺序。e.g.
1、presets/plugin的区别,用哪些presets,用哪些plugin,怎么配置;
2、例如一般我们开发会用的框架react/vue,或者是typescript,在调用babel之前,我们需要先通过对应的编译工具(e.g. vue-loader, ts-loader等)编译为符合ECMAscript规范的js代码,再通过babel编译到我们预期的ECMAscript版本;

在成型项目打包中运用前,我们可以先尝试单独使用babel:
1、创建一个简单的项目,安装依赖;

// 编译的主要逻辑在core中,cli是命令行集成,preset-env是我们最常用的编译规则集合
yarn add -D @babel/cli @babel/core @babel/preset-env

2、创建一个需要编译的js文件,e.g.

// index.js, 需要编译的内容有async..await, const, 箭头函数, array.includes, promise, 字符串模板
(async() => {
  const fn = () => {
    return new Promise((resolve, reject) => {
      const arr = [1,2,3]
      setTimeout(() => {
        if(arr.includes(1)) {
          console.log(`wellcome to bable!`)
        }
      })
    })
  }
  const wellcome = await fn();
})()

3、执行babel cli;

npx babel index.js --presets=@babel/preset-env

4、stdout

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

_asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
  var fn, wellcome;
  return regeneratorRuntime.wrap(function _callee$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          fn = function fn() {
            return new Promise(function (resolve, reject) {
              setTimeout(function () {
                console.log("wellcome to bable!");
              });
            });
          };

          _context.next = 3;
          return fn();

        case 3:
          wellcome = _context.sent;

        case 4:
        case "end":
          return _context.stop();
      }
    }
  }, _callee);
}))();

可以看到除了 promise, array.includes 之外都被translate了。
因为 babel 将 ECMAScript 分两部分,语法和api;
语法部分由 @babel/core 处理,而 api 部分由 polyfill 处理

polyfill

由于引入方式麻烦,或可能导致更大的包体积,babel在 7.4.0版本已废除了 polyfill 这个包。
废除之后,改为在@babel/preset-env引入这两个包(core-js, regenerator),不再需要手动引入 polyfill,而是通过@babel/preset-env的 useBuiltIns 选项进行配置;

"presets": [
  ["@babel/preset-env", 
    {
      "useBuiltIns": "usage", // "usage" | "entry" | false, default表示不引入 polyfill , usage表示按需加载 polyfill , entry表示需要手动引入 polyfill
      "corejs": "3.21", // 指定 corejs版本,默认2.0
    }
  ]
]

不建议使用 'entry' 选项,因为在手动引入, e.g.

import "core-js/stable";

会将所有 api 的引用全部导入,导致包体积过大

// 引入后结果(过多,省略部分包展示)
require("core-js/modules/es.symbol.js");
require("core-js/modules/es.symbol.description.js");
require("core-js/modules/es.symbol.async-iterator.js");
require("core-js/modules/es.symbol.has-instance.js");
require("core-js/modules/es.symbol.is-concat-spreadable.js");
...

core-js && regenerator

core-js 包含了大部分新特性的polyfill, e.g. promises, symbols, collections, iterators, typed arrays...

regenerator 主要是处理 generators/yield, Asynchronous Iteration proposal , spits out efficient JS-of-today.

babel plugin && preset

preset是一系列babel plugin的集合,如果不用preset的话,我们需要这样去配置:

"plugins": [
  "@babel/plugin-transform-arrow-functions",
  "@babel/plugin-transform-async-to-generator",
  "@babel/plugin-transform-block-scoped-functions",
  ...
]

babel的preset如下:

1@babel/preset-env;  // es6,7,8,9...
2@babel/preset-react;  // jsx
3@babel/preset-typescript;  // typescript 语法
4@babel/preset-flow;  // flow,facebook的一个类型编程语言

Babel Preset视为Babel Plugin的集合,preset和plugin的执行顺序如下:

1、先执行完所有Plugin,再执行Preset。
2、多个Plugin,按照声明次序顺序执行。
3、多个Preset,按照声明次序逆序执行。

@babel/preset-env

@babel/preset-env 除了上面提到 useBuiltIns/corejs 配置项外,还有一个重要且复杂的配置项 targets;

这个是 babel 优化打包体积的一个重要配置项。

browserslist

targets 主要是通过 browserslist 这个插件来查询需要兼容的浏览器版本,e.g.

// 全局安装 browserslist 后键入命令,即可查看默认项的浏览器版本列表,babel默认项配置 '> 0.5%, last 2 versions, Firefox ESR, not dead'
browserslist 

// 输出
and_chr 98
and_ff 96
and_qq 10.4
and_uc 12.12
android 98
baidu 7.12
chrome 98
chrome 97
chrome 96
...

也可以直接在根目录下生成配置 .browserslistrc, e.g.

// .browserslistrc
Chrome 98

常用配置项

1、 > 5% // 全球超过 5% 的人使用的浏览器,也可以在后面指定[国家代码](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements);  
2last 2 version // 所有浏览器兼容最后两个版本,也可以指定浏览器,e.g. last 1 chrome version
3、dead // 超过24个月没有更新的浏览器,现在特指 IE 10, IE_Mob 11

@babel/plugin-transform-runtime

当 @babel/preset-env 配置了 useBuildIns 后,
1、对于 polyfill 会通过 require 引入:

require("regenerator-runtime/runtime.js");

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

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

引入的文件会修改原型的方式,污染全局变量

2、对于 @babel/core 编译的语法,会有部分直接通过插入别名函数的方式直接插入到代码中,当打包的时候容易出现重复。

transform-runtime 通过模块引入的方式解决了这两个问题,e.g.

// 需要编译的代码
var p = new Promise()
// 编译后的代码
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
var p = new _Promise()

但同时也有新的问题, transform-runtime 打包出来的文件会比 useBuildIns 更大,因为 transform-runtime 不会处理你配置的 targets ,会将所有的 polyfill 都打包进来;

那我们在什么场景下使用 useBuildIns ,什么场景使用 transform-runtime 呢?

1、当你在开发业务应用,非提供给其他人使用的库时,建议使用 useBuildIns;  
2、当你在开发第三方库时,建议使用 transform-runtime ,避免污染;  

问题记录

1、是否可以 userBuildIns 和 transform-runtime 公用,e.g.

["@babel/preset-env", 
  {
    "useBuiltIns": "usage", // "usage" | "entry" | false, default to false ()
    "corejs": 3,
  }
],
"plugins": [
  [
    "@babel/plugin-transform-runtime",
    {
      "corejs": false 
    }
  ]
]

不可以,这样会将corejs 2/3两个版本都打包进去;

参考文章:

1 官网: babeljs.io/docs
2、What is @babel/preset-env and why do I need it?: blog.jakoblind.no/babel-prese… 3、Understanding and properly configuring @babel/env and @babel/transform-runtime: www.jmarkoski.com/understandi…