动态语言是有用的工具。脚本允许用户快速简洁地将复杂系统连接在一起并表达想法,而无需担心内存管理或构建系统等细节。近年来,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 编写的。
从头到尾的承诺
Node 是在 JavaScript 拥有 Promise 或 async/await 概念之前设计的。Node 对 promise 的对应物是 EventEmitter,重要的 API 是围绕它构建的,即套接字和 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 箱子,以允许在不同层进行集成。
的 deno_core 箱子是一个非常精简的 Deno 版本。它没有依赖于 TypeScript 也不依赖于 Tokio。它只是提供了我们的 Op 和资源基础设施。也就是说,它提供了一种将 Rust future 绑定到 JavaScript promise 的组织方式。CLI 当然完全构建在 deno_core 之上。
的 rusty_v8 箱子为 V8 的 C++ API 提供了高质量的 Rust 绑定。该 API 试图尽可能地匹配原始的 C++ API。它是一个零成本绑定——在 Rust 中公开的对象正是您在 C++ 中操作的对象。(以前尝试 Rust V8 绑定强迫使用持久句柄,例如。)该箱子提供了在 Github Actions CI 中构建的二进制文件,但它也允许用户从头开始编译 V8 并调整其许多构建配置。所有 V8 源代码都在箱子本身中分发。最后,rusty_v8 试图成为一个安全的接口。它还不完全安全,但我们越来越接近。能够以安全的方式与像 V8 这样的 VM 进行交互非常棒,并且让我们能够发现 Deno 本身中的许多难以发现的错误。
稳定性
我们承诺在 Deno 中维护一个稳定的 API。Deno 有很多接口和组件,因此在“稳定”的含义方面保持透明很重要。我们发明的用于与操作系统交互的 JavaScript API 都位于“Deno”命名空间中(例如 Deno.open()
)。这些经过仔细检查,我们不会对它们进行向后不兼容的更改。
所有尚未准备好稳定化的功能都隐藏在 --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 服务器每秒大约可以处理 25k 个请求,最大延迟为 1.3 毫秒。一个类似的 Node 程序每秒可以处理 34k 个请求,最大延迟在 2 到 300 毫秒之间,变化很大。
Deno 的 HTTP 服务器是用 TypeScript 在原生 TCP 套接字之上实现的。Node 的 HTTP 服务器是用 C 编写的,并作为高级绑定暴露给 JavaScript。我们抵制了添加原生 HTTP 服务器绑定的诱惑,因为我们希望优化 TCP 套接字层,更广泛地说,是操作接口。
Deno 是一个真正的异步服务器,每秒 25k 个请求对于大多数目的来说已经足够了。(如果不是,JavaScript 可能不是最好的选择。)此外,我们预计 Deno 通常会表现出更好的尾部延迟,因为普遍使用承诺(如上所述)。尽管如此,我们相信系统中还有更多性能提升空间,我们希望在未来的版本中实现这一点。
TSC 瓶颈
Deno 在内部使用 Microsoft 的 TypeScript 编译器来检查类型并生成 JavaScript。与 V8 解析 JavaScript 所需的时间相比,它非常慢。在项目早期,我们希望“V8 快照”能够在此提供显著的改进。快照确实有所帮助,但速度仍然令人不满意地慢。我们当然认为,在现有 TypeScript 编译器的基础上,可以进行改进,但对我们来说很明显的是,最终需要用 Rust 实现类型检查。这将是一项巨大的工程,而且不会很快发生;但这将为开发人员遇到的关键路径提供数量级的性能提升。TSC 必须移植到 Rust。如果您有兴趣协作解决这个问题,请与我们联系。
插件/扩展
我们有一个新生的插件系统,用于用自定义操作扩展 Deno 运行时。但是,此接口仍在开发中,已被标记为不稳定。因此,访问超出 Deno 提供的原生系统很困难。
致谢
衷心感谢 众多贡献者 使此版本成为可能。尤其是:@kitsonk 在系统的许多部分(包括但不限于 TypeScript 编译器主机、deno_typescript、deno bundle、deno install、deno types、流实现)方面做出了巨大贡献。@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 上的加载页面以及此页面顶部的精美图片!