在 NodeJS 中使用 ES6 模块
当前较新版本的 NodeJS 支持 ESM 和 CJS ,但默认使用的是 CJS 规范去解析 JS 代码,直接使用 CJS 是没有任何问题的,而使用 ESM 需要做一些处理
.mjs 文件
在 NodeJS 中用.mjs 后缀的文件名表示这个文件为 ES6 模快文件,可以在.mjs 文件中直接使用 ESM 语法(使用import/export 指令)。在执行含 ES6 模块的脚本时,由于不同 NodeJS 版本的支持程度不一样,需要按照不同方式执行
v16.4.0 完全支持
v13.2.0 试验性支持,可省略 --experimental-modules
更老的版本需要显式使用参数 --experimental-modules 执行 或 甚至不支持该试验特性
node --experimental-modules <path/to/file>
修改模块规范
可以通过修改 NodeJS 解析 JS 代码时的模块规范,使得 ESM 可以直接在.js 文件中使用(不修改的话直接使用会报错)。在需要修改默认解析规范的模块文件根目录下创建一个package.json 文件(可以在要创建的目录下执行npm init -y 生成package.json ),并将"type" 设置为"module"
{
"type": "module"
}
做了以上配置之后,如果要使用 CJS 语法(使用module.exports/require() ),则需要在.cjs 文件中使用,否则会报未定义的错误
当被认为时 ES6 模块时,就会自动采用严格模式(浏览器端也一样)
ES6 模块 和 CommonJS 模块的相互引用
ESM 是编译时加载(静态加载),有一个独立的模块依赖的解析阶段,模块输出的是接口(引用),它的加载、解析、执行都是异步的;CJS 是运行时加载(动态加载),模块输出的是值(模块对象),值会被缓存,它的加载、解析、执行都是同步的(require() 为一个同步方法)。
由上述对两种规范的描述可见, ESM 和 CJS 规范的实现是不一样的,如果想在 ES6 模块中引入 CJS 模块或在 CJS 模块中引入 ES6 模块,则需要遵循以下规则
CJS 模块加载 ES6 模块:
由于 ES6 为异步加载,CJS为同步加载,不能直接用require() 去加载 ES6 模块,只能利用同样是异步加载且 CJS 和 ESM 都支持的import() 来加载
import('....').then(res => {});
(async () => {
await import('....');
}())
ES6 模块加载 CJS 模块:
ESM 的import 可以直接加载 CJS 模块,但只能整体加载。因为 ES6 模块需要支持静态代码分析,而 CommonJS 模块的输出接口是module.exports ,是一个对象,无法被静态分析,所以只能整体加载
import { a } from "./foo.cjs";
import m from "./foo.cjs";
const { a } = m;
require() 不能在.mjs 文件中使用(只能使用import 或import() ),也不能用于加载.mjs 文件(即上述的不能加载 ES6 模块)
另外值得注意的是,在使用 webpack 打包的项目中,同一个文件中不建议 ESM 和 CJS 混用,babel 转译能让 ESM 变为 CJS,这往往可能会导致出现一些莫名其妙的错误
import m from "./foo.js";
module.exports = {
m
}
https://github.com/xiaoxiaojx/blog/issues/27 https://www.tangshuang.net/7686.html
模块(包)加载规则
默认情况下(不通过package.json 的exports 选项设置别名的情况下),import 命令加载一个不在node_modules 下的模块时,除非是系统模块,否则不能省略后缀名,必须给出完整的文件路径
import "./foo/index.js";
import "./foo";
但对于 CJS 没有这些限制
require("./foo")
NodeJS 完全支持的 CJS 规范,默认情况下,它的加载规则如下
require("./foo/index.js");
require("./foo");
require("foo");
配置入口文件和路径别名
可以通过package.json 中的main 和exports 配置一些方便引入模块的设置
-
package.json 中的main : 这个是指定模块入口
require("./foo");
require("./foo");
-
package.json 中的exports : exports 的优先级高于main 可以用于设置文件路径别名: {
"exports": {
"./foo": "./utils/foo",
}
}
上述提及,import 指令引入的模块路径必须是完整的,这里可以通过配置起路径别名来简略路径 {
"exports": {
"./foo": "./foo.js",
}
}
此时就可以直接import "./foo" 可以用于设置主入口 main 的路径别名: 用. 表示主入口 {
"exports": {
".": "./index.js",
}
}
{
"exports": "./index.js"
}
由于exports字段只有支持 ES6 的 Node.js 才认识,所以可以用来兼容旧版本的 Node.js
{
"main": "./index.js",
"exports": "./index2.js"
}
可以实现条件加载: 入口文件的别名设置. 中,还有两个子配置require 和default ,分别指定 CJS 的入口和其他入口(包括 ESM) {
"exports": {
".": {
"require": "./index.cjs",
"default": "./index.mjs"
}
}
}
{
"exports": {
"require": "./index.cjs",
"default": "./index.mjs"
}
}
这个功能需要在 Node.js 运行的时候,打开--experimental-conditional-exports 标志 node --experimental-conditional-exports ....
同时支持两种格式的模块
-
指定不同的入口文件 通过上文提到的exports 配置实现 {
"exports": {
".": {
"require": "./index.cjs",
"default": "./index.mjs"
}
}
}
-
CJS 和 ESM 相互转化 CJS 转 ESM:
import m from "./foo.cjs";
export const bar = m.bar;
ESM 转 CJS
let m = null;
(async () => {
m = await import("./foo.mjs");
})()
module.exports = {
...m
}
或者在子目录使用package.json 的"type" 配置当前模块使用的规范,让一个目录为 CJS 格式,另一个目录为 ESM 格式,但终究还是涉及到格式转换,入口文件指定等这些配置,具体可以参考一些现有的包,他们往往都支持两种格式
ES6 模块注意事项
-
import 命令的限制 除了上述提到的import 时路径必须是完整的(包括后缀名),import 的路径也必须是相对路径,不能为绝对路径(浏览器中可以为绝对路径)、只支持加载本地模块 -
为了和浏览器的 import 加载规则相同,NodeJS 的.mjs 文件支持 URL 路径 import "./foo.mjs?query=10086"
同一个脚本只要参数不同,就会被加载多次,并且保存成不同的缓存(没有参数时或参数相同时都是加载多次只执行一次)。由于这个原因,只要文件名中含有: 、% 、# 、? 等特殊字符,最好对这些字符进行转义 -
内部变量 为了使 ES6 模块能够不作任何修改就在服务器和浏览器端通用,NodeJS 规定了在 ES6 模块中不能使用 NodeJS 的一些内部变量 arguments
module
exports
require
__dirname
__filename
参考:
https://es6.ruanyifeng.com/#docs/module-loader https://es6.ruanyifeng.com/#docs/module https://zhuanlan.zhihu.com/p/337796076 https://www.jianshu.com/p/1e42fcabf039
|