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 | { |
可以看到,每个字段写在顶层。我们看下 presets 预设字段,它是一个数组,数组每一项填写一个预设。预设自身的配置要用该项的其他索引表达。例如:
1 | { |
可以看到,env 这个预设允许我们去指定当前代码要运行的目标环境,比如我们可以指定 targets 字段为一个 node 版本。这样 babel 发现代码中有该 node 版本不支持的语法特性时就会进行转译。
配置完成之后,就可以使用 babel-node 命令进行编译了:
1 | babel-node ./index.js // 如果你是局部安装,则可能需要使用 npx 或 npm scripts |
编译结果解读
我们来写一个使用 ES6 module 模块编写的代码:
1 | // index.js |
执行 babel-node -d ./dist
编译, 看下该 ES6 模块的编译结果是什么:
1 | // 编译结果 index.js |
可以看到,编译后,本质上还是要用 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 | import { promisify } from "util"; |
这种方式编译后:
1 | ; |
看了这么多调用者的如何 hack 的,我们来看看使用 esmodule 的 export 导出时,babel 是如何 hack 的:
1 | // 原始代码 |
可以发现,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 属性,单独导出的就直接读取模块对象的对应属性名即可。