根据当前运行时解决 JavaScript 导入问题?



有没有办法根据正在使用的JavaScript运行时(Node,Deno,Bun)导入JavaScript/TypeScript模块?

像这样:

if (Bun) {
import MyLib from "./mylib-bun.js";
} else if (Deno) {
import MyLib from "./mylib-deno.js";
} else if (Node) {
import MyLib from "./mylib-node.js";
}

它可以是这样的,特定于平台的导入映射,import runtime=Deno { MyFunction } from "./mylib-deno.js",或者其他任何东西。我只需要一种方法来导入特定于平台的绑定,具体取决于正在使用的 JS 运行时。

虽然当然可以使用"动态"import()语句,如本答案中所述,但通过将模块限制为使用静态import语句来避免这种模式是有好处的。例如,其中一个好处是保留模块图的静态分析,这是由JS生态系统中的许多工具执行的。

一种方法是在预期运行时之上使用抽象编写库代码,并使用功能检测有条件地访问这些运行时的功能。以下是我的意思的一个例子:

env.mjs

export function getEnvAsObject () {
// Bun
if (typeof globalThis.Bun?.env === 'function') {
return globalThis.Bun?.env;
}
// Deno
if (typeof globalThis.Deno?.env?.toObject === 'function') {
return globalThis.Deno?.env?.toObject();
}
// Node
if (typeof globalThis.process.env === 'object') {
return globalThis.process.env;
}
// Handle unexpected runtime
return {};
}

这样,当需要导入和使用运行时,运行时详细信息将从使用者中抽象出来:

module.mjs

import {getEnvAsObject} from './env.mjs';
const env = getEnvAsObject();
console.log(env);
# Run using Bun
bun module.mjs
# Run using Node
node module.mjs
# Run using Deno
deno run --allow-env module.mjs

上面的示例env.mjs模块不使用其他导入作为依赖项,因此非常简单。

在需要涉及其他导入的更复杂的代码的情况下,保留静态分析可能需要根据运行时为库提供不同的入口点。我还将在下面提供一个例子来说明我的意思。

在示例中,假设我们有一个提供两个函数的库。每个都读取磁盘上文件的文本内容。

  • 一个返回文本的大写版本。
  • 一个返回文本的小写版本。

两者都接受取消操作的AbortSignal选项。

在 TypeScript 中,每个函数的函数签名可能如下所示:

(filePath: string, options?: { signal?: AbortSignal }) => Promise<string>

第一步是为每个运行时创建一个单独的模块,这需要不同的导入语句。在该模块中,我们创建了一个通用抽象,用于基于上面的签名读取文本文件。

一个用于 Node,我们从 Node 的fs模块导入:

io.node.mjs

import {readFile} from 'node:fs/promises';
export function readTextFile (filePath, {signal} = {}) {
// Ref: https://nodejs.org/docs/latest-v18.x/api/fs.html#fspromisesreadfilepath-options
return readFile(filePath, {encoding: 'utf-8', signal});
}

一个用于 Bun(这很简单,因为 Bun 使用 Node 的 API,所以我们只是从 Node 模块重新导出):

io.bun.mjs

export * from './io.node.mjs';

一个用于 Deno,其中不需要导入语句,因为此功能位于 Deno 命名空间中:

io.deno.mjs

export function readTextFile (filePath, {signal} = {}) {
// Ref: https://doc.deno.land/deno/stable@v1.24.3/~/Deno.readTextFile
return Deno.readTextFile(filePath, {signal});
}

然后,库函数的实际逻辑可以用设计为柯里化的样式编写一次(我们将在下一步中介绍):

io.mjs

export async function getUpperCaseFileText (readTextFile, filePath, {signal} = {}) {
const text = await readTextFile(filePath, {signal});
return text.toUpperCase();
}
export async function getLowerCaseFileText (readTextFile, filePath, {signal} = {}) {
const text = await readTextFile(filePath, {signal});
return text.toLowerCase();
}

最后,我们可以为每个运行时环境创建一个库入口点。这是核心函数被柯里化和导出的地方:

一个用于节点:

lib.node.mjs

import {readTextFile} from './io.node.mjs';
import {
getLowerCaseFileText as lower,
getUpperCaseFileText as upper,
} from './io.mjs';
export function getLowerCaseFileText (filePath, {signal} = {}) {
return lower(readTextFile, filePath, {signal});
}
export function getUpperCaseFileText (filePath, {signal} = {}) {
return upper(readTextFile, filePath, {signal});
}

一个用于 Bun(同样,只需从 Node 模块重新导出):

lib.bun.mjs

export * from './lib.node.mjs';

还有一个给德诺:

lib.deno.mjs

import {readTextFile} from './io.deno.mjs';
import {
getLowerCaseFileText as lower,
getUpperCaseFileText as upper,
} from './io.mjs';
export function getLowerCaseFileText (filePath, {signal} = {}) {
return lower(readTextFile, filePath, {signal});
}
export function getUpperCaseFileText (filePath, {signal} = {}) {
return upper(readTextFile, filePath, {signal});
}

我希望很明显,柯里是很少的额外代码,并且这些入口点之间唯一真正的区别是特定于每个运行时的导入说明符。这允许代码进行静态分析,这提供了很多好处!

如果有人想使用这个人为的库,他们唯一需要更改的是静态导入说明符中的运行时名称:

module.mjs

// import {getUpperCaseFileText} from './lib.bun.mjs'; // when using Bun
// import {getUpperCaseFileText} from './lib.node.mjs'; // when using Node
import {getUpperCaseFileText} from './lib.deno.mjs'; // when using Deno
const text = await getUpperCaseFileText('./lib.bun.mjs');
console.log(text); // logs => "EXPORT * FROM './LIB.NODE.MJS';"

并在相应的运行时中运行它:

bun module.mjs
node module.mjs
deno run --allow-read module.mjs

这似乎需要做更多的工作,但是仅使用静态导入发布库可以让您的消费者享受静态分析的所有相同好处,相反,通过发布使用动态import()的库,您将剥夺消费者的这些好处。

您可以检测运行时,然后使用动态导入来加载所需的模块。

要检测运行时,您可以检查globalThis

function getRuntime() {
if ("Bun" in globalThis) return "bun";
if ("Deno" in globalThis) return "deno";
if (globalThis.process?.versions?.node) return "node";
}

这可能不适用于所有版本的 Bun/Deno/Node,但它是一个开始并说明了这个想法(另见 https://github.com/dsherret/which_runtime)。

然后,您可以加载所需的模块:

const { default: MyLib } = await import(`./mylib-${getRuntime()}.js`);

最新更新