发布于 2026-01-06 1 阅读
0

Node.js 原生 ESM,支持 require() 回退机制,并兼容所有前端编译器!DEV 全球项目展示挑战赛,由 Mux 主办:快来展示你的项目吧!

Node.js 中的原生 ESM,支持 require() 回退机制,并兼容所有前端编译器!

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

几个月前,Node.js CURRENT 和 LTS 版本中取消了对原生 ESM 的支持。但当我开始深入研究后,发现它比我预想的要困难一些。

我担心的一点是,前端编译器对 ESM 的解析方式与 Node.js 的解析方式可能存在差异。如果我想为浏览器、ESM 和 require 分别设置入口点,它们都需要理解相同的 package.json 属性。

答案是“不!”编译器目前还无法理解Node.js的导出映射。

如果您希望库的使用者能够导入它,require()则需要使用导出映射,此映射将由 Node.js 使用,但对编译器不可见。

这意味着几件事:

  1. 您可能需要{ “type”: “module” }在 package.json 文件中进行设置,以便默认在所有地方使用 ESM。这样,Node.js 会将项目中的 .js 文件解释为 ESM,编译器也能在源文件中检测到 ESM。除非您想维护多个实现相同的独立源文件(而您可能并不需要这样做),否则使用 .mjs 文件实际上没有任何好处。

  2. 您将无法按预期的方式使用导出映射,即允许类似这样的操作,import main from ‘packageName/defaults’因为这不是有效的文件路径,并且此映射对编译器不可见。

你可以使用import`<module>` 加载按照旧模块标准编写的 Node.js 模块,但不能加载require()ESM 模块,因此兼容性是单向的。

如果您想支持旧模块格式,则必须有一个单独的源文件,并将其与导出映射中的 ESM 文件叠加require()

以下是js-multiformats的一个示例,它有很多导出项。

 "exports": {
    ".": {
      "import": "./index.js",
      "require": "./dist/index.cjs"
    },
    "./basics.js": {
      "import": "./basics.js",
      "require": "./dist/basics.cjs"
    },
    "./bytes.js": {
      "import": "./bytes.js",
      "require": "./dist/bytes.cjs"
    },
    "./cid.js": {
      "import": "./cid.js",
      "require": "./dist/cid.cjs"
    },
    ...
}

在 @mylesborins 的指导下,用 rollup 编译这些文件非常简单,但我还需要更多。

以下是js-multiformats的另一个示例。

import globby from 'globby'
import path from 'path'

let configs = []

const _filter = p => !p.includes('/_') && !p.includes('rollup.config')

const relativeToMain = name => ({
  name: 'relative-to-main',
  renderChunk: source => {
    while (source.includes("require('../index.js')")) {
      source = source.replace("require('../index.js')", "require('multiformats')")
    }
    while (source.includes("require('../")) {
      source = source.replace('require(\'../', 'require(\'multiformats/')
    }
    return source
  }
})

const plugins = [relativeToMain('multiformats')]
const add = (pattern) => {
  configs = configs.concat(globby.sync(pattern).filter(_filter).map(inputFile => ({
    input: inputFile,
    output: {
      plugins: pattern.startsWith('test') ? plugins : null,
      file: path.join('dist', inputFile).replace('.js', '.cjs'),
      format: 'cjs'
    }
  })))
}
add('*.js')
add('bases/*.js')
add('hashes/*.js')
add('codecs/*.js')
add('test/*.js')
add('test/fixtures/*.js')
console.log(configs)

export default configs

你需要编译所有 .js 文件所有测试。这种转换过程中可能会出现很多问题,因此编译每个测试的版本require()非常有用。它还能确保每个入口点导出的接口保持一致。

你还需要确保在测试中编译掉所有相对导入,并改用本地包名。Node.js 会正确解析本地包名,但如果你使用相对导入,实际上会完全跳过导出映射,从而导致测试失败。

虽然很想把测试从相对导入迁移出去,但编译器通常不支持像 Node.js 那样对本地包名进行查找,所以你做不到。

文章来源:https://dev.to/mikeal_2/native-esm-in-node-js-w-require-fallbacks-and-support-for-all-front-end-compilers-2ded