跳到主要内容
Deno 2.4 已发布,带来 deno bundle、字节/文本导入、OTel 稳定版等更多功能
了解更多

我们为何在 Deno 中添加 package.json 支持

Deno 的最新版本引入了一项重大变更:通过支持 package.json 增强了与 Node 和 NPM 的兼容性。这一更新引发了关于我们优先级是否发生转变的疑问,因为 Deno 长期以来一直与开辟一条与 Node 不同的道路相关联。事实上,package.json 在Deno 的首次演示中被明确提及是一个遗憾。因此,许多用户对此发展感到惊讶。

在本文中,我们将阐述这些担忧,分享我们不断演进的思考,并概述我们未来的目标。

简化和加速 JavaScript 开发

Deno 的创建旨在简化和加速 JavaScript 开发。其核心功能包括原生 TypeScript 支持、内置工具、默认零配置以及 Web 标准 API。从历史上看,这也意味着一个独立于 NPM 生态系统、基于 Web 标准 HTTP 导入的极简主义、去中心化模块系统。

Deno 的主要目标是让编程既简单又快速。TypeScript、Web 标准 API 和许多其他功能都为此目标做出了贡献,但 Deno 的极简模块系统是否真的让编程变得简单快速,这一点变得越来越不确定。

依赖管理之梦

JavaScript 生态系统对单一集中式模块注册表的依赖与 Web 的去中心化特性相冲突。随着ES 模块的引入,现在有了一个加载远程模块的标准,而这个标准与通过 NPM 加载模块的方式截然不同。Deno 通过 HTTP URL 实现 ES 模块加载——这允许任何人只需运行一个文件服务器即可在任何域上托管代码。

这种方法带来了巨大的好处。单个文件程序可以访问强大的工具,而无需依赖清单(一个列出依赖项及其获取位置的文件,例如package.jsonimport_map.jsonCargo.tomlGemfile)。此外,Deno 程序受益于更小的下载量,因为只下载所需文件,而不是包含不必要工件的大型 npm 包压缩包。此外,使用 HTTP 进行模块链接为创新开辟了机会,例如根据 accept header 显示 HTML 文档,如在deno.land 注册表中所示。

我们一直致力于将 HTTP 导入作为 Deno 模块系统的骨干。我们已将各种实用功能内置到 deno.land 注册表中,例如基于标签的 GitHub 发布集成、不可变缓存和自动生成的文档。第三方注册表如skypackesm.sh已使 NPM 包中的单个文件作为 ES 模块通过 HTTP 导入可访问。我们已经建立了像deps.ts这样的约定,用于在一个方便的位置整合依赖项。

遗留问题

根据上下文,模块 URL,例如 https://deno.land/std@0.179.0/uuid/mod.ts 有时会过于具体。它们不仅标识了包(std/uuid/mod.ts),还指定了版本(0.179.0)以及从中获取它的服务器(deno.land)。当程序包含相似但略有不同的模块时,就会出现问题——如果另一个模块导入的 URL 引用了略有不同的版本,例如 https://deno.land/std@0.179.1/uuid/mod.ts,即使代码几乎相同,这两个模块版本都将被包含在模块图中。这被称为重复依赖问题(点击链接查看此问题的更具体示例)。

在库代码中,我们开发了例如 deps.ts 的模式用于管理远程依赖项。然而,deps.ts 并不是特别符合人体工程学——它需要扁平化并重新导出每个依赖的符号。(未来可以通过模块声明导入反射TC39 提案来改进这一点。)与使用裸说明符的 package.json 相比,deps.ts 通常更冗长,语法也更混乱。

理想情况下,导入映射(Import Maps)可以通过允许用户在其代码中使用裸说明符并仅在导入映射中指定 URL 来解决此问题。然而,导入映射不可组合:已发布的库不能同时使用裸说明符和导入映射,因为导入该库的顶层项目无法将库的导入映射与其自身的导入映射组合起来。

转译服务器,例如esm.shskypack在将某些 NPM 模块导入 Deno 时运行良好,但它们有固有的局限性。例如,如果一个 NPM 模块在运行时加载数据文件,这些转译服务器将无法提供兼容的版本。类似这样的问题导致了不尽如人意的开发体验。

裸说明符(Bare Specifiers)的力量

使用裸说明符的导入语句(如下所示)简洁且易于理解

import express from "express";

裸说明符("express")提供了对依赖项有用的模糊引用,这使得重复依赖问题可以通过 semver(语义化版本)解析来解决。然而,如果库是使用裸说明符编写的,则必须有一个依赖清单来阐明这些裸说明符所指代的内容。

通过兼容性简化和加速

我们希望 Deno 用户能够比使用 Node 更高效地工作。开发者希望能够轻松导入和使用库。因此,有了我们的 npm 说明符支持。除了库之外,开发者还希望能够直接在 Deno 中运行现有的 Node 项目。因此,新增了 package.json 支持。

Deno 的向后兼容性使得 Node 和 NPM 中不太理想的遗留功能保持距离。例如,Deno 仅通过 NPM 导入支持 CommonJS。此外,Deno 不允许用户在 NPM 依赖之外使用裸说明符("fs")引用内置 Node 模块,而是必须使用 "node:fs"。或者例如,在 Deno 中,setTimeout 将根据 Web 标准返回一个数字(与 Node 不同)。Deno 不会随意运行安装后脚本,并强制执行用户空间安全权限。向后兼容性并不意味着 JavaScript 生态系统无法演进和改进。

需要强调的是,Deno 将始终支持通过 URL 链接代码,并继续像浏览器一样使用 HTTP 导入。现在,使用 https: URL 只是 Deno 中链接代码的一种方式。自 Deno 1.28 以来,我们有了 npm: URL,并且很容易设想其他可以提高开发速度的方式,例如 github: URL。

一个新的主要版本

我们正在努力实现Deno 的一个新的主要版本,预计在未来几个月内发布。即将发布的这个版本的主要主题是鼓励在 Deno 工作流程中使用裸说明符。

尽管 Deno 与 NPM 模块有出色的向后兼容性,但我们不建议将为 Deno 用户分发的 Deno 代码发布到 NPM 上。如果需要与 Node 和 NPM 项目共享代码,我们推荐使用我们官方的 Deno 到 NPM 编译器 DNT,它能输出高质量的 NPM 包,其中包含转译后的 Node 兼容 JavaScript 和源自原始 TypeScript 的 TypeScript 声明文件。然而,要生活在一个真正的 TypeScript 优先的世界中,最好分发并链接到实际的 TypeScript 代码,而不是某些编译输出。

为了解决重复依赖问题并改善使用 TypeScript 优先的 Deno 模块注册表的人体工程学,Deno 的下一个主要版本将引入 deno: URL 方案。通过使用这些特殊的 URL,而不是 HTTP URL,Deno 运行时能够进行 semver 解析和模块去重。此外,它消除了编写完整 URL 的需要。

例如,导入Oak会是这样

import oak from "deno:oak@12";

请注意,只指定了主版本——运行时将有特殊的 semver 解析逻辑来找到匹配的 Oak 版本。

下一个主要版本还将倡导使用导入映射(import maps),作为 package.json 工作流程的现代替代方案。Deno 中的导入映射在自动发现的 deno.json 配置文件中指定。示例如下

{
  "imports": {
    "oak": "deno:oak@12",
    "chalk": "npm:chalk@5"
  }
}

此配置允许任何代码在整个代码中使用 "oak""chalk" 裸说明符。Oak 来自 Deno 注册表,Chalk 来自 NPM 注册表。代码中的导入将是

import oak from "oak";
import chalk from "chalk";

无论是使用现代导入映射工作流还是 Node.js package.json 工作流,Deno 的目标都是成为开发者们可以信赖的工具,以加速他们的工作。JavaScript 作为世界默认的编程语言,值得我们继续努力改进其生态系统和工具。



关注 Twitter 上的@deno_land,了解最新动态。