原文:Deno官方博客
作者:Ryan Dahl 2024年7月29日
一切应尽可能简单,但不要太简单。 ------ 阿尔伯特·爱因斯坦
从一开始,HTTP 导入就是 Deno 的一个关键特性。多年来,这一直是整个模块系统,旨在通过利用网络的分布式特性简化 JavaScript 开发,而不是像 npm 那样的集中式注册表。
例如,你可以这样从标准库中导入 assertEquals()
函数:
jsx
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";
assertEquals(1, 2);
这个想法是革命性的(而且仍然是)。我们全力以赴追求这一目标,但最终意识到:这一设计决策伴随着显著的权衡。
让我们探讨为什么这种方法无法像我们最初希望的那样适应项目复杂性的增加,以及 Deno 如何推荐在今天分享和使用模块来克服这些挑战。
理想
围绕 HTTP 导入设计 Deno 的模块系统是一项雄心勃勃的工作。它旨在用 HTTP 分布式系统取代 npm,符合浏览器中 ES 模块的工作方式。这消除了对 package.json
文件和 node_modules
文件夹的需求,简化了项目结构。Deno 脚本可以缩减为单文件程序,无需项目目录或配置。与 npm 不同的是,HTTP 导入只获取必要的源代码,而不是下载大型 tar 包。私有注册表仅需成为经过身份验证的代理。
我们将其深度集成到 Deno 的工作流中,包括缓存、预加载和重新加载。我们还建立了 deno.land/x,一个通过 HTTP 连接 git 仓库并共享的注册表,具有生成文档等功能。
现实
尽管充满希望,自从最初实施以来,HTTP 导入出现了几个问题。
URL 的长度
长 URL 会使代码库变得混乱,尤其是在较大的项目中。比较:
jsx
import express from "express";
import oak from "https://deno.land/x/oak@v16.1.0";
Node 的导入显然更短(且更容易记住)。
依赖管理
随着项目的增长,管理长 URL 和版本变得越来越繁琐。
最初,我们采用了 deps.ts
约定,将依赖项集中在项目的一个文件中:
jsx
export { concat } from "https://deno.land/std@0.200.0/bytes/mod.ts";
export * as base64 from "https://deno.land/std@0.200.0/encoding/base64.ts";
然后,可以这样导入依赖项:
jsx
import { concat } from "../../deps.ts";
虽然这可行,但与简单的 package.json
文件相比,这很麻烦。
依赖重复
URL 缺乏语义版本控制,难以管理依赖关系。
虽然可以在 URL 中嵌入版本字符串(例如,https://deno.land/std@0.224.0/fs/copy.ts
),但 HTTP 导入将你锁定在一个确切的版本,直到你手动更新 URL。在较大的项目中,这意味着你可能会在代码库中轻松地得到多个相同库的变体(这在实际操作中很少是必要的或有益的)。
语义版本控制有助于消除重复的依赖,减少加载模块的数量。理想情况下,Deno 应该识别可互换的模块,使用最新版本。
可靠性
去中心化的模块系统也导致了可靠性问题。许多模块托管在随机网站或个人服务器上,导致了正常运行时间问题。虽然这些服务器的宕机不会立即导致 Deno 程序宕机(因为我们缓存了远程依赖),但可能会导致 CI 和新部署的中断。尽管 Deno 确保其 deno.land/x
注册表的高可用性,但它无法控制其他主机,使整体可用性依赖于依赖关系图中最不可靠的主机。
解决方案
为了解决上述所有问题,Deno 引入了两个主要改进:导入映射和 JSR。
导入映射和 JSR 的转折点
需要明确的是:Deno 并没有删除 HTTP 导入。我们仍然相信它们的有用性。然而,很明显,通常需要更多结构。
我们致力于通过简化代码编写和分发方式来改进 JavaScript 生态系统。JavaScript,基本上是默认的编程语言,应该拥有一个伟大的模块系统。
解决方案的一部分是导入映射,这是来自浏览器的另一个网络标准,在 Deno 中实现。导入映射允许你找回简短且易记的标识符,并在多个文件中管理版本:
json
{
"imports": {
"$ga4": "https://raw.githubusercontent.com/denoland/ga4/main/mod.ts",
"$marked-mangle": "https://esm.sh/marked-mangle@1.0.1",
"@astral/astral": "jsr:@astral/astral@^0.4.0",
"@fresh/plugin-tailwind": "./plugin-tailwindcss/src/mod.ts",
"@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.10.3"
}
}
然而,仅靠导入映射无法解决语义版本控制问题或可靠性问题------这就是 JSR 的作用。
我们创建了 JSR 作为一个理解语义版本控制的集中式存储库,以解决剩下的两个问题:
- JSR 避免了依赖多个主机来提供模块的可靠性问题;以及
- JSR 使用语义版本控制避免了重复依赖问题(类似于 npm 中的
package.json
工作方式)。
我们相信这个新注册表将极大地简化 JavaScript 的使用和共享方式。虽然它确实比 HTTP 导入稍微复杂一些,但我们觉得这些好处是值得的。
什么是 JSR?
我们在三月份发布了 JSR。JSR 是一个开源的、跨运行时的代码注册表,允许用户轻松共享现代 JavaScript 和 TypeScript。它的构建目的是可靠且廉价托管,基本上充当一个高度缓存的文件服务器,因为不变性保证。
JSR 理解并强制执行语义版本控制,解决了重复依赖问题。集中式存储库还允许我们提供许多其他方法无法实现的改进,从简单的TypeScript 发布到包评分鼓励最佳实践。(你可以在这里阅读更多关于 JSR 的内容和我们为什么构建 JSR。)
在底层,JSR 仍然使用 HTTP 导入。例如,看看这个标识符:
bash
jsr:@luca/flag
上面的内容实际上可以看作是一个智能重定向到:
bash
https://jsr.io/@luca/flag/1.0.0/mod.ts
这意味着 JSR 继承了 HTTP 导入真正出色的部分。 例如:仅下载实际导入的代码(没有大型 tar 包!)。因为用户不会直接暴露于这些 HTTP 导入,因此诸如长 URL 和手动字符串管理的问题消失了。
这对现代 Deno 的意义
现有的带有 HTTP 导入的 Deno 脚本将继续工作------它们适用于一定规模的项目。然而,我们现在建议使用导入映射代替 deps.ts
,以及 JSR 替代 deno.land/x
和/或 npm。
所以,回到上面提到的 assert
示例:在这个新系统中会更简洁。并且由于语义版本控制的解决,依赖项会自动保持最新(只要它们没有被锁文件固定)!
jsx
// ❌ Deno 1.x:
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";
// ✅ Deno 2
import { assertEquals } from "jsr:@std/assert@1";
assertEquals(1, 2);
在较大的项目中使用时,你可以选择添加导入映射以使导入标识符更短,并更轻松地管理跨文件的版本。然后 assert
示例看起来更加简洁:
jsx
import { assertEquals } from "@std/assert";
assert(1, 2);
json
{
"imports": {
"@std/assert": "jsr:@std/assert@1"
}
}
你可以选择何时或是否采用这种方法。我们重视 Deno 脚本缩减为单文件(无需 deno.json
配置)的能力,因此导入映射是完全可选的。
Deno 2 即将推出
JavaScript 应该拥有一个与浏览器标准对齐的简单模块系统。我们希望提升生态系统,并帮助它成为我们认为不可避免的行业基石。
要实现这一目标,我们需要良好的设计,而良好的设计需要迭代------我们必须诚实地审视问题并加以解决。
解决这些问题定义了 Deno 2 中的许多变化:
- 用于共享模块的 JSR,而不是随机文件服务器
- Deno 包的语义版本控制
- 用于管理依赖项的导入映射
还有一些我们尚未讨论的 Deno 2 的其他特性:
- 工作空间和 monorepo 支持 ,已在 Deno 1.45 中实现
- 深度 Node/npm 兼容性,包括 N-API 支持和与 Next.js 的兼容性
我们将在今年九月发布 Deno 2(这次是真的)。
我很期待看到大家如何利用下一代尽可能简单但不过于简单的 JavaScript 工具链。