Node.js 中的原生 ESM,支持 require() 回退机制,并兼容所有前端编译器!
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
几个月前,Node.js CURRENT 和 LTS 版本中取消了对原生 ESM 的支持。但当我开始深入研究后,发现它比我预想的要困难一些。
我担心的一点是,前端编译器对 ESM 的解析方式与 Node.js 的解析方式可能存在差异。如果我想为浏览器、ESM 和 require 分别设置入口点,它们都需要理解相同的 package.json 属性。
package.json 中新的条件 `exports` 映射在不同编译器中的支持程度如何?/cc @webpack @pikapkg @browserify nodejs.org/dist/latest-v1…2020年6月8日 下午6:24
答案是“不!”编译器目前还无法理解Node.js的导出映射。
如果您希望库的使用者能够导入它,require()则需要使用导出映射,此映射将由 Node.js 使用,但对编译器不可见。
这意味着几件事:
-
您可能需要
{ “type”: “module” }在 package.json 文件中进行设置,以便默认在所有地方使用 ESM。这样,Node.js 会将项目中的 .js 文件解释为 ESM,编译器也能在源文件中检测到 ESM。除非您想维护多个实现相同的独立源文件(而您可能并不需要这样做),否则使用 .mjs 文件实际上没有任何好处。 -
您将无法按预期的方式使用导出映射,即允许类似这样的操作,
import main from ‘packageName/defaults’因为这不是有效的文件路径,并且此映射对编译器不可见。
是我遗漏了什么吗?还是说在 Node.js 中根本无法使用 `require()` 函数引入一个用 ESM 编写的、带有 `{ type: module }` 的模块/文件?
我能找到的都是使用独立入口点的例子,这意味着你需要在 CJS 中实现模块,然后再暴露一个 ESM 入口点。2020年6月19日 下午2:48
你可以使用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