首页
/ Deno 运行时入门与架构解析:从安装、第一个 Web 服务到 V8 + Rust + Tokio 分层设计

Deno 运行时入门与架构解析:从安装、第一个 Web 服务到 V8 + Rust + Tokio 分层设计

2026-09-06 11:30:05作者:田桥桑Industrious

Deno 是一个以安全默认值和良好开发者体验为核心的 JavaScript、TypeScript 与 WebAssembly 运行时,构建在 V8、Rust 和 Tokio 之上。本文以仓库根目录 README 为主线,覆盖安装、第一个 Deno.serve 服务、权限模型等实战内容,并结合 doc/architecture.md 等仓库文档与源码,解释 deno run --allow-net server.ts 背后逐层工作的实现原理。

一、Deno 是什么

README 对 Deno 的定义非常直接:它是一个 JavaScript、TypeScript 和 WebAssembly 运行时(/ˈdiːnoʊ/,读作 “dee-no”),特性上强调两点:

  • 安全默认值(secure defaults):程序默认不能读取文件、访问网络、执行子进程,必须显式通过 --allow-* 参数或运行时权限询问授予能力;
  • 基于三大技术栈:V8(JavaScript 引擎)、Rust(系统语言)、Tokio(异步运行时)。

当前仓库的版本号为 2.9.6,记录在 cli/lib/version.txt

二、安装 Deno

README 给出了五种官方安装途径,覆盖 macOS、Linux 与 Windows。以下命令均可直接复制运行:

Shell(Mac、Linux):

curl -fsSL https://deno.land/install.sh | sh

PowerShell(Windows):

irm https://deno.land/install.ps1 | iex

Homebrew(Mac):

brew install deno

Chocolatey(Windows):

choco install deno

WinGet(Windows):

winget install --id=DenoLand.Deno

Scoop(Windows):

scoop install main/deno

README 同时说明,以上只是部分途径,完整安装方式列表见 Deno 官方文档。若希望从源码构建安装,则参考贡献指南(.github/CONTRIBUTING.md 中的 “Building from source” 一节)。构建所需的前提是理解本仓库的工作区结构——根 Cargo.toml 定义了一个包含 cliruntime、数十个 ext/*libs/* 成员的 Cargo workspace,这正是下文分层架构在工程上的对应物。

三、你的第一个 Deno 程序:用 Deno.serve 写一个 Web 服务

README 指出,Deno 最典型的用途是构建 Web 服务器。完整步骤如下:

创建文件 server.ts

Deno.serve((_req: Request) => {
  return new Response("Hello, world!");
});

然后运行:

deno run --allow-net server.ts

启动后,本地 Web 服务默认监听 http://localhost:8000,访问该地址即可看到 Hello, world!

3.1 为什么需要 --allow-net

这里的权限参数不是装饰,而是 Deno 安全模型的入口:

  • --allow-net 授予程序打开网络监听/连接的许可。不带该参数运行同一文件时,Deno.serve 会在权限检查处报错,而不是默认放行。
  • 该 flag 的解析逻辑在 cli/args/flags.rs 中,可以看到 --allow-net 支持精确到地址的白名单形式(如 --allow-net=8000--allow-net=127.0.0.1:8000),源码中会将其拼接为 --allow-net=<allowlist> 参数。
  • 权限的实际执行发生在 Rust 侧的 op 边界。doc/architecture.md 明确指出:“权限在 Rust 中、在 op 边界处检查,绝不在 JavaScript 中检查”,且“未授予的能力会让该 op 在执行任何工作之前就报错”。对应的权限模型代码位于 runtime/permissions/ 目录。

3.2 Deno.serve 的默认端口 8000 从哪里来

Deno.serve 的实现位于 ext/http/00_serve.ts。其中:

  • 函数入口 serve(arg1, arg2) 支持两种签名:直接传 handler 函数,或传 { handler, port, hostname, ... } 选项对象(见 ext/http/00_serve.ts);
  • README 中省略 port 时服务落在 8000 端口,对应源码中的默认值 port: options.port ?? 8000(见 ext/http/00_serve.ts)。

也就是说,README 示例中的 http://localhost:8000 并非文档随口写的地址,而是 Deno.serve 内置的默认监听端口。

四、架构总览:README 背后的五层堆叠

README 本身是面向用户的最小文档,而 doc/architecture.md 给出了运行时本身的分层设计,这是理解“deno run 一条命令如何工作”的关键。原文档中的层叠结构如下(自顶向下,每层只依赖其下的层):

+-----------------------------------------------------------+
|  cli/            the `deno` binary: subcommands, tooling   |
+-----------------------------------------------------------+
|  runtime/        deno_runtime: assembles the JS runtime     |
+-----------------------------------------------------------+
|  ext/*           extensions: native capabilities for JS     |
+-----------------------------------------------------------+
|  libs/*          deno_core + supporting crates (V8 bridge) |
+-----------------------------------------------------------+
|  V8 + Tokio      JavaScript engine and async runtime       |
+-----------------------------------------------------------+

4.1 CLI 层(cli/):用户直接触碰的一切

deno crate 拥有 flag 解析、所有子命令(runtestfmtlintcompilebundleinstallpublish 等)、包管理工具、LSP,以及把模块解析与运行时连接起来的 module loader。关键入口文件:

文件 职责
cli/main.rs 进程入口与命令路由
cli/args/flags.rs 完整的 clap flag 与子命令定义,新增 flag 或子命令从这里开始
cli/tools/<tool>/ 每个子命令一个模块(简单命令如 cli/tools/fmt.rs,复杂命令如 cli/tools/test/ 目录)
cli/module_loader.rs 解析并加载模块,把 resolver 与 module graph 桥接到运行时

架构文档强调 CLI 层是“刻意沉重”的:它引入 TypeScript 类型检查、npm 与 JSR 解析、lockfile、bundler 等能力,而更低的层不允许反向依赖它。

4.2 运行时层(runtime/):deno_runtime crate

这一层把 deno_core 加上一组精选的 extensions 组装成一个可工作的 JavaScript 运行时,是希望嵌入“Deno 运行时”而非“Deno CLI”的外部项目使用的部分。关键文件:

4.3 扩展层(ext/*):平台能力的真正所在

ext/ 下每个目录都是一个自包含的 extension:一个 Rust crate,定义 ops(可从 JS 调用的原生函数),外加在其上暴露高层 API 的 JavaScript 模块。Web 平台能力(ext/webext/fetchext/cryptoext/webgpu 等)、系统访问(ext/fsext/netext/process 等)、Deno 独有特性(ext/kvext/cronext/ffiext/napi)以及 Node 兼容层(ext/node/ 承载大部分 node:* 内建模块,另有 ext/node_cryptoext/node_sqlite)都分布在这里。

一个 extension 的典型形态是三步:

  1. Rust 侧用 #[op2] 函数完成特权操作,需要时附带权限检查;
  2. 00_*.js / 01_*.js 等编号 JS 模块构建公开 API,通过 Deno.core.ops 调用这些 op——例如本文第三节的 Deno.serve 就位于 ext/http/00_serve.ts,同层的原生 op 定义在 ext/http/lib.rs
  3. runtime/worker.rs(以及 CLI 的 snapshot 构建)中注册该 extension,使其成为组装后运行时的一部分。

架构文档还给出了一条明确的扩展守则:新增原生功能时,应在对应的 ext/<name>/ crate 中添加 op,而不是伸手去改 runtime 或 CLI。

4.4 核心层(libs/*):Rust 与 V8 之间的桥

libs/ 保存 deno_core 及支撑 crate,拥有 op 基础设施、module loader trait、snapshot 机制、JsRuntime 事件循环,以及 serde_v8 序列化层。值得注意的成员:

这些 crate 刻意不含任何 CLI 关注点,以便独立单测并被其他工具复用。

4.5 横切概念:Ops、Extensions、Workers、Resources、Permissions

架构文档归纳了五个贯穿全栈的概念:

  • Ops 是 JavaScript 触及原生代码的唯一途径。同步 op 立即返回,异步 op 返回一个在未来事件循环上解决的 future;
  • Extensions 把 op 与其 JavaScript 打包在一起,是运行时组合的基本单元;
  • Workers 是隔离的 JS 执行上下文(主 worker 与 Web Worker),各自拥有独立的 V8 isolate;
  • Resources 是被管理的句柄(打开的文件、socket、reader),由 deno_core 跟踪,以整型 id 跨 Rust/JS 边界传递;
  • Permissions 在 Rust 侧的 op 边界强制执行,未授予的能力会让 op 在任何工作发生前就报错。

五、定位你的改动:架构文档中的速查表

doc/architecture.md 末尾给出了一张“我要做 X,应该从哪开始”的表,对理解各模块职责边界非常有用:

需求 起始位置
添加或修改 CLI flag / 子命令 cli/args/flags.rscli/tools/
给 JS 增加原生能力 ext/<name>/(op + JS)
修改运行时的组装方式 runtime/worker.rs
触碰 Rust/V8 桥或 op 宏 libs/corelibs/ops
修改模块 / npm / JSR 解析 libs/resolverlibs/npm、CLI

更细粒度的目录地图见 doc/codebase-map.md,各层的测试方式见 doc/testing.md,此外 doc/ci.md 解释了 CI 工作流如何生成、以及纯文档改动(仅触碰 doc/ 的 PR)为何只需跑 lint 任务。

六、延伸阅读资源

README 列出的官方资源与本文相互印证:

  • Deno Docs:运行时、部署等的官方指南与参考文档;
  • Deno Standard Library@std):官方维护的通用工具库;
  • JSR:面向现代 JavaScript/TypeScript 的开源包注册表,其解析逻辑在仓库中对应 cli/jsr.jslibs/resolver 等模块;
  • 贡献指南:见 .github/CONTRIBUTING.md,涵盖从源码构建的完整步骤。

七、小结

从仓库视角看,README 中“安装 → 写 server.tsdeno run --allow-net server.ts → 访问 localhost:8000”这条最短路径,实际上穿过了整条技术栈:cli/args/flags.rs 解析出 --allow-netcli/ 的 module loader 拉取并解析 server.tsruntime/worker.rs 组装 isolate 与 op 集合,ext/http 提供 Deno.serve(默认端口 8000),libs/corelibs/ops 承担 Rust/V8 桥接,而 net 权限在 op 边界完成裁决。理解这条调用链,也就理解了 Deno“安全默认 + 分层可复用”这两大设计目标的工程落地方式。

登录后查看全文
热门项目推荐
相关项目推荐