如何将 Deno 模块发布到 NPM
我编写了 oak,一个功能齐全的 HTTP 中间件/路由框架。它是 Deno 最常用的 HTTP 框架,为许多网站提供支持;例如 doc.deno.land。
许多人希望将 Deno 作为他们的主要开发平台,但也希望能够在 Node 生态系统内共享代码。使用 dnt 可以轻松实现这一点。在本文中,我将向您展示如何以一种在 Node 中可用的方式将 Oak 发布到 NPM 模块。
注意:本文发布时内容准确无误,Deno、oak、dnt 和 Node.js 将持续发展,具体技术细节和声明未来可能不准确。
oak 概述
如果您不熟悉 oak,它是一个受 koa 启发的 HTTP 中间件框架,并带有一个路由器。其主要目的是提供一种结构化的方式来处理 HTTP 请求。虽然 oak 的基本原理自 2018 年 12 月发布以来(当时 Deno 还是 v0.2 版本)一直保持一致,但它随着 Deno CLI 的演进而不断发展,包括迁移到原生 HTTP 服务器并确保支持 Deno Deploy。
Deno 中的基本用法很简单
import { Application } from "https://deno.land/x/oak/mod.ts";
const app = new Application();
app.use((ctx) => {
ctx.response.body = "Hello from oak";
});
app.listen({ port: 8000 });
但它始终是为 Deno 设计的,利用内置的 Deno API,不仅能够处理 HTTP 请求和响应,还能实现高级功能,例如为静态文件生成 ETag
签名以及处理复杂的表单数据体。在 Node.js 上运行它从未在设计考虑范围之内。
dnt
dnt 提供了一个构建管道,用于将 Deno 代码转换为 Node 兼容的 NPM 包。
该管道在高级别上执行以下操作
- 将 Deno 代码转换为 Node.js 兼容的 TypeScript 代码
- 重写 Deno 的带扩展名模块说明符,使其与 Node.js 模块解析兼容。
- 为检测到的任何
Deno
命名空间 API 以及其他可配置的全局变量注入 shims。 - 将 Skypack 或 esm.sh 中的远程导入重写为裸说明符导入,并将其添加到
package.json
作为依赖项。 - 其他远程导入将被下载并包含在包中。
- 使用
tsc
对转换后的 TypeScript 代码进行类型检查。 - 将包输出为一组 ESM、CommonJS 和 TypeScript 类型声明文件,以及一个
package.json
。 - 通过支持
Deno.test()
API 的测试运行器在 Node.js 中运行最终输出。
这意味着您可以在 Deno 中开发和测试所有代码,当您想将其发布到 npm 并使其可用于 Node.js 时,可以使用 dnt
来导出它并验证其是否按预期工作,所有这些都只需对源代码进行最少或无需更改。
设置完成后,您可以继续迭代您的包,轻松地将其发布,以便在 Deno 和通过 npm 消费。
让 oak 在 Node.js 上运行
在使其工作过程中学到了很多经验教训,并非所有工作都已完成。需要添加对 HTTP/2、WebSockets 和 FormData
文件处理的支持,但主要功能已经可用。
Web 标准
Deno 充分利用了 Web 标准和 Web 平台。Node.js 正在越来越多地添加 Web 平台 API,但它们通常不会全局暴露,并且有些仍然需要依赖项。以下是需要在 oak 中解决的问题:
Web 兼容的 Streams 在 Deno 中是全局可用的,但在 Node.js 中只能通过内置模块
"stream/web"
获得。这些需要通过 dnt 配置来暴露。{ shims: { custom: [{ package: { name: "stream/web", }, globalNames: ["ReadableStream", "TransformStream"], }], } }
Web Crypto 在 Deno 中是全局可用的,但在 Node.js 中只能通过
"crypto"
内置模块的webcrypto
符号获得。这需要通过 dnt 配置的shims
部分中的crypto: true
选项来暴露。虽然未在 oak 中直接使用,但某些依赖项需要从内置
"buffer"
模块提供Blob
全局变量。这通过 dnt 配置的shims
部分中的blob: true
选项来暴露。Deno 暴露了 oak 使用的 Web 标准
fetch()
和Headers
。Node.js 目前没有内置这些(尽管正在改变),因此需要通过"undici"
包来暴露它们,dnt 通过 dnt 配置的shims
部分中的undici: true
选项支持这一点。oak 为内部错误扩展了 Web 标准
ErrorEvent
,该事件在 Deno 中是全局可用的。Node.js 没有此功能,因此我必须创建一个ErrorEvent
类,该类扩展了 Node.js 中全局可用的Event
,并由 dnt 作为全局 shim 加载。
ESM 和 Node.js
Deno 是围绕 ES 模块构建的,虽然 Node.js 已在很长一段时间内取消了对 ES 模块的标志支持,但这仍然是一个复杂的问题。对于许多工作负载,dnt 可以轻松地将您的代码降级为 CommonJS 和 ESM,从而使您的包的使用者可以选择如何在 Node.js 中加载它。对于 oak 来说,这要复杂一些。Node.js 有一种情况不支持 ES 模块,那就是顶层 await 的情况。
我有一个使用顶层 await 初始化变量的地方,为了支持 CommonJS,我需要对其进行重构。
在 Node.js 上支持 HTTP
Deno 和 Node.js 之间最大的区别在于它们处理 HTTP 请求的方式。Deno 已经发展,最初它只通过 Deno std
库支持 HTTP,现在它提供了一个围绕 Web 平台 fetch()
API 构建的原生实现。Node.js 的内置 "http"
模块是一个非常低级的 API,自 Node.js 早期以来基本保持不变。
当 oak 从 std
库 HTTP 服务器迁移到原生服务器时,我实现了一个抽象层来处理请求和响应。即使在 oak 中放弃了对 std
库 HTTP 服务器的支持,该抽象层仍得以保留。这个抽象层,经过一些重构和改进,使得对低级 Node.js "http"
服务器的支持成为可能,并且对代码库的其余部分几乎没有改动。
如果您感兴趣,代码位于 http_server_node.ts
模块中。
经过一些重构,我能够使用 dnt 和 mapping
功能来“替换” Deno 抽象与 Node.js 抽象。这是一个最小量的平台特定代码(Deno 大约 160 行,Node.js 大约 220 行)。
其他小知识
在 Node.js 下运行测试时,我遇到的一个初始测试失败是针对 oak 许多类中的自定义检查逻辑。在 Deno 中,自定义检查方法由 "Deno.customInspect"
符号定义,但在 Node.js 中,它作为 "nodejs.util.inspect.custom"
符号可用,并且 API 略有不同。因此,我为每个具有 Deno 自定义检查的类添加了一个 "nodejs.util.inspect.custom"
符号方法,并对其进行了调整,以更好地与 Node.js API 对齐。
即便如此,检查的输出仍然存在细微差别,因此这是 oak 中少数几个我不得不根据代码是在 Deno 还是 Node.js 下运行来分支测试代码的地方之一。
此外,undici 的 Headers
类与 Deno 的 Headers
类有所不同。Deno 的版本通过了所有 WHATWG 测试,而 undici 的设计则优先考虑 Node.js。然而,Web 标准和服务器端 JavaScript 在使用 Headers
方面存在一个根本问题,特别是在处理 Set-Cookie
头部方面,该头部只在服务器端设置。Deno 和 undici 都独立地解决了这个问题,因此在 Deno 和 Node.js 上运行时会导致不同的行为。我认为这些行为不会直接影响用户,但它们可能会,我将来可能需要在 oak 中进行更改以适应这种情况。
构建、测试和 CI
dnt 是一个构建管道,您通常会将其集成到构建过程中。对于 oak,我选择创建一个 _build_npm.ts
脚本,该脚本除了运行 dnt 管道外,还执行一些超出 dnt 范围的其他构建步骤。
通常,您只需编写一次测试,dnt 默认会在 Node.js 下为您运行类似 Deno 的集成测试工具。对于 oak,我有一些特定于 Deno 或 Node.js 的测试,以及一些在不同平台之间结果有所不同的测试。我尽力避免分支或忽略测试,但在某些情况下它们是不可避免的。我创建了一个简单的实用函数来检测环境
export function isNode(): boolean {
return "process" in globalThis && "global" in globalThis;
}
将所有这些集成到 CI 中非常简单,因为只需将构建脚本作为 CI 过程的一部分运行即可,它将为我构建、类型检查和测试包。
发布到 npm
一旦 dnt 为您构建了包,它就已准备好发布到 npm。我只需在 oak 中切换到构建脚本的输出目录,然后运行 npm publish
即可。
结论
我显然有所偏颇,但我认为 Deno 是一个很棒的开发平台,也是一个很棒的 JavaScript 和 TypeScript 运行时。它拥有一套丰富的现代化内置 API,对 Web 平台 API 有大量支持,并且内置了 TypeScript 代码编写支持,是一个出色的运行时。再加上“开箱即用”的测试、二进制分发打包、调试和 IDE 语言服务器,很难找到更好的体验。
然而,阻碍采用的一大障碍是如何在 Deno 中开发代码,但又能在不大量复制代码或自己做大量工作的情况下与 Node.js 共享。我感到 dnt 在消除这一障碍方面做得非常出色,以 oak 这样的项目为例,能够在 Deno 中编写一次代码,然后轻松地与 Node.js 共享,这是一个很好的例子。
我们非常希望看到更多项目开始遵循这条路径。不可避免地,我们将会发现 dnt 需要更改或改进的地方,我们很乐意听取有关它的反馈和经验。