跳到主要内容
Deno v1

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

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

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

随着 JavaScript 语言的不断变化,以及 TypeScript 等新功能的加入,构建 Node 项目可能会变得非常繁琐,涉及管理构建系统和其他繁重的工具,这会降低动态语言脚本编写的乐趣。此外,链接到外部库的机制从根本上是通过 NPM 仓库集中化的,这与 Web 的理想背道而驰。

我们认为 JavaScript 的格局和周围的软件基础设施已经发生了足够的变化,以至于简化是值得的。我们寻求一个有趣且高效的脚本环境,可以用于广泛的任务。

命令行脚本的 Web 浏览器

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

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

import { serve } from "https://deno.land/[email protected]/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 套接字为例。当套接字接收到传入数据包时,它会发出“data”事件。这些“data”回调将以不受约束的方式发出,使进程充斥着事件。由于 Node 继续接收新的数据事件,因此底层 TCP 套接字没有适当的背压,远程发送方不知道服务器已过载并继续发送数据。为了缓解这个问题,添加了一个 pause() 方法。这可以解决问题,但需要额外的代码;并且由于洪水问题仅在进程非常繁忙时才会出现,因此许多 Node 程序可能会被数据淹没。结果是一个尾部延迟很差的系统。

在 Deno 中,套接字仍然是异步的,但接收新数据需要用户显式 read()。不需要额外的暂停语义来正确构建接收套接字。这并非 TCP 套接字独有。系统最低级别的绑定层从根本上与 Promise 相关联 - 我们称这些绑定为“ops”。Deno 中的所有回调都以某种形式或另一种形式来自 Promise。

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

Rust API

我们发布的主要组件是 Deno 命令行界面 (CLI)。CLI 是今天版本 1.0 的内容。但 Deno 不是一个单体程序,而是设计为 Rust crate 的集合,以允许在不同层进行集成。

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

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

稳定性

我们承诺在 Deno 中维护一个稳定的 API。Deno 有很多接口和组件,因此透明地说明“稳定”的含义非常重要。我们发明的用于与操作系统交互的 JavaScript API 都位于“Deno”命名空间内(例如 Deno.open())。这些都经过仔细检查,我们将不会对它们进行向后不兼容的更改。

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

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

Deno 还有许多 Rust API,即 deno_core 和 rusty_v8 crate。这些 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 服务器是在原生 TCP 套接字之上用 TypeScript 实现的。Node 的 HTTP 服务器是用 C 编写的,并作为高级绑定公开给 JavaScript。我们抵制了向 Deno 添加原生 HTTP 服务器绑定的冲动,因为我们想优化 TCP 套接字层,更普遍地说是 op 接口。

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

TSC 瓶颈

在内部,Deno 使用 Microsoft 的 TypeScript 编译器来检查类型并生成 JavaScript。与 V8 解析 JavaScript 所需的时间相比,它非常慢。在项目早期,我们曾希望“V8 快照”能够在此处提供显着的改进。快照确实有所帮助,但仍然慢得令人不满意。我们当然认为可以在现有 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 历史记录。他还创作了徽标。@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 评论