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

NodeJS: loading ES Modules and native addons in the same project

在实际问题之前(见最后),请让我通过示例展示导致该问题的步骤:

正在创建项目

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

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 will treat the following as ES modules when passed to node as the initial input, or when referenced by import statements within ES module code: • Files ending in .js when the nearest parent package.json file contains a top-level "type" field with a value of "module".

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

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'
}

深入了解上述错误的原因:

The filename extension of the compiled addon binary is .node (as opposed to .dll or .so). The require() function is written to look for files with the .node file extension and initialize those as dynamically-linked libraries.

Using require to load an ES module is not supported because ES modules have asynchronous execution. Instead, use import() to load an ES module from a CommonJS module.

临时解决方案

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

The current specifier resolution does not support all default behavior of the CommonJS loader. One of the behavior differences is automatic resolution of file extensions and the ability to import directories that have an index file. The --experimental-specifier-resolution=[mode] flag can be used to customize the extension resolution algorithm. To enable the automatic extension resolution and importing from directories that include an index file use the node mode.

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 1, )。 但是,使用 实验性标志 似乎不是 运行 生产应用程序的正确方法。

使用 esm

的替代方案失败

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

然而,此时,当我尝试这个 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)。

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 addons 的问题?
    • 避免使用 --experimental-specifier-resolution=node 标志。
  2. 也许esm可以解决上述问题。 usage esm 包有什么我做错了吗?
    • 如果它是正确的用法,有没有办法使用 the proposed patch 我自己作为变通方法?

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

注意:示例的最终状态可以从https://github.com/rellampec/test-esm.git

克隆

经过一些尝试找出问题的根源。

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

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

旁注: 如果您尝试使用 node ES 模块然后尝试切换到 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');