跳到主要内容
Deno 2.4 已发布,带来 deno bundle、bytes/text 导入、稳定的 OTel 等功能
了解更多
Deno v1

动态语言是实用的工具。脚本化允许用户快速简洁地将复杂的系统连接起来,表达想法,而无需担心内存管理或构建系统等细节。近年来,Rust 和 Go 等编程语言使得生成复杂的本地机器码变得容易得多;这些项目是计算机基础设施中极其重要的发展。然而,我们认为拥有一个强大的脚本环境,能够解决广泛的问题领域,仍然非常重要。

JavaScript 是使用最广泛的动态语言,在每个带有网页浏览器的设备上运行。大量的程序员精通 JavaScript,并且在优化其执行方面投入了大量精力。通过 ECMA International 等标准组织,该语言得到了精心和持续的改进。我们相信 JavaScript 是动态语言工具的自然选择;无论是在浏览器环境中还是作为独立进程。

我们在这个领域的最初尝试 Node.js,被证明是一个非常成功的软件平台。人们发现它对于构建Web开发工具、构建独立的Web服务器以及无数其他用例都很有用。然而,Node 是在 2009 年设计的,当时的 JavaScript 语言与现在大不相同。出于必要,Node 不得不发明一些概念,这些概念后来被标准组织采纳并以不同的方式添加到语言中。在 Node 中的设计错误 这一演示中,对此进行了更详细的讨论。由于 Node 拥有大量用户,因此系统演进起来既困难又缓慢。

随着 JavaScript 语言的变化,以及 TypeScript 等新功能的加入,构建 Node 项目可能会变得一项艰巨的任务,需要管理构建系统和其他繁重的工具,从而失去了动态语言脚本的乐趣。此外,链接到外部库的机制基本上通过 NPM 仓库集中化,这与 Web 的理念不符。

我们认为 JavaScript 和周围软件基础设施的现状已经发生了足够大的变化,值得进行简化。我们追求一个既有趣又高效的脚本环境,可以用于广泛的任务。

命令行脚本的网页浏览器

Deno 是一个在 Web 浏览器之外执行 JavaScript 和 TypeScript 的新运行时。

Deno 尝试提供一个独立的工具,用于快速编写复杂功能的脚本。Deno 是(并且永远是)一个单独的可执行文件。像网页浏览器一样,它知道如何获取外部代码。在 Deno 中,一个单独的文件可以定义任意复杂的行为,而无需任何其他工具。

import { serve } from "https://deno.land/std@0.50.0/http/server.ts";

for await (const req of serve({ port: 8000 })) {
  req.respond({ body: "Hello World\n" });
}

这里,一个完整的 HTTP 服务器模块通过一行代码作为依赖项添加。没有额外的配置文件,事先无需任何安装,只需执行 deno run example.js

此外,像浏览器一样,代码默认在安全的沙箱中执行。未经许可,脚本不能访问硬盘、打开网络连接或执行任何其他潜在的恶意操作。浏览器提供访问摄像头和麦克风的 API,但用户必须首先给予权限。Deno 在终端中提供了类似的行为。除非提供了 --allow-net 命令行标志,否则上述示例将失败。

Deno 致力于不偏离标准化的浏览器 JavaScript API。当然,并非所有浏览器 API 都与 Deno 相关,但只要相关,Deno 都不会偏离标准。

一流的 TypeScript 支持

我们希望 Deno 能够适用于广泛的问题领域:从小型单行脚本到复杂的服务器端业务逻辑。随着程序的复杂性增加,某种形式的类型检查变得越来越重要。TypeScript 是 JavaScript 语言的扩展,允许用户选择性地提供类型信息。

Deno 支持 TypeScript,无需额外工具。运行时设计时就考虑到了 TypeScript。deno types 命令提供了 Deno 所提供的一切的类型声明。Deno 的标准模块都用 TypeScript 编写。

完全基于 Promise

Node 是在 JavaScript 还没有 Promise 或 async/await 概念之前设计的。Node 中与 Promise 对应的概念是 EventEmitter,许多重要 API 都围绕它构建,特别是 sockets 和 HTTP。抛开 async/await 的人体工程学优势不谈,EventEmitter 模式存在背压问题。以 TCP socket 为例,当收到传入数据包时,socket 会发出“data”事件。这些“data”回调会以不受约束的方式发出,导致进程被事件淹没。由于 Node 不断接收新的数据事件,底层 TCP socket 没有适当的背压,远程发送方不知道服务器已过载并继续发送数据。为了缓解这个问题,添加了一个 pause() 方法。这可以解决问题,但需要额外的代码;而且由于淹没问题只在进程非常繁忙时出现,许多 Node 程序可能会被数据淹没。结果是系统具有糟糕的尾部延迟。

在 Deno 中,sockets 仍然是异步的,但接收新数据需要用户明确地 read()。无需额外的暂停语义即可正确构建接收 socket。这并非 TCP socket 独有。与系统的最低层绑定层从根本上与 Promise 绑定——我们称这些绑定为“ops”。Deno 中所有回调都以某种形式源自 Promise。

Rust 有其自己的类似 Promise 的抽象,称为 Futures。通过“op”抽象,Deno 可以轻松地将基于 Rust Future 的 API 绑定到 JavaScript Promise 中。

Rust API

我们发布的主要组件是 Deno 命令行接口 (CLI)。CLI 是今天达到 1.0 版本的。但 Deno 并非一个单一的程序,而是被设计为 Rust 包的集合,以允许在不同层级进行集成。

deno_core 包是 Deno 的一个非常精简的版本。它不依赖 TypeScript 也不依赖 Tokio。它只提供了我们的 Op 和 Resource 基础设施。也就是说,它提供了一种组织化的方式,将 Rust Future 绑定到 JavaScript Promise。CLI 当然完全建立在 deno_core 之上。

rusty_v8 包提供了 V8 C++ API 的高质量 Rust 绑定。该 API 试图尽可能接近原始的 C++ API。它是一个零成本绑定——在 Rust 中暴露的对象正是您在 C++ 中操作的对象。(例如,以前的 Rust V8 绑定尝试强制使用 Persistent handles。)该包提供了在 GitHub Actions CI 中构建的二进制文件,但也允许用户从头编译 V8 并调整其许多构建配置。所有 V8 源代码都分发在该包本身中。最后,rusty_v8 尝试成为一个安全的接口。它尚未 100% 安全,但我们正在接近。能够以安全的方式与 V8 这样复杂的虚拟机交互是非常惊人的,并且使我们发现了 Deno 本身中的许多困难错误。

稳定性

我们承诺在 Deno 中保持 API 的稳定。Deno 拥有许多接口和组件,因此透明地说明我们所说的“稳定”的含义非常重要。我们为与操作系统交互而发明的 JavaScript API 都存在于“Deno”命名空间内(例如 Deno.open())。这些 API 经过了仔细审查,我们不会对其进行向后不兼容的更改。

所有尚未准备好稳定的功能都已隐藏在 --unstable 命令行标志后面。您可以在此处查看不稳定接口的文档。在后续版本中,其中一些 API 也将稳定。

在全局命名空间中,您会找到各种其他对象(例如 setTimeout()fetch())。我们非常努力地使这些接口与浏览器中的接口保持一致;但如果我们发现无意中存在不兼容之处,我们会发布更正。浏览器标准定义了这些接口,而不是我们。我们发布的任何更正都是错误修复,而不是接口更改。如果与浏览器标准 API 存在不兼容之处,该不兼容之处可能会在主要版本发布前得到纠正。

Deno 还拥有许多 Rust API,即 deno_core 和 rusty_v8 包。这些 API 都没有达到 1.0 版本。我们将继续对它们进行迭代。

局限性

重要的是要理解 Deno 不是 Node 的一个分支——它是一个全新的实现。Deno 仅开发了两年,而 Node 已经开发了十多年。鉴于 Deno 引起了大量关注,我们预计它将继续发展和成熟。

对于某些应用程序,Deno 今天可能是一个不错的选择,对于其他应用程序则不然。这将取决于具体要求。我们希望公开这些局限性,以帮助人们在考虑使用 Deno 时做出明智的决定。

兼容性

不幸的是,许多用户会发现与现有 JavaScript 工具缺乏兼容性,这令人沮丧。Deno 通常与 Node (NPM) 包不兼容。目前正在 https://deno.land/std/node/ 构建一个初步的兼容层,但它远未完成。

尽管 Deno 在简化模块系统方面采取了强硬立场,但最终 Deno 和 Node 是具有相似目标的非常相似的系统。随着时间的推移,我们期望 Deno 能够开箱即用地运行越来越多的 Node 程序。

HTTP 服务器性能

我们持续跟踪 Deno HTTP 服务器的性能。一个 hello-world Deno HTTP 服务器每秒处理约 2.5 万次请求,最大延迟为 1.3 毫秒。一个可比较的 Node 程序每秒处理 3.4 万次请求,最大延迟在 2 到 300 毫秒之间波动较大。

Deno 的 HTTP 服务器是使用 TypeScript 在原生 TCP sockets 之上实现的。Node 的 HTTP 服务器是用 C 语言编写的,并作为高级绑定暴露给 JavaScript。我们抵制了在 Deno 中添加原生 HTTP 服务器绑定的冲动,因为我们希望优化 TCP socket 层,更普遍地说,是 op 接口。

Deno 是一个合适的异步服务器,每秒 2.5 万次请求足以满足大多数目的。(如果不够,JavaScript 可能不是最佳选择。)此外,由于普遍使用 Promise(如上所述),我们预计 Deno 通常会表现出更好的尾部延迟。尽管如此,我们确实认为系统中有更多性能提升的空间,我们希望在未来的版本中实现这些提升。

TSC 瓶颈

Deno 内部使用微软的 TypeScript 编译器来检查类型并生成 JavaScript。与 V8 解析 JavaScript 所需的时间相比,它非常慢。项目早期,我们曾希望“V8 Snapshots”能在此处带来显著改进。快照确实有所帮助,但速度仍然令人不满意。我们确信可以在现有 TypeScript 编译器之上进行改进,但我们清楚,最终类型检查需要用 Rust 实现。这将是一项巨大的工程,不会很快发生;但它将为开发者体验的关键路径带来数量级的性能提升。TSC 必须移植到 Rust。如果您有兴趣在此问题上合作,请与我们联系。

插件 / 扩展

我们有一个初步的插件系统,用于通过自定义 ops 扩展 Deno 运行时。然而,该接口仍在开发中,并已被标记为不稳定。因此,访问 Deno 提供的原生系统之外的功能是困难的。

致谢

衷心感谢为本次发布做出贡献的众多贡献者。特别是:@kitsonk 在系统的许多部分都发挥了巨大作用,包括(但不限于)TypeScript 编译器宿主、deno_typescript、deno bundle、deno install、deno types、streams 实现。@kevinkassimo 在项目整个历史中贡献了无数的错误修复。他的贡献包括定时器系统、TTY 集成、wasm 支持、Deno.makeTempFile、Deno.kill、Deno.hostname、Deno.realPath、std/node 的 require、window.queueMircotask 和 REPL 历史记录。他还创作了 Deno 的标志。@kt3k 实现了持续基准测试系统(这对于几乎每一次重大重构都至关重要)、信号处理器、权限 API 和许多关键的错误修复。@nayeemrmn 贡献了 Deno 许多部分的错误修复,最值得注意的是他极大地改进了堆栈跟踪和错误报告,并为 1.0 版本的 API 稳定性提供了强有力的帮助。@justjavac 贡献了许多小型但关键的修复,使 Deno API 与 Web 标准保持一致,最著名的是他编写了 VS Code Deno 插件。@zekth 为 std 贡献了许多模块,其中包括 std/encoding/csv、std/encoding/toml、std/http/cookies 以及许多其他错误修复。@axetroy 协助处理了所有与 prettier 相关的事情,贡献了许多错误修复,并维护了 VS Code 插件。@afinch7 实现了插件系统。@keroxp 实现了 WebSocket 服务器并提供了许多错误修复。@cknight 提供了大量的文档和 std/node polyfills。@lucacasonato 几乎构建了整个 deno.land 网站。@hashrock 创作了许多惊人的艺术作品,例如 doc.deno.land 上的加载页面和本页顶部的精美图片!

HN 评论