NodeJS:在同一个项目中加载 ES 模块和原生插件

问题描述

在实际问题之前(见文末),请让我通过一个例子展示导致该问题的步骤:

创建项目

tests$ mkdir esm && cd esm
tests/esm$ nvm -v
0.37.2
tests/esm$ nvm use v15
Now using node v15.6.0 (npm v7.5.6)
tests/esm$ node -v
v15.6.0
tests/esm$ npm -v
7.5.6
tests/esm$ npm init
package name: (esm) test-esm
entry point: (index.js)

安装 nodehun

tests/esm$ npm install nodehun
added 2 packages,and audited 3 packages in 11s
tests/esm$ npm ls
test-esm@1.0.0 tests/esm
└── nodehun@3.0.2
  • nodehun here 的依赖项

index.js

import { suggest } from './checker.js'
suggest("misspeling");

checker.js

import Nodehun  from 'nodehun'
import fs from 'fs';

const affix       = fs.readFileSync('dictionaries/en_NZ.aff')
const dictionary  = fs.readFileSync('dictionaries/en_NZ.dic')
const nodehun     = new Nodehun(affix,dictionary)

export const suggest = (word) => hun_suggest(word);

async function hun_suggest(word) {
  let suggestions = await nodehun.suggest(word);
  console.log(suggestions);
}

获取所需的 Hunspell 词典 文件affix 和 dictionary):

tests/esm$ mkdir dictionaries && cd dictionaries
tests/esm/dictionaries$ curl https://www.softmaker.net/down/hunspell/softmaker-hunspell-english-nz-101.sox > en_NZ.sox
tests/esm/dictionaries$ unzip en_NZ.sox en_NZ.aff en_NZ.dic

运行项目

根据 nodejs 文档 (Determining Module System) 支持 import / export

Node.js 在作为初始输入传递给 node 或被 ES 模块代码中的 import 语句引用时,会将以下内容视为 ES modules: • 当最近的父 .js 文件包含值为 package.json 的顶级 "type" 字段时,以 "module" 结尾的文件

我们在项目的 "type": "module" 文件添加 package.json 字段。

package.json

{
  ...
  "main": "index.js","type": "module",...
}

第一次失败的运行

tests/esm$ node index.js
TypeError [ERR_UNKNowN_FILE_EXTENSION]: UnkNown file extension ".node" for tests/esm/node_modules/nodehun/build/Release/Nodehun.node
... omitted ...
at async link (node:internal/modules/esm/module_job:64:9) {
  code: 'ERR_UNKNowN_FILE_EXTENSION'
}

挖掘一下上述错误的原因:

  • 在关于如何load addons的文档中,它指的是使用require

已编译的插件二进制文件文件扩展名是 .node(与 .dll.so 相反)。编写 require() 函数以查找具有 .node 文件扩展名的文件并将其初始化为动态链接库。

  • 一旦您将节点项目定义为 "type": "module"requireceases to be supported(如与 Commonjs 的互操作性中所述):

不支持使用 require 加载 ES 模块,因为 ES 模块具有异步执行。相反,使用 import() 从 Commonjs 模块加载 ES 模块。

临时解决方

搜索文档一段时间后,我找到了一个临时解决方Customizing ESM specifier resolution algorithm

当前的说明符解析不支持 Commonjs 加载器的所有认行为。行为差异之一是文件扩展名的自动解析以及导入具有索引文件的目录的能力。 --experimental-specifier-resolution=[mode] 标志可用于自定义扩展解析算法。 要启用自动扩展解析并从包含索引文件的目录导入,请使用 node 模式。

tests/esm$ node --experimental-specifier-resolution=node index.js
(node:XXXXX) ExperimentalWarning: The Node.js specifier resolution in ESM is experimental.
(Use `node --trace-warnings ...` to show where the warning was created)
[
  'misspelling','misspending','misspeaking','misspell','dispelling','misapplier','respelling'
]

有一些帖子达到了相同的分辨率(ref 1ref 2)。 但是,使用实验性标志似乎不是在生产环境中运行应用程序的正确方法

失败的 esm 包替代方案

从那时起,为了避免使用 --experimental-* 标志,尝试了几次失败的尝试。经过一番搜索,我发现了一些推荐使用 esm 包的帖子(ref 1ref 2)。

  • esm 每周下载 130 万次。
  • 根据 GitHub 中的自述文件,它不需要任何更改。

然而,此时,当我尝试这个node -r esm index.js时,出现了一个错误

tests/esm$ npm install esm
added 1 package,and audited 4 packages in 709ms
tests/esm$ npm ls
test-esm@0.1.0 tests/esm
├── esm@3.2.25
└── nodehun@3.0.2
tests/esm$ node -r esm index.js
tests/esm/index.js:1
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: tests/esm/index.js
    at new NodeError (node:internal/errors:329:5)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1125:13) {
  code: 'ERR_REQUIRE_ESM'
}

上述情况可能是由于报告的问题 (Error [ERR_REQUIRE_ESM]: Must use import to load ES Module / require() of ES modules is not supported)。

  • proposed patch 可以修复它,虽然我自己不知道如何使用它。
const module = require('module');
module.Module._extensions['.js'] = function(module,filename) {
  const content = fs.readFileSync(filename,'utf8');
  module._compile(content,filename);
};

问题

  1. 是否有一种(标准)方法可以使用 import / export(ES 模块)而不会导致 import 插件 出现问题?
    • 避免使用 --experimental-specifier-resolution=node 标志。
  2. 也许 esm 可以解决上述问题。使用 esm 包有什么问题吗?

任何有助于解决它的提示将不胜感激。

注意:可以从https://github.com/rellampec/test-esm.git

克隆示例的最终状态

解决方法

经过一番胡说八道试图找出这个问题的根本原因。

使用 node -r esm index.js 时,esm 包已经为您完成了所有工作(如其他答案中所述),因此(其他答案中未提及):

  • package.json 应通过删除 "type:" "module" 进行更新(因为它会在原生 node ES 模块功能和 esm您安装的软件包)

旁注:如果您尝试使用 node ES Modules 然后尝试切换到 esm 包,很容易错过这一点。 >

,

我遇到了类似的问题并通过以下方式修复了它: https://nodejs.org/api/module.html#module_module_createrequire_filename

// The project is "type": "module" in package json
// createRequire is native in node version >= 12
import { createRequire } from 'module'; 
import path from 'path'; 
// Absolute path to node modules (or native addons)
const modulesPath = path.resolve(process.cwd(),'node_modules');
// Create the require method
const localRequire = createRequire(modulesPath);
// require the native add-on
const myNativeAddon = localRequire('my-native-addon');