TS 中的 ES Module 和 Commonjs
你是否遇到过以下问题? 本文来为你解答。
require() of ES Module [file path] from [file path] not supported
To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
exports is not defined in ES Module scope
Nodejs中的ES Module
一切问题的开始,node-plop
库。
node-plop是一个将handlebar模版转换成对应文件的工具库。
新建了一个ts项目,一切都那么正常,直到当我使用node-plop库时
编译..运行,你会发现不能使用 require() 在es module中
是因为默认ts项目在编译时会打包成commonjs模块,而 node-plop 是一个只支持 es module 的库,当然和他类似的库node-fetch、nanoid、chalk等等。
这块也是因为ecmaScript 中 es module 的标准逐渐被社区认同,nodejs 整体还处于一个cjs、esm兼容的过程,有些库比较“激进”只支持es module,而出现的兼容问题。
nodejs 在 v13.2.0 版本支持了 es module 方式。可以使用
--experimental-module
开启
经过经过多天的研究,目前有两种方式(在使用tsc编译的情况下)
- 不优雅降级:比如 node-plop 是从 3.0.0 开始支持 es module,只要降级到3.0.0之前,就可以work。属于是时代的倒退。
- 向下不兼容:既然他们都“激进”了,你又必须要使用这个库又不能改变他们,那么就加入。
所以,我选择第二种,改成es module的方式。
来的如何在node环境中使用es module的环境,也有两种方式:
To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
-
在 package.json中设置
"type": "module"
-
显式声明
.m[t|j]s
后缀
使用 方式1 在 package.json中设置 "type": "module"
。新的问题来了,es module 中没有 exports。
项目是使用的tsc编译的,上面也提到默认会编译成commonjs模块,ts 编译成什么模块是由 module 来决定的
- CommonJS: node 提出的模块化标准
- UMD、AMD
- ES2015 ES6+系列、ESNext:es module
- node16/nodenext: TS 4.7 提出,为了更好的兼容 es module 和 commonjs 模块化,输入的模块取决于文件名(.cjs、.mjs) 或 package.json 中的type字段是("commonjs"、"module")
更改 module 通常需要更改 moduleResolution 配合,moduleResolution 是指ts文件中导入和导出的解析策略
-
node16 和 nodenext:从 node v13.2.0 版本,node 支持了 es module 方式。
-
node10(alias node): commonjs
-
bundler:ts5新特性,结合第三方构建工具使用。(TODO
我们将tsconfig中的module和moduleResolution字段改成node16
当我们使用node16或者nodenext时,文件引入必须强制写后缀
nodejs对以上问题也有说明:
至此问题就解决了,产生的后果:
-
下游使用者必须也要使用es module的方式导入导出
-
在写 ts 时相对路径的导入必须指定.js 、.mjs后缀(: 写个 ts,还要写 js 后缀!! 毁灭吧
使用方式2同样也能解决问题:
在 esm 中使用commonjs
在commonjs中使用esm会出现阻塞性的问题,那么反过来会不会也有问题呢?
如何使用 tsc 打包出 esm 和 cjs 两种包?
在不考虑上述问题(依赖库只支持es)的情况下,如何使用 tsc 打出esm 和 cjs 两种类型的包。
step 1
刚刚也提到,ts编译成什么模块是通过 tsconfig 中的 module 字段控制的。所以需要有两个tsconfig.json文件
非常简单,配置两份tsconfig文件,在编译时 执行
tsc -p tsconfig.json && tsc -p tsconfig.cjs.json
可以看到 dist 目录下生成了 cjs 和 esm 两个目录,但是注意 cjs/index.mjs
cjs模块下,还带有mjs后缀!这是因为 tsc 编译的时候不会改变“开发者实际开发的内容” ,也就是说,即使你用 commonjs 模块的方式打包得到的也是.mjs
后缀。
how to fix it ?
写一个脚本,去修改cjs的文件后缀以及文本内容中的含.mjs
的内容。当然,如果你也输出了 .d.mts
也需要修改对应的后缀
import path from 'node:path'
import fs from 'node:fs/promises'
import { globby } from 'globby'
const mjsFiles: string[] = await globby(['lib/cjs/**/*.mjs', '!**/node_modules'])
mjsFiles.forEach(async (pathname: string) => {
// 1. Rename imports
const fileContent = await fs.readFile(pathname, 'utf-8')
await fs.writeFile(pathname, fileContent.replace(/require(['"]([^'"]*).mjs['"])/g, "require('$1.js')"))
// 2. Rename files
const newPath = path.format({
...path.parse(pathname),
base: '',
ext: '.js',
})
console.log(`Renaming ${pathname} to ${newPath}...`)
await fs.rename(pathname, newPath)
})
上述方法是我们使用 .mts 的方式去使用es module。如果我们使用"type": "module",在定义文件和导入的时候使用的是 .ts/.js 后缀,那我们编译之后是不是就不需要修改了?
打包出来的结果确实是不含mjs后缀的,但是因为package.json中指定了"type": "module"
导致在解析的时候,默认认为 .js 后缀是使用 es module 的方式运行。你同样需要将 .js 修改成 .cjs 才能正常运行。
打包成两种格式就是为了让自己的库有更好的兼容性,使用者可以根据情况来选择使用 esm 还是 cjs。那么如何配置发包配置呢?
在 package.json 文件中,"exports"、"module" 和 "main" 字段有着不同的作用。
- "exports" 字段是在 Node.js 版本 12 及以上引入的,它用于指定模块的导出方式。导入模块时应该使用 cjs 还是 esm 取决于使用时的导入语法。
- "module" 字段是在 Node.js 版本 8 及以上引入的,它用于指定 ES 模块的入口文件路径。在使用支持 ES 模块的环境中,例如现代浏览器或 Node.js 版本 13 及以上,这个字段可以用来指定默认的模块入口。
- "main" 字段是 Node.js 中常用的字段,它用于指定 CommonJS 模块的入口文件路径。在使用 CommonJS 模块的环境中,例如 Node.js 版本 12 及以下,这个字段可以用来指定默认的模块入口。
{
"main": "lib/cjs/index.js",
"module": "lib/esm/index.mjs"
"typings": "lib/cjs/types/index.d.ts",
"exports": {
".": {
"import": {
"types": "./lib/esm/types/index.d.mts",
"default": "./lib/esm/index.mjs"
},
"require": {
"types": "./lib/cjs/types/index.d.ts",
"default": "./lib/cjs/index.js"
}
}
}
}
转载自:https://juejin.cn/post/7282758586108526592