Node 环境使用 ESM
概念
名词解释:
-
模块化:将代码拆分成多个文件(即模块)。
模块化的好处是:
- 解决变量名冲突问题
- 将代码拆分,能够整理成可复用、易维护、可读性强的代码块
- 自动处理依赖关系,不会造成混乱
- 抽离公共代码
- 隔离作用域,不会污染全局变量
模块的核心是:
- 拆分:将代码拆分成多个可复用的模块
- 加载:通过指定方式加载模块并执行
- 注入:将一个模块的输出注入到另一个模块中
- 管理:管理各个模块间的依赖关系
-
ESM:ES6 模块化方案 —— 常用
import、export等引入、导出模块 -
CJS:CommonJS 模块化方案 —— Node 服务端环境采用方案,常用
required来引入模块
CJS 和 ESM 的主要区别
| - | CJS | ESM |
|---|---|---|
| 语法类型 | 动态 | 静态 |
| 关键声明 | require | export与import |
| 加载方式 | 运行时加载 | 编译时加载 |
| 加载行为 | 同步加载 | 异步加载 |
| 书写位置 | 任何位置 | 顶层位置 |
| 指针指向 | this指向当前模块 | this指向undefined |
| 执行顺序 | 首次引入时加载模块 再次引入时读取缓存 | 引入时生成只读引用 执行时才是正式取值 |
| 属性引用 | 基本类型属于复制不共享 引用类型属于浅拷贝且共享 | 所有类型属于动态只读引用 |
| 属性修改 | 工作空间可修改引入的值 | 工作空间不可修改引入的值 但可通过引用的方法修改 |
- 运行时加载指整体加载模块生成一个对象,再从对象身上获取所需的属性和方法。最大特点是全局加载,只有运行时才能得到该对象
- 编译时加载指直接从模块中获取所需的属性和方法。最大特性是按需加载。在编译时就完成模块加载,效率比其他方案高,无法引用模块本身(
本身不是对象),但可拓展 JS 高级语法(类型校验)。
高版本 Node 解决方案
web 端已大量采用 ESM 编码,但由于历史原因, Node 服务端官方是采用 CJS 编码的,使用ESM编码时需要做一些细微的处理。
对于Node v13.2.0版本及以上的 Node 环境,只需要做以下操作之一即可实现 ESM
- 使用
type指定模块方案:- 在
package.json中指定type为commonjs,则使用CJS - 在
package.json中指定type为module,则使用ESM
- 在
- 使用
--input-type指定入口文件的模块方案,与type一样:- 命令中添加参数
--input-type=commonjs,则使用CJS - 命令中添加参数
--input-type=module,则使用ESM
- 命令中添加参数
- 支持新扩展名:
- 文件后缀名使用
.cjs,则使用CJS - 文件后缀名使用
.mjs,则使用ESM
- 文件后缀名使用
除此之外还有一些新特性:
- 使用
--es-module-specifier-resolution指定文件名称引入方式:- 命令中添加参数
--es-module-specifier-resolution=explicit,则引入模块时必须使用文件扩展名(默认) - 命令中添加参数
--es-module-specifier-resolution=node,则引入模块时无需使用文件扩展名
- 命令中添加参数
- 在
package.json中指定main后会根据type指定模块方案加载文件
示例
bash1$ mkdir node-demo 2$ cd node-demo 3$ code . 4$ npm init
一直按回车后生成 package.json
json1{ 2 "name": "demo", 3 "version": "1.0.0", 4 "description": "", 5 "main": "index.js", 6 "scripts": { 7 "test": "echo \"Error: no test specified\" && exit 1" 8 }, 9 "author": "qiuyanxi", 10 "license": "ISC" 11}
然后新建 main.js文件和 utils.js文件
utils.js
js1export const user = { name: 'qiuyanxi' }
main.js
js1import { user } from './utils' 2 3console.log(user)
执行命令:
js1$ node main.js
会出来以下报错:
js1(node:7248) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension. 2(Use `node --trace-warnings ...` to show where the warning was created) 3/Users/qiuyanxi/Desktop/node-demo/main.js:1 4import { user } from './utils'; 5^^^^^^
根据提示,在 package.json写入 "type": "module" 字段,或者将 main.js后缀改成 .mjs。
此时重新执行命令,会报错:
js1node:internal/process/esm_loader:94 2 internalBinding('errors').triggerUncaughtException( 3 ^ 4 5Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/qiuyanxi/Desktop/node-demo/utils' imported from /Users/qiuyanxi/Desktop/node-demo/main.js 6Did you mean to import ../utils.js?
这是因为我们引入时没有写.js后缀名。
修改命令:
js1$ node --es-module-specifier-resolution=node main.js
此时就没有问题了。
这段命令可以写到package.json的scripts属性中:
diff1 "scripts": { 2 "test": "echo \"Error: no test specified\" && exit 1", 3+ "start": "node --es-module-specifier-resolution=node main.js" 4 },
此时执行以下命令,也会有一样的效果。
js1$ npm start
为了让Node支持ESM,我们还需为其指定Node/Npm版本限制。这是为了避免预设和实际情况不同而报错,比如说预设该项目在高版本中运行,实际却在低版本中运行。
Node与Npm是成双成对地安装,可通过Node Releases查询到Node v13.2.0对应Npm v6.13.1。
json1{ 2 "type": "module", 3 "engines": { 4 "node": ">=13.2.0", 5 "npm": ">=6.13.1" 6 } 7}
低版本 Node 解决方案
低版本Node不支持 ESM。
而且Npm 包很多都是用 CJS 编码,同时使用require和export/import会报错,所以有些模块可能没办法使用 ESM。
对于这个问题,我们需要用babel将代码从ESM转化成CJS。
步骤:
-
安装
babel相关模块bash1npm install @babel/cli @babel/core @babel/node @babel/preset-env -dev- @babel/cli - 提供
@babel/core的命令行环境 - @babel/core - 提供转译函数
- @babel/node - 提供支持
ESM的命令行运行环境 - @babel/preset-env - 语法转换的预设环境
- @babel/cli - 提供
-
修改
package.jsonjson1 "scripts": { 2 "start": "babel-node src/index" 3 }, 4 "babel": { 5 "presets": [ 6 "@babel/preset-env" 7 ] 8 } -
运行
npm start
这个方案下,我们不需要设置package.json的type与engines字段了。
一句话总结这个方案就是开发用 ESM,编译后的代码依然是 CMJ。
添加 nodemon
nodemon 是自动检测项目文件发生变化就会重启的 Npm 模块。每次修改代码后,都会自动帮我们执行npm start。
以下以低版本 Node 解决方案为例:
安装:
bash1npm install nodemon -dev
修改package.json:
json1"scripts": { 2 "start": "nodemon -x babel-node src/index" 3 }, 4"nodemonConfig": { 5 "env": { 6 "NODE_ENV": "dev" 7 }, 8 "execMap": { 9 "js": "node --harmony" 10 }, 11 "ext": "js json", 12 "ignore": [ 13 "dist/" 14 ], 15 "watch": [ 16 "src/" 17 ] 18 }
Node 环境下使用 TypeScript
按照上面的步骤就可以在 Node 环境下运行 TypeScript并直接使用ESM 编码,相当于低版本 Node +nodemon的解决方案。
TypeScript 会直接根据tsconfig.json中的module来对代码进行编码并转译成对应的JS代码。
例如,默认的module为commonjs,那么会生成commonjs的代码。
示例代码:
src/index.ts
typescript1import user from '../utils' 2console.log(user)
utils.ts
typescript1const user = { name: 'qiuyanxi' } 2export default user
转译成:
src/index.js
js1'use strict' 2exports.__esModule = true 3var utils_1 = require('../utils') 4console.log(utils_1['default'])
Utils.js
js1'use strict' 2exports.__esModule = true 3var user = { name: 'qiuyanxi' } 4exports['default'] = user
Node 使用 ESM 产生的差异
Node 环境使用 ESM 后会有以下差异:
- 不能使用
__dirname、__filename - 不能使用
require、module和exports - 不能引入
json文件
解决方法:
-
__dirname、__filename使用import.meta.url重建js1import { dirname } from 'path' 2import { fileURLToPath } from 'url' 3 4const __filename = fileURLToPath(import.meta.url) 5const __dirname = dirname(__filename)如果是找项目的根目录,则可以使用
process来完成js1process.cwd() -
require、module和exports使用import、export语法代替 -
json文件通过fs模块来引入js1import { readFileSync } from 'fs' 2 3const json = readFileSync('./info.json') 4const info = JSON.parse(json)