Skip to content

重新说一说 babel

babel 单词解释

英语单词 Babel 指的是一个城市(古巴比伦)。

据《圣经·创世纪》记载,大洪水后,诺亚的子孙越来越多,遍布地面。他们向东迁移,在古巴比伦附近定居下来。他们彼此商量说,要烧砖为石,拿石漆为灰泥,建造一座城市和一座通天塔,以传扬其名。由于他们语言相通,齐心协力,很快就建成了一座巨大的城市,高塔也直插云霄,快到天上了。

没想到此事惊动了上帝。上帝担心人类办成此事后将狂妄自大,为所欲为,便变乱众人的语言,使大家无法交流,通天塔也因此半途而废。这座城市和塔便被称为 Babel,在英语中比喻“嘈杂声”。

babel 简介

官网上介绍到,Babel 是一个 JavaScript 编译器。

它是一个工具链,主要用来转换 ECMAScript 2015+ 代码,将其变成兼容当前版本和老版本浏览器的 JavaScript 代码。

Babel 能做不止如下牛逼的事情:

  • 转换语法
  • 在你的目标环境中添加没被实现的 API。
  • 源代码转换

babel 概括理解

babel 意味 babel 塔,将混乱的东西变得整洁清晰。它能将 es6+ 代码转为 es5 代码,能识别 ts 代码。

babel 是一系列插件构成的。每一个插件都有自己的职责。比如箭头函数插件,可以将箭头函数转为普通函数。class 插件,可以将 class 类转为由 Function 模拟的类。ts 插件,可以将 ts 的文件及语法转为 js 的文件及语法。

但在实际项目中,一个个的配置多麻烦呀。于是出现了 preset-env 预设。它是一系列插件的集合。

体验 babel

1 在一个空目录下执行 npm init -y 命令,然后下载三个包。

shell
npm init -y

npm install --save-dev @babel/core @babel/cli @babel/preset-env

2 在根目录下创建 babel 配置文件 babel.config.json

json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "> 0.25%, not dead",
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ]
}

3 创建 src/index.js 文件

js
console.log([1, 2, 3].map(n => n + 1))

4 执行命令 npx babel src --out-dir lib

改变 targets 选项,查看转换后的 index.js。

image.png

学习使用预设 preset-env

在上面体验 babel 中,我们使用到了预设 preset-env,并且对其选项进行了配置。

babel 的插件有很多,而 preset-set 是一些插件的集合。

现在我们来学习一下如何使用 preset-env。

在上面体验 babel 的时候,我们已经下载了 @babel/preset-env 包,所以不需要再下载。

preset-env 的默认配置

preset-env 全部默认配置如下。但是最重要的配置就三个,我们主要了解这三个就可以了。

js
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {},
        // "bugfixes": false,
        // "spec": false,
        // "loose": false,
        // "modules": "auto",
        // "debug": "auto",
        // "include": [],
        // "exclude": [],
        "useBuiltIns": false,
        "corejs": "2.0",
        // "forceAllTransforms": false,
        // "configPath": process.cwd(),
        // "ignoreBrowserslistConfig": false,
        // "browserslistEnv": undefined,
        // "shippedProposals": false
      }
    ]
  ]
}

targets

targets 为默认值空对象 {} 时,将会把所有 ES6 语法转换为 ES5 语法。

怎么判断哪些是 ES5 语法,哪些是 ES6 语法呢?

ES6 比 ES5 增加了很多特殊的方法,如果你遇到了这些特殊的方法,你就可以确定它是 ES6。

如果你的代码中没有引用这些特殊的方法,那我们就可以认为他是 ES5 的。

所以前提你需要了解 ES6 的语法才能做判断,高频使用的特性有箭头函数、解构赋值、let、const 等等。

这里推荐两本书,更好的区分 ES5 代码和 ES6 代码。一本是阮一峰的 ES6 入门教程,一本是 JavaScript 教程 https://wangdoc.com/javascript/

下面这行代码

js
console.log([1, 2, 3].map(n => n + 1))

将会被转为如下代码。因为箭头函数是 ES6 新增的新特性,而其它都是 ES5 就有的东西。

js
"use strict";

console.log([1, 2, 3].map(function (n) {
  return n + 1;
}));

useBuiltIns

useBuiltIns 选项告诉 @babel/preset-env 怎么处理垫片 polyfill。注意它是不会影响 preset-env 怎么处理 ES6 的基本语法的。

当设置为 false 时,不会为每个文件自动添加 polyfill。也就是说,只会转换一些基本的语法,比如 let,const,箭头函数等。而如果你使用了一些需要 polyfill 的 特性,比如 Promise,则不会进行处理。

比如下面这行代码

js
console.log([1, 2, 3].map(n => n + 1))
console.log(new Promise())

转换后并没有对 Promise 进行垫片处理

js
"use strict";

console.log([1, 2, 3].map(function (n) {
  return n + 1;
}));
console.log(new Promise());

corejs

默认为 2.0。支持所有 core-js 版本。比如 "3.8" 或者 "2.0"。

这个选项仅仅会在 useBuiltIns 选项设置为 usage 或 entry 时生效。它的作用是控制 @babel/preset-env 注入 polyfill 时所引入的 core-js 版本。

取默认值或 2 的时候,Babel 转码的时候使用的是 core-js@2 版本(即 core-js 2.x.x)。因为某些新 API 只有 core-js@3 里才有,例如数组的 flat 方法,我们需要使用 core-js@3 的 API 模块进行补齐,这个时候我们就把该项设置为 3。

为进行演示,把配置文件改一下:

diff
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {},
+       "useBuiltIns": "usage",
        "corejs": "2.0"
      }
    ]
  ]
}

再对一下代码进行转换

js
console.log([1, 2, 3].map(n => n + 1))
console.log(new Promise())

转换结果如下。可以看到 polyfill 是从 core-js 包引入的。

js
"use strict";

require("core-js/modules/es6.array.map.js");
require("core-js/modules/es6.object.to-string.js");
require("core-js/modules/es6.promise.js");
console.log([1, 2, 3].map(function (n) {
  return n + 1;
}));
console.log(new Promise());

除此之外,如果将配置文件中的 corejs 选项去掉,控制台还会警告:

diff
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "chrome 44",
        "useBuiltIns": "usage",
-       "corejs": "3.8"
      }
    ]
  ]
}

image.png

大意就是叫我们,当 useBuiltIns 设置为 usage 或则 entry 时最好指定 corejs 选项版本。(在 package.json 里 core-js 的版本。)否则 corejs 默认指定 2.x 版本。

学习使用插件 plugin-transform-runtime

默认配置

使用 plugin-transform-runtime 插件默认开启什么功能?

1 将每个文件里生成定义的辅助函数转换为从包 @babel/runtime 里引入的辅助函数

2 转换 async/await 函数,并且不污染全局作用域

以下的配置是默认的。可以看到参数 helpers、regenerator 配置是默认开启的,为 true。

json
{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": false,
        "helpers": true,
        "regenerator": true
      }
    ]
  ]
}

辅助函数是什么?

babel 在转换某些语法的时候,会在转换后的代码里注入一些函数。比如转换 ES6 的 class 语法,会顺带生成一些内联辅助函数,如下:

转换前

js
class a {

}

转换后

diff
"use strict";

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, _toPropertyKey(descriptor.key), descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var a = /*#__PURE__*/_createClass(function a() {
  _classCallCheck(this, a);
});

而如果应用了 plugin-transform-runtime 插件并使用其默认配置,转换结果如下

diff
"use strict";

+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
+ var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
+ var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var a = /*#__PURE__*/(0, _createClass2.default)(function a() {
  (0, _classCallCheck2.default)(this, a);
});

corejs 配置选项

corejs 配置能干什么?有哪些注意点?

corejs 有三个选项。false、2、3。默认值为 false。

为 false 时,辅助函数会从 包 @babel/runtime 里引入。

为 2 时,辅助函数会从包 @babel/runtime-corejs2 里引入,并且也会从这个包里引入 polyfill 实现垫片功能。

为 3 时,辅助函数会从包 @babel/runtime-corejs3 里引入,并且也会从这个包里引入 polyfill 实现垫片功能。

runtime-corejs2 和 runtime-corejs3 有什么区别?

runtime-corejs2 只会垫片全局变量(比如 Promise)和静态属性(比如 Array.from)

runtime-corejs3 支持 2 的所有垫片功能,还支持实例属性(比如 [].includes 方法)

plugin-transform-runtime 和 preset-env 区别

plugin-transform-runtime 和 preset-env 有何区别?应用场景是什么?

preset-env 和 plugin-transform-runtime 都能实现 polyfill 功能。但 preset-env 会转换 ES6 语法。

preset-env 引入的 polyfill 会污染全局空间,会根据目标环境选择性的将 ES6 特性转为 ES5 的。

plugin-transform-runtime 引入的 polyfill 不会污染全局空间,会将代码中用到的全部 ES6 特性转为 ES5 的。

建议

如果你写的代码会作为库给其他人使用,或者你不知道你的代码将会在哪个目标环境中运行,推荐使用 plugin-transform-runtime。因为它引入的 polyfill 不会污染全局作用域。

如果你是在写业务代码,可以直接使用 preset-env 提供的 polyfill。虽然它污染了全局作用域,但其实影响不大。

至于 preset-env 的 useBuiltIns 该选择 usage 还是 entry,可以参考这篇文章:

「前端基建」探索不同项目场景下 Babel 最佳实践方案 https://juejin.cn/post/7051355444341637128

babel 转换 ts 文件为 js 文件

需要使用预设 preset-typescript。

下载

shell
npm install --save-dev @babel/preset-typescript

新增配置。对于多个 preset,babel 会按照其声明次序逆序执行。

diff
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "chrome 44",
        "useBuiltIns": "usage",
        "corejs": "3.8"
      }
    ],
+   ["@babel/preset-typescript"]
  ]
}

新增 src/main.ts 文件

ts
const a: number = 123
console.log(a);

目录截图

处理 ts 文件目录截图.png

如果要养 babel 处理 .ts 文件,cli 必须加上参数 --extensions ".ts"

所以我们执行脚本命令 npx babel src --out-dir lib --extensions ".ts"

Released under the MIT License.