跳至主要内容
Deno 2 终于来了 🎉️
了解更多
Why Deno's HTTP imports struggle at scale

我们对 HTTP 导入的误解

一切都要尽可能简单,但不能更简单。

阿尔伯特·爱因斯坦

从一开始,HTTP 导入一直是 Deno 的一项关键功能。多年来,这整个模块系统都是为了简化 JavaScript 开发而设计的,它利用了网络的分布式特性,与 npm 的集中式注册表不同。

例如,您可以从标准库中导入 assertEquals() 函数,如下所示

import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";

assertEquals(1, 2);

这个想法具有革命性(现在仍然如此)。我们全力以赴地追求它,最终意识到:这种设计决策带来了巨大的权衡。

让我们探讨一下为什么这种方法无法像我们最初希望的那样随着项目复杂度的增加而扩展,以及 Deno 如何建议今天共享和使用模块以克服这些挑战。

梦想

围绕 HTTP 导入设计 Deno 的模块系统是一个雄心勃勃的目标。它旨在用通过 HTTP 的分布式系统取代 npm,与 ES 模块在浏览器中的工作方式保持一致。这消除了对 package.json 文件和 node_modules 文件夹的需求,简化了项目结构。Deno 脚本可以缩减到单个文件程序,无需项目目录或配置。与下载大型 tar 包的 npm 不同,HTTP 导入只获取必要的源代码。私有注册表仅仅是经过身份验证的代理。

我们将此深度集成到 Deno 的工作流程中,包括缓存、预加载和重新加载。我们还构建了 deno.land/x,这是一个注册表,用于将 git 仓库连接起来并通过 HTTP 共享它,它还附带了诸如生成的文档之类的功能。

现实

尽管它很有希望,但自最初实施以来,HTTP 导入出现了一些问题。

URL 长度

较长的 URL 会使代码库变得杂乱,尤其是在大型项目中。对比一下

import express from "express"; // Node

import oak from "https://deno.land/x/[email protected]"; // Deno 1.x

Node 的导入明显更短(也更容易记住)。

依赖关系管理

随着项目的增长,管理较长的 URL 和版本变得越来越乏味。

最初,我们采用了 deps.ts 约定,将依赖项集中在一个项目的单个文件中

// deps.ts
export { concat } from "https://deno.land/[email protected]/bytes/mod.ts";
export * as base64 from "https://deno.land/[email protected]/encoding/base64.ts";

然后,可以像这样导入依赖项

import { concat } from "../../deps.ts";

虽然这可以工作,但与简单的 package.json 文件相比,它很麻烦。

重复依赖

URL 缺乏语义版本控制,这使得管理依赖项变得很困难。

虽然版本字符串可以嵌入到 URL 中(例如:https://deno.land/[email protected]/fs/copy.ts),但 HTTP 导入会将您锁定到单个精确版本,直到您手动更新 URL。在大型项目中,这意味着您很容易在代码库中使用同一个库的几个变体(实际上,这在实践中很少必要或有益)。

语义版本控制有助于消除重复依赖项,从而减少已加载模块的数量。理想情况下,Deno 应该识别可互换的模块,并使用最新版本。

可靠性

去中心化的模块系统也导致了可靠性问题。许多模块托管在随机网站或个人服务器上,导致了正常运行时间问题。虽然这些服务器宕机不会立即导致 Deno 程序宕机(因为我们缓存远程依赖项),但它可能会破坏 CI 和新部署。虽然 Deno 为其 deno.land/x 注册表确保了高可用性,但它无法控制其他主机,因此整体可用性取决于依赖关系图中最不可靠的主机。

解决方案

为了解决上述所有问题,Deno 引入了两项重大改进:导入映射和 JSR。

用导入映射和 JSR 转折

请明确一点:Deno 不会 删除 HTTP 导入。我们仍然相信它们的用处。然而,很明显,通常需要更多结构。

我们致力于通过简化代码的编写和分发方式来改善 JavaScript 生态系统。JavaScript 本质上是默认的编程语言,它应该有一个很棒的模块系统。

该解决方案的一部分是 导入映射,这是浏览器中的另一个 Web 标准,在 Deno 中得到了实现。导入映射允许您获得简短且易于记忆的标识符,并在多个文件中管理版本

{
  "imports": {
    "$ga4": "https://raw.githubusercontent.com/denoland/ga4/main/mod.ts",
    "$marked-mangle": "https://esm.sh/[email protected]",
    "@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 作为理解 semver 的集中式存储库,以解决剩余的两个问题

  • JSR 避免了依赖多个主机来提供模块的可靠性问题;以及
  • JSR 使用语义版本控制避免了重复依赖问题(类似于 Node 中的 package.json 与 npm 的工作方式)。

我们相信这个新的注册表将极大地简化 JavaScript 的使用和共享方式。虽然它确实比 HTTP 导入更复杂,但我们认为好处值得权衡。

什么是 JSR?

我们在 3 月份推出了 JSR。JSR 是一个开源的、跨运行时的代码注册表,允许用户轻松共享现代 JavaScript 和 TypeScript。它旨在可靠且托管成本低廉,本质上是由于 不变性保证而充当高度缓存的文件服务器。

JSR 理解并执行语义版本控制,解决了重复依赖问题。集中式存储库还允许我们提供许多其他方式无法实现的改进,从简单的 TypeScript 发布包评分,鼓励最佳实践。(您可以在此处了解更多关于 JSR 的信息,以及 我们构建 JSR 的原因。)

在幕后,JSR 仍然使用 HTTP 导入。例如,使用这个标识符

jsr:@luca/flag

上述内容实际上可以看作是对以下内容的智能重定向

https://jsr.deno.org.cn/@luca/flag/1.0.0/mod.ts

这意味着 JSR 继承了 HTTP 导入中真正优秀的特性。例如:仅下载实际导入的代码(没有大型 tar 包!)。由于用户不会直接接触这些 HTTP 导入,因此诸如长 URL 和手动字符串管理等问题便消失了。

这对现代 Deno 意味着什么

使用 HTTP 导入的现有 Deno 脚本将继续工作——它们非常适合特定规模的项目。但是,我们现在建议使用导入映射而不是 deps.ts,并使用 JSR 代替 deno.land/x 和/或 npm。

因此,回到上面提到的 assert 示例:你会发现它在这个新系统中更加简洁。并且由于 semver 解析,依赖项会自动保持最新(只要它们没有被锁定文件固定)!

// ❌ Deno 1.x:
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";

// ✅ Deno 2
import { assertEquals } from "jsr:@std/assert@1";

assertEquals(1, 2);

在较大的项目中使用时,您可以选择添加导入映射,以使导入说明符更短,并更轻松地在各个文件之间管理版本。然后,assert 示例看起来更加简洁

import { assertEquals } from "@std/assert";
assert(1, 2);
{
  "imports": {
    "@std/assert": "jsr:@std/assert@1"
  }
}

您可以选择何时以及是否使用这种方法。我们重视 Deno 脚本能够缩减到单个文件(无需 deno.json 配置)的能力,因此导入映射完全可选非常重要。

Deno 2 即将推出

JavaScript 应该有一个与浏览器标准一致的简单模块系统。我们希望提升生态系统,并帮助它成为我们相信它最终会成为的行业基石。

为了实现这一目标,我们需要良好的设计,而良好的设计需要迭代——我们必须诚实地审查问题并解决它们。

解决这些问题定义了 Deno 2 中的许多变化

  • JSR 用于共享模块,而不是随机的文件服务器
  • semver 用于 Deno 包的版本控制
  • 导入映射用于管理依赖项

Deno 2 还有一些其他特性我们没有讨论

  • 工作区和单仓库支持,已在 Deno 1.45 中实现
  • 与 Node/npm 的深度兼容性,包括 N-API 支持和与 Next.js 的兼容性

我们将在今年 9 月发布 Deno 2(这次是真的)。

我很期待看到人们如何使用下一代尽可能简单但绝不简单的 JavaScript 工具链。

🚨️ Deno 2 即将推出 🚨️

Deno 2 中有一些 细微的破坏性更改,但您可以通过使用 DENO_FUTURE=1 标志从现在开始使迁移更顺利。