node.js开发系列[6]-babel

本文我们介绍如何配置 babel,从而可以让我们在 Node 中书写所有最新的 ES6+ 特性。因此,本文的方法也主要是适用于 Node 的,浏览器端的 babel 编译跟 node 侧是类似的,只是在某些配置的地方有一点小区别。具体可以参考我的 webpack 相关文章。

babel 安装

由于 Node 还没有完全支持所有的 ES 新特性(比如 import export 模块语法),因此如果需要使用最新特性,免不了要使用转换器进行语法转换。(Node 的版本支持情况可以查看:https://node.green/

Node 如果要使用 babel 进行编译,则需要安装一个 babel 编译工具 babel-node 啦。之后要用 babel-node 命令代替 node 命令来运行脚本(这样便会自动转换 ES 语法啦)。而 babel-node 这个命令行程序是内置在 babel-cli 这个包里面的。因此我们需要安装 babel-cli:

1
npm i babel-cli

babel 配置

babel 自身需要依赖各个插件包来告诉 babel 要转换哪些语法特性,因此,我们需要安装一些 plugin 来告诉 babel 如何转换语法。不过茫茫插件,我们如何知道要安装哪些呢? 实际上 babel 提供了一些预设,这些预设可以跟 ES 版本特性进行映射从而让我们方便的选择插件。对于一般的懒人来说,我们直接使用 preset-env 这个预设即可,他默认能根据你指定的环境来决定加载哪些插件。我们先来安装这个预设包:

1
npm install babel-preset-env

然后我们需要配置 babel,告诉它使用哪个插件哪个预设。除了命令行之外,最方面的就是在项目根目录下创建一个 babel 的配置文件 .babelrc。 该文件的配置是一个 json,其中最主要的字段就是 presets,这个就是 babel 需要配置的预设。所谓预设就是 plugin 的集合咯。一般如果很懒的话,直接使用 babel-preset-env 预设即可。

先看下 babelrc 配置文件的格式,如下:

1
2
3
4
5
6
7
8
{
"presets": [
[],
[],
[]
],
"plugins": []
}

可以看到,每个字段写在顶层。我们看下 presets 预设字段,它是一个数组,数组每一项填写一个预设。预设自身的配置要用该项的其他索引表达。例如:

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
["env", {
"targets": {
"node": "current"
}
}],
[],
[]
],
"plugins": []
}

可以看到,env 这个预设允许我们去指定当前代码要运行的目标环境,比如我们可以指定 targets 字段为一个 node 版本。这样 babel 发现代码中有该 node 版本不支持的语法特性时就会进行转译。

配置完成之后,就可以使用 babel-node 命令进行编译了:

1
babel-node ./index.js // 如果你是局部安装,则可能需要使用 npx 或 npm scripts

编译结果解读

我们来写一个使用 ES6 module 模块编写的代码:

1
2
3
// index.js
import util from "util";
console.log(util.promisify);

执行 babel-node -d ./dist 编译, 看下该 ES6 模块的编译结果是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 编译结果 index.js
"use strict";

var _util = require("util");

console.log("_util", _util.__esModule);

var _util2 = _interopRequireDefault(_util);

function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}

console.log(_util2.default.promisify);

可以看到,编译后,本质上还是要用 node 支持的 require 去加载模块,同时 hack 了 es6 的语法。它是使用了一个 _interopRequireDefault 的函数来对引入的 util 模块进行包装。该函数会判断被加载的模块 util:

  • 如果 util 对象上没有 __esModule 属性,说明这是个原生 commonjs 模块(使用 module.exports 导出了默认内容),这时候 util 本身就是默认导出,因此 {default: util} 这样包装。
  • 如果 util 对象上有 __esModule 属性,则说明 util 模块是做了 ES module 的 hack 的(即是被 babel 转译过的 es module 模块)。被 hack 之后的 util 的默认导出会自动放置在 default 属性上。因此直接返回即可。

来看我们的 index.js 自身代码: 我们使用 import util from 'util' 来引用了 util 模块的默认导出。 所以下方的 util.promisify 则被编译为 util.default.promisify

我们来换个写法,我们这次使用个别属性的导入方式来编写 index.js. 看如下代码:

1
2
import { promisify } from "util";
console.log(promisify);

这种方式编译后:

1
2
3
"use strict";
var _util = require("util");
console.log(_util.promisify);

看了这么多调用者的如何 hack 的,我们来看看使用 esmodule 的 export 导出时,babel 是如何 hack 的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 原始代码
let boy = {
a: 2,
};

export default boy; // 默认导出
const aaa = 123;
export { aaa }; // 属性导出。 属性导出跟默认导出的内容毫无关系

// 编译结果
("use strict");

Object.defineProperty(exports, "__esModule", {
value: true,
});
let boy = {
a: 2,
};
exports.default = boy; // 默认导出,是把导出内容挂载在导出对象的default属性上

const aaa = 123;
exports.aaa = aaa; // 属性导出,就把导出的属性挂载在导出对象上

可以发现,babel 是这样用 commonjs 来 hack ES6 模块语法的:

  • 它将 es module 语法的属性导出,作为 commonjs 的 exports 对象上的属性
  • 它将 es module 语法的默认导出,作为 commonjs 的 exports 对象上的 default 属性

这就要 babel 在导入时做一些配合: 通过 require 来的模块对象上是否有 __esModule 属性来判断是否是 hack 过的 commonjs 模块。

  • 如果没有被 hack 过,则这个 commonjs 模块就是一个原始 commonjs 模块(如上文的 util 模块)。 可以认为它只有属性导出,没有默认导出。如果调用者真的使用了 import xx from '' 来寻找他的默认导出; 则 babel 就用 _interopRequireDefault 函数把该模块的整个导出对象作为 default 导出(毕竟一般人也会觉得默认导出就应该是单独导出的属性的集合)。
  • 如果发现被调模块被 hack 过,则 babel 就直接按照自己的机制来加载 hack 过的这个模块: default 导出就直接读取模块的 default 属性,单独导出的就直接读取模块对象的对应属性名即可。