likes
comments
collection
share

TS 中的 ES Module 和 Commonjs

作者站长头像
站长
· 阅读数 10

你是否遇到过以下问题? 本文来为你解答。

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库时

TS 中的 ES Module 和 Commonjs

编译..运行,你会发现不能使用 require() 在es module中

TS 中的 ES Module 和 Commonjs

是因为默认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编译的情况下)

  1. 不优雅降级:比如 node-plop 是从 3.0.0 开始支持 es module,只要降级到3.0.0之前,就可以work。属于是时代的倒退。

TS 中的 ES Module 和 Commonjs

  1. 向下不兼容:既然他们都“激进”了,你又必须要使用这个库又不能改变他们,那么就加入。

所以,我选择第二种,改成es module的方式。

来的如何在node环境中使用es module的环境,也有两种方式:

To load an ES module, set "type": "module" in the package.json or use the .mjs extension.

  1. 在 package.json中设置 "type": "module"

  2. 显式声明.m[t|j]s后缀

使用 方式1 在 package.json中设置 "type": "module"。新的问题来了,es module 中没有 exports。

TS 中的 ES Module 和 Commonjs

TS 中的 ES Module 和 Commonjs

项目是使用的tsc编译的,上面也提到默认会编译成commonjs模块,ts 编译成什么模块是由 module 来决定的

www.typescriptlang.org/tsconfig#mo…

  • 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文件中导入和导出的解析策略

www.typescriptlang.org/tsconfig#mo…

www.typescriptlang.org/docs/handbo…

我们将tsconfig中的module和moduleResolution字段改成node16

TS 中的 ES Module 和 Commonjs

当我们使用node16或者nodenext时,文件引入必须强制写后缀

nodejs对以上问题也有说明:

nodejs.org/docs/latest…

TS 中的 ES Module 和 Commonjs

至此问题就解决了,产生的后果:

  • 下游使用者必须也要使用es module的方式导入导出

  • 在写 ts 时相对路径的导入必须指定.js 、.mjs后缀(: 写个 ts,还要写 js 后缀!! 毁灭吧

使用方式2同样也能解决问题:

TS 中的 ES Module 和 Commonjs

esm 中使用commonjs

在commonjs中使用esm会出现阻塞性的问题,那么反过来会不会也有问题呢?

nodejs.org/docs/latest…

TS 中的 ES Module 和 Commonjs

如何使用 tsc 打包出 esm 和 cjs 两种包?

在不考虑上述问题(依赖库只支持es)的情况下,如何使用 tsc 打出esm 和 cjs 两种类型的包。

step 1

刚刚也提到,ts编译成什么模块是通过 tsconfig 中的 module 字段控制的。所以需要有两个tsconfig.json文件

非常简单,配置两份tsconfig文件,在编译时 执行

tsc -p tsconfig.json && tsc -p tsconfig.cjs.json

TS 中的 ES Module 和 Commonjs

可以看到 dist 目录下生成了 cjs 和 esm 两个目录,但是注意 cjs/index.mjs cjs模块下,还带有mjs后缀!这是因为 tsc 编译的时候不会改变“开发者实际开发的内容” ,也就是说,即使你用 commonjs 模块的方式打包得到的也是.mjs后缀。

TS 中的 ES Module 和 Commonjs

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 后缀,那我们编译之后是不是就不需要修改了?

TS 中的 ES Module 和 Commonjs

打包出来的结果确实是不含mjs后缀的,但是因为package.json中指定了"type": "module"导致在解析的时候,默认认为 .js 后缀是使用 es module 的方式运行。你同样需要将 .js 修改成 .cjs 才能正常运行。

TS 中的 ES Module 和 Commonjs

打包成两种格式就是为了让自己的库有更好的兼容性,使用者可以根据情况来选择使用 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"
        }
      }
    }
}