ES Modules in Node.js
ES Modules 作为 JavaScript 语言层面的标准,它会逐渐取代 JavaScript 领域内所有模块化的需求。
Node.js 作为 JavaScript 一个重要运行环境,它已经开始逐渐支持这种特性了。
支持情况
在 node.js8.5 版本后,内部就开始以实现特性的方式支持 ES Module 了。所以现在可以直接在 node.js 中编写 ES Module 的代码。
在 node.js 中使用 ES Module,首先需要将文件的扩展名设置为 mjs。
创建一个 module.mjs 文件。
export foo = 'foo' export bar = 'bar'
再创建一个 index.mjs 文件。
import { foo, bar } from "./module.mjs"; console.log(foo, bar);
在命令行运行 index.mjs 时需要加上 --experimental-modules
参数,表示启用 ES Modules 实验特性。
node --experimental-modules index.mjs
虽然这两个成员会被正常打印,但同时也会有一句警告。
(node:7586) ExperimentalWarning: The ESM module loader is experimental.
意思就是告诉我们 ESM module 属于实验特性,尽量不要在生产环境去使用。
上面就是在 node.js 直接使用 ES Module 需要做的两件事情。
导入内置模块和第三方模块
node.js 内置模块可以正常导入,没有任何问题。
import fs from "fs"; fs.writeFileSync("./foo.txt", "es module working");
第三方模块也可以这样导入,比如 lodash。
首先安装一下 lodash。
npm i lodash
编写测试代码。
import _ from "lodash"; console.log(_.upperCase("es module"));
运行代码,发现它是可以正常工作的。
再尝试一下导出 upperCase。
import { upperCase } from "lodash"; console.log(upperCase("es module"));
会得到一个错误。
SyntaxError: The requested module 'lodash' does not provide an export named 'upperCase'
原因也很简单。
lodash 只提供了一个 default 的默认导出,而没有将所有的成员全部单独导出。
所以我们也只能通过 import default 的方式导入默认成员。
很多第三方模块都是这样,因为它们都只提供了 CommonJS 标准的模块。
但是 node.js 内置模块对导出做了兼容,可以使用命名成员的方式单独导出。
import { writeFileSync } from "fs"; writeFileSync("./foo.txt", "es module working");
上面的代码是可以正常工作的。
ES Module 与 CommonJS 模块交互
因为很多第三方模块提供的都是 CommonJS 标准的模块,所以就会面临 ES Module 和 CommonJS 交互的情况。
ES Module 导入 CommonJS
在 ES Module 中导入 CommonJS 和导入 ES Module 没有太大区别。
编写 commonjs.js。
module.exports = { foo: "hello world", };
编写 esm.js,导入 commons.js 中导出的模块。
import mod from "./commonjs.js"; console.log(mod);
运行 esm.js,可以正常拿到 commonjs.js 中导出的对象。
在 commons.js 中,是无法实现 ES Module 命名导出的。
exports.foo = "hello world";
上面这种导出方式仍然是一个 default 导出。
在 ES Module 中仍然无法命名导入。
import { foo } from "./commonjs.js";
这会得到一个错误信息,提示我们没有导出一个名为 foo 的成员。
CommonJS 导入 ES Module
在 node.js 环境中,不允许 CommonJS 直接导入 ES Module。
修改 esm.js。
export const foo = "hello world";
修改 common.js
const mod = require("./esm.mjs"); console.log(mod);
运行 common.js,会得到一个错误。
Must use import to load ES Module
如果要在 node.js 中使用 CommonJS 导入 ES Module,需要使用 webpack 之类的打包工具。
ES Module 与 CommonJS 的差异
CommonJS 提供了几个模块内置的全局成员,类似于全局变量。
创建 cjs.js 文件,并输出这几个全局成员。
// 加载模块函数 console.log(require); // 模块对象 console.log(module); // 导出对象别名 console.log(exports); // 当前文件绝对路径 console.log(__filename); // 当前文件所在目录 console.log(__dirname);
运行之后,没有任何问题。
再创建一个 esm.mjs 文件,把 cjs.js 中的内容复制过去,通过 node --experimental-modules esm.mjs
来运行。
会得到一个错误。
ReferenceError: require is not defined
使用了 ES Module 载入模块后,这些全局成员全部都无法获取了。
require、module 和 exports 在 ES Module 中没有意义,但 __filename 和 __dirname 还是需要的。
通过 import.meta.url 拿到当前的文件绝对路径,再通过 nodejs 内置的 fileURLToPath 和 dirname 函数,就可以获取到 __filename 和 __dirname 了。
import { fileURLToPath } from "url"; import { dirname } from "path"; const __filename = fileURLToPath(import.meta.url); console.log(__filename); const __dirname = dirname(__filename); console.log(__dirname);
新版本的进一步支持
在 nodejs 最新的版本(v12+)中,对 ES Module 支持度更高了。
可以通过在 package.json 中添加 type:module 属性来提高对 ES Module 的支持。
{ "type": "module" }
这样项目下所有的 js 文件都会按照 ES Module 的方式执行,但是 --experimental-modules
参数不可以省略。
如果开启了这个特性,那么以 Commonjs 编写的模块也会以 ES Modules 的模式来运行,这样并不会识别 require 等函数,所以会报错。
const fs = require("fs"); fs.writeFileSync("./foo.txt", "es module working");
这种情况下,就需要将 CommonJS 标准规范的模块后缀名改为.cjs。
Babel 兼容方案
在早期的 nodejs 版本中,并不支持 ES Module。这时就需要借助 babel 来让 ES Module 转换为 CommonJS 代码。
可以安装一个低版本的 node,或者使用类似 nvm 或 n 的工具来设置一个低版本的 node。
在项目中安装 babel 和 babel 相关的插件。
npm i -D @babel/node @babel/core @babel/preset-env
只是安装了 babel 是不够的,因为 babel 并不会自动帮助我们转换代码,所以还需要自己编写 babel 的配置,在项目根目录下创建.babelrc 文件。
{ "presets": ["@babel/preset-env"] }
再去运行 ES Module 的代码,就可以正常工作了。
npx babel-node index.js
@babel/preset-env 是一个 ES6 转 ES5 的插件集合,我们也可以通过单个的插件去转换。
比如使用@babel/plugin-transform-modules-commonjs。
安装。
npm i -D @babel/plugin-transform-modules-commonjs
修改配置。
{ "presets": ["@babel/plugin-transform-modules-commonjs"] }
这样的话,编译速度可能会更快一些。
写在最后
前端模块化是前端工程化的第三块内容,到此讲解结束,接下来会发布另外几篇关于前端工程化的文章,敬请期待。