跳到主要内容

为什么我们为 Deno 添加了 package.json 支持

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

在这篇文章中,我们将解决这些疑虑,分享我们不断演变的思考的见解,并概述我们未来的目标。

简化和加速 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 包 tarball。 此外,使用 HTTP 进行模块链接为创新开辟了机会,例如根据 accept 标头显示 HTML 文档,如 deno.land 注册表中所示。

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

遗留问题

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

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

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

esm.shskypack 这样的转译服务器非常适合将一些 NPM 模块导入到 Deno 中,但它们具有固有的局限性。 例如,如果 NPM 模块在运行时加载数据文件,则这些转译服务器将无法提供兼容的版本。 诸如此类的问题会导致次等的开发者体验。

裸标识符的力量

使用裸标识符的导入语句(如下所示)简洁而熟悉

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 不会随意运行 post-install 脚本,并强制执行用户空间安全权限。 向后兼容性并不意味着 JavaScript 生态系统不能发展和改进。

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

新的主要版本

我们正在努力在未来几个月内发布 Deno 的新主要版本。 即将发布的这个主要版本的主题是鼓励在 Deno 工作流程中使用裸标识符。

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

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

例如,导入 Oak 将如下所示

import oak from "deno:oak@12";

请注意,仅指定了主要版本 - 运行时将具有特殊的 semver 解析逻辑来查找与匹配的 Oak 的正确版本。

下一个主要版本还将提倡使用导入映射,作为 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,以了解最新进展。