Agent Developer Roadmap
JavaScript 运行时

Deno:权限优先、Web 标准与 Node 兼容层

核验日期:2026-07-16。本文以 Deno 2.9 为稳定版本线;Deno 2.9 于 2026-06-25 发布,继续增强 Node.js 26 兼容、锁文件迁移与内置测试能力。Deno minor 更新频繁,部署前应核对 Deno BlogRuntime Docs

Deno 不是“默认支持 TypeScript 的 Node”。它以 V8 为引擎,却重新设计了宿主接口、权限默认值、模块来源和工具链;Deno 2 又主动吸收 package.jsonnode_modules、npm 与大量 node: API,降低存量项目迁移成本。理解 Deno 的关键是同时保持两幅图:原生 Deno/Web 标准路径强调显式 URL、默认拒绝和一体化工具;Node 兼容路径把 npm、CJS、Node globals 和 native addon 映射进同一权限系统。两条路径能组合,但兼容层不是无成本同义替换。

学习目标与边界

读者完成本文后,应能:

  • 从 CLI、模块图、V8、Rust/Tokio 宿主到 OS 调用解释一段程序怎样运行。
  • 为读取、网络、环境、进程、系统信息、FFI 和远程 import 写出最小 allow/deny 策略。
  • 区分 Deno ESM、JSR、npm specifier、package.json dependency、local node_modules 与全局 cache。
  • 说明“原生执行 TypeScript”是即时去类型/转译,不等价于运行前完成类型检查。
  • deno fmt/lint/check/test/bench/coverage/task/compile 组成可复现工作流,并知道何时仍需外部工具。
  • 评估 Node 项目迁移中 CJS、package lifecycle scripts、Node-API addon、loader、隐式权限和行为兼容风险。

本文不把 Deno Deploy 等云产品与本地 Deno CLI 混为一谈。部署平台可以改变权限 flags、文件系统、生命周期和版本;例如平台可能在外层隔离环境中以广权限运行 Deno,不能由本地默认拒绝推导云端行为。

分层:相同的 V8,不同的宿主契约

ECMAScript 执行和 GC 仍由 V8 负责;Deno.readTextFilefetchDeno.Command 等由宿主实现。node:fs 在 Deno 中经过 Node compatibility layer,但最终读取仍受 Deno 文件权限约束。相同 V8 版本不保证 Node 与 Deno 的 timer、I/O、模块解析、错误对象或性能完全相同,因为宿主和调度实现不同。

一次原生请求的路径通常是:CLI 读取配置和入口;解析 import map/package 信息形成模块图;从本地、cache 或 registry 获取模块;必要时转译 TS/JSX;实例化并求值 ESM;Deno.serve 注册 handler;传入标准 Request;用户代码通过 Web API 或 Deno API访问宿主;返回 Response。权限在敏感宿主操作发生时判断,而不是仅在源文件下载时做一次扫描。

权限模型:默认拒绝到底拒绝什么

Deno Permissions列出主要能力:--allow-read--allow-write--allow-net--allow-env--allow-run--allow-sys--allow-ffi--allow-import,以及对应 --deny-*。deny 优先于 allow;-A/--allow-all 关闭这层保护,安全属性接近普通 Node 进程。

能力最小范围示例常见越权观察方式
文件读取--allow-read=./data/catalog.json给整个 home/root 读取拒绝错误的 permission/resource、规范化路径
文件写入--allow-write=./tmp允许覆盖源码、配置或密钥写入审计、临时文件/原子替换
网络--allow-net=api.example.com:443允许 loopback、metadata、任意端口DNS/redirect 后目标、请求 trace
环境--allow-env=API_URL--allow-env 暴露全部 secret读取名称审计,日志不输出值
子进程--allow-run=/usr/bin/git命令参数进入 shell、继承全部 envexecutable、argv、cwd、exit code
系统信息--allow-sys=cpus,systemMemoryInfo指纹/主机信息过度暴露具体接口名与拒绝结果
FFI--allow-ffi=./native原生库获得进程级能力library hash、平台、ABI、来源
import--allow-import=jsr.io,npmjs.com动态加载未审查远程代码lockfile、cache、来源 host
deno run \
  --no-prompt \
  --allow-read=./data/technologies.json \
  --allow-net=127.0.0.1:3000 \
  server.ts

这条命令适合只读本地数据并监听本机端口的示例。CI 使用 --no-prompt,因为非 TTY 本就不会依赖交互授权;把 prompt 当生产控制会造成环境差异。若服务还要访问上游,必须显式加入其 host:port。路径和网络范围应从真实需求推导,不为“先跑起来”直接 -A

allow/deny 不是依赖级 capability

权限授予当前进程中的代码,同一权限级别的第三方依赖可以调用已允许能力。Deno 不能自动保证“只有我的 config.ts 能读 API_KEY,而某个 npm 包不能”。如果依赖不可信,缩小整个进程的范围,拆进受限子进程/容器或通过消息传递提供最小代理能力。

权限也不限制纯 CPU、JavaScript heap 或无限 microtask。执行不可信代码仍需要进程/VM 级资源限制与网络隔离。eval、动态 import、Worker 等在相同权限域内不是安全沙箱。Deno 默认拒绝降低误用和供应链 blast radius,但不能替代输入验证、身份授权与多租户隔离。

符号链接、路径与网络重定向

文件 allow 范围必须结合 Deno 当前对 symlink 的规则核对。业务层仍应用固定根目录规范化用户路径,不应把 runtime permission 当 traversal 校验。网络 allow 检查也不替代 SSRF 防护:DNS rebinding、redirect、IPv4/IPv6 表示、私网和 metadata endpoint 要在受控 fetch 层处理。

模块来源与依赖身份

Deno 原生采用 ESM 和显式 specifier。现代项目可能同时出现:

  • 相对/绝对 file: 模块。
  • jsr: 包,面向跨 runtime 的 JavaScript Registry。
  • npm: 包,进入 npm 解析与兼容路径。
  • node: 内建模块,由 Deno compatibility layer 提供。
  • HTTPS URL import;是否采用取决于供应链策略。
  • deno.json#importspackage.json dependencies 和 workspace 映射。
{
  "imports": {
    "@std/assert": "jsr:@std/assert@^1.0.0",
    "catalog-core": "./src/catalog.ts",
    "chalk": "npm:chalk@^5.4.0"
  },
  "tasks": {
    "check": "deno fmt --check && deno lint && deno check src/server.ts && deno test"
  }
}

版本号仅为配置示例,真实仓库必须由 lockfile 固定解析结果。裸 specifier 通过 imports/package 信息解析,不应假设和 Node 的 node_modules 向上搜索完全一致。调试 Module not found 时记录原始 specifier、importer、deno.json、package scope、lockfile、cache/node_modules mode 和是否允许远程 import。

lock、cache、vendor 与离线

lockfile 固定内容完整性和版本解析;全局 cache 避免每个项目重复下载;vendor 可把远程依赖放入项目可审查目录;npm compatibility 可能使用 local node_modules。这些机制解决的问题不同:cache 命中不是版本契约,lockfile 也不保证 registry 永远在线,vendor 增加仓库体积与更新责任。

CI 应使用与团队版本相符的 frozen/lock 验证方式,先安装/缓存依赖,再在受限网络或离线步骤执行测试。不要让生产首次请求触发依赖下载。远程 URL import 必须有 lock 与来源策略;动态构造 URL 会扩大审计面。

TypeScript:可直接运行不等于已类型检查

Deno 能直接执行 .ts,运行时需要把类型语法移除/转译为 JavaScript。这个过程和完整 type checking 分离。deno run server.ts 的目标是执行,不应作为 CI 类型正确性的唯一证据;使用 deno check 或明确配置的测试/任务完成静态检查。

type Technology = { id: string; name: string };

function find(items: Technology[], query: string): Technology[] {
  return items.filter((item) => item.name.toLowerCase().includes(query.toLowerCase()));
}

console.log(find([{ id: "deno", name: "Deno" }], "de"));

运行 deno check example.ts 预期无诊断;运行 deno run example.ts 预期输出含 Deno 项。类型在 runtime 不存在,外部 JSON 仍要运行时校验。与 Node/Bun 比较启动时,要注明是否包含 type check;把 Deno 的 check 时间算入而不把 Node 的 tsc 算入是不公平实验。

Node 与 npm 兼容:分层验证而非布尔结论

Deno 2 支持 package.json、npm packages、CommonJS、许多 node: APIs、node_modules 和 Node-API addon。Deno 2.9 继续改善现有 Node 项目的直接迁移;但“Node compatible”必须拆成至少五层:

  1. 依赖能被解析和安装。
  2. ESM/CJS 与 exports 选择正确。
  3. 所用 Node globals/built-ins 的行为足够一致。
  4. lifecycle script/native addon 成功且权限明确。
  5. 框架、测试、诊断和部署在真实负载下通过。

Deno Node/npm Compatibility说明,Node built-ins 推荐显式 node:;npm 包仍处在 Deno 权限系统内。CommonJS 解析通常需要读取 package.jsonnode_modules,因此会要求文件/环境权限。Node-API addon 需要 local node_modules--allow-ffi,许多包还依赖默认不执行的 postinstall 下载或编译二进制。

import { readFile } from "node:fs/promises";

const text = await readFile(new URL("./data/technologies.json", import.meta.url), "utf8");
console.log(JSON.parse(text).length);

同一代码在 Node 与 Deno 中形式兼容,但 Deno 运行需 deno run --allow-read=./data data.ts。这正是兼容与权限正交的例子。迁移测试要保留“无权限应失败”的负例,避免把最终命令悄悄变成 -A

lifecycle scripts 与供应链

Deno 默认不执行 npm lifecycle scripts,因为脚本可执行任意安装时代码。某些 native 包因此缺失下载的二进制。正确流程是查看被忽略脚本、审查来源和内容,只信任必要包,再锁定产物;不要全局允许所有脚本。安装成功也要在目标 OS/arch 运行 addon fixture,检查 ABI、libc 和容器镜像依赖。

兼容测试数字的边界

Deno Release 可能公布 Node test suite pass rate。它是持续改进的有价值证据,但测试选择、跳过条件、平台和 Node 参照会影响数字。不能由总通过率推导某个 ORM、observability agent 或 native driver 一定可用。项目自己的依赖/行为对照才是上线门槛。

Deno.serve 与标准 Request/Response

type Technology = { id: string; name: string; category: string };

const source = new URL("./data/technologies.json", import.meta.url);

Deno.serve({ hostname: "127.0.0.1", port: 3000 }, async (request) => {
  const url = new URL(request.url);
  if (request.method !== "GET") {
    return Response.json({ code: "METHOD_NOT_ALLOWED" }, {
      status: 405,
      headers: { allow: "GET" },
    });
  }
  if (url.pathname !== "/technologies") {
    return Response.json({ code: "NOT_FOUND" }, { status: 404 });
  }

  try {
    const items: Technology[] = JSON.parse(await Deno.readTextFile(source));
    const query = url.searchParams.get("query")?.toLowerCase() ?? "";
    return Response.json(items.filter((item) => item.name.toLowerCase().includes(query)));
  } catch (error) {
    console.error("catalog_read_failed", { name: error?.name });
    return Response.json({ code: "CATALOG_UNAVAILABLE" }, { status: 503 });
  }
});

环境:Deno 2.9;命令:deno run --no-prompt --allow-read=./data/technologies.json --allow-net=127.0.0.1:3000 server.ts。请求 curl 'http://127.0.0.1:3000/technologies?query=deno' 预期返回匹配数组。去掉 read permission 应稳定失败并返回 503,内部日志只记录错误类型,不泄露绝对路径。

真实服务应在启动时读取并校验只读数据,避免每请求磁盘读取;若需要热更新,设计原子替换和版本字段。示例故意展示权限发生在 I/O 边界。标准 Request/Response 提高核心逻辑迁移性,但 Deno.serve 的监听、TLS、并发和 shutdown 行为仍需按版本测试。

Web API 与 Deno API 的选择

跨 runtime 核心优先 fetchURLAbortControllerReadableStreamWeb CryptoTextEncoder 等标准 API。需要文件、命令、系统信息、Unix signal 时使用 Deno API,并让适配层显式拥有。不要为了“可移植”创造一个覆盖所有宿主的巨大 abstraction;通常只需把数据读入和 handler registration 放在两端,中间保持纯函数。

fetch 的存在不保证所有实现细节一致。代理、DNS、TLS、连接池、自动解压、HTTP/2、header 限制与错误类需用代表测试验证。标准 API 也不意味着浏览器安全模型:服务器没有浏览器 CORS 强制保护,仍需自行限制目标和凭据。

异步、取消与资源

Deno 使用 Promise-first API,事件循环仍可能被同步 CPU 阻塞。await 只在 Promise pending 时让出执行,不会把 JSON.parse、正则、加密循环移到别处。CPU 密集工作用 Web Worker/进程或外部服务,并设置有界并发。

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort("deadline"), 2_000);

try {
  const response = await fetch("https://api.example.com/catalog", {
    signal: controller.signal,
  });
  if (!response.ok) throw new Error(`upstream ${response.status}`);
  await response.body?.cancel();
} finally {
  clearTimeout(timeout);
}

现代环境可用 AbortSignal.timeout/any 时减少手写 timer,但部署前核对 Deno 版本。取消是协作协议:底层 API 必须接受 signal,业务循环也要检查。响应体不消费或取消可能占连接资源。对 streaming 使用 Web Streams backpressure,不把大 body 全量 arrayBuffer()

Worker、进程与 FFI

Web Worker 提供独立 realm/Isolate 以处理 CPU 或隔离模块状态,消息使用 structured clone/transferable;权限行为要按当前 Deno 文档显式配置和测试。Deno.Command 启动 OS 进程,隔离更强但成本更高;默认拒绝要求 --allow-run。FFI 与 native addon 进入原生代码后可破坏进程安全,必须视为高信任依赖。

方案适用主要成本必验失败
同一事件循环短 CPU + 大量 async I/O同步阻塞影响全部请求event-loop lag、无界并发
Web Worker pool可分割 CPU、较大任务启动、内存、消息复制worker crash、队列满、取消
Deno.Command外部工具、强隔离/不同语言进程启动、argv/env、安全executable 不存在、timeout、僵尸进程
FFI/Node-API必需原生库ABI、崩溃、权限、供应链错架构、缺动态库、非法内存

内置工具链的真实边界

Deno 单一 binary 提供 fmt、lint、check、test、bench、coverage、doc、task、compile 等,减少版本拼装和配置漂移。这是维护收益,不表示外部工具永远不需要。框架 bundling、浏览器 E2E、复杂 lint rule、APM agent 或特殊测试生态仍可能使用独立工具。

推荐最小任务:

{
  "tasks": {
    "verify": "deno fmt --check && deno lint && deno check src/server.ts && deno test --allow-read=./fixtures"
  }
}

测试权限也应最小化。若测试需要网络,优先本地 fake server 并只 allow loopback;真实第三方调用作为手动集成测试。deno test 的 sanitizer 能发现资源/异步操作泄漏时,应修复泄漏而不是全局关闭。coverage 是缺口提示,不等价于断言质量。

deno compile

compile 可把程序与依赖打进可执行文件,便于分发,但输出仍受目标平台、动态资源、权限 flags、CA/系统库和 native addon 限制。它不是把任意 Node 项目变成完全静态单文件。验收需在目标 OS/arch 启动、读取资源、处理信号和升级回滚。

配置、环境与 secret

deno.json 是项目配置和任务入口;.env 只是数据来源,不应提交生产 secret。通过 --allow-env=NAME 把可读变量限制到必要名称。配置解析要在启动阶段完成并验证类型/范围,错误时 fail fast;不要让每个请求重新读取环境。

错误日志只记录变量名缺失,不打印值。deno task、npm scripts 和子进程会继承环境时,需要显式构造 allowlist env。CI secret masking 不能保证任意编码后的值被遮蔽,根本措施是不记录。

性能与基准

Deno 2.x Release 包含启动、HTTP、npm install、Node compatibility 等性能数据。这些结果可用来定位改进方向,但必须查看版本、硬件、样例和脚本。自己的决策按工作负载测试:

  1. 冷 CLI:无 cache/有 cache、模块数、是否 type check。
  2. 常驻 HTTP:固定 handler、连接复用、并发与 body,记录 p50/p95/p99、错误、CPU、RSS。
  3. Node compatibility:同一包与配置在 Node/Deno 执行,比较正确性后再测。
  4. 权限开销:相同能力范围与日志配置,不能一边 -A 一边细粒度后宣布差异。
  5. install:固定 registry/cache/lock/node_modules linker,区分 cold network、warm cache 与 CI restore。

即时 TS 转译、远程依赖 cache、JIT warmup 和平台 DNS/TLS 都会混入结果。保留命令、环境与原始 samples,不用单次 time 宣布普遍结论。

调试与可观测性

Deno 支持 inspector/DevTools、日志、CPU/heap 观察及内置测试输出;Deno 2.8/2.9 还持续改善网络调试。工具能力随版本演进,生产 agent 的 Node compatibility 不能想当然。

统一日志仍应使用 request id、稳定事件名、duration/status 与安全错误 cause。跨 Worker/子进程显式传上下文。profile 开销和敏感性要记录。平台部署时结合平台 invocation log、CPU/wall、驱逐/冷启动信息,不只看应用 console.log

生命周期与关闭

本地 Deno 进程处理 SIGTERM 时应停止监听、拒绝新工作、等待有界在途请求、关闭数据库/worker/子进程并设置退出码。具体 server shutdown API 按 Deno 2.9 文档实现。不要依赖模块顶层对象析构自动 flush。

部署到 serverless/edge 平台后,实例可能被驱逐,后台 Promise 在响应结束后可能取消。使用平台提供的生命周期机制;业务写入保持幂等并持久化。Deno CLI 的长进程经验不能直接套到 Deno Deploy 或其他平台。

高频错误与定位

表现根因观察修正
本地提示授权、CI 直接失败CI 无 TTY/--no-prompt,权限未声明完整命令与 permission descriptor显式最小 flag,不依赖 prompt
npm 包安装成功但运行缺文件postinstall 未执行或 local node_modules 模式不符install warning、包脚本、文件树审查并信任必要脚本,固定模式
node:os 调用拒绝Deno 对兼容 API 同样施加 --allow-sysAPI name 与拒绝资源只允许所需 sys interface
.ts 能运行但 CI 类型错执行转译不等于 check独立 deno checkverify 中显式检查
首次启动慢、之后快依赖下载/cache/转译/JIT 混在一起cold/warm cache 分组生产预取,分别记录阶段
迁移后可用但 APM 无 traceagent 依赖未实现 Node hook/inspector最小 instrumentation fixture选支持路径或显式 telemetry

迁移路径

从 Node 到 Deno 不必一次重写。最小风险顺序是:先在现有项目用 Deno package manager/任务执行做隔离实验;建立纯 JS/TS 单元测试;显式 node: imports 与 package boundaries;用 Deno 跑 CI 兼容组合;收紧权限;替换少量宿主边界为 Web/Deno APIs;最后才切生产 runtime。每一步保留 Node 回退命令。

绿色项目可优先 Web APIs、ESM、JSR/明确 imports、内置工具和默认拒绝。不要仅为了“Deno 风格”替换工作正常且有测试的 npm 包;迁移价值应落到安全边界、工具维护、部署或性能的可测改善。

官方资料

交付检查

完成示例时保存:Deno 精确版本;deno.json 与 lockfile;模块来源清单;最小 allow/deny 命令和拒绝测试;无 -A 的 CI;fmt/lint/check/test 结果;一个 npm/CJS fixture 和一个 Node-API 风险记录;HTTP 取消/流/错误契约;进程/平台生命周期说明;冷/热分组性能原始数据。这样才能说明选择的是一套可验证的宿主契约,而不是一句“更安全的 Node”。

权限设计演练:从审计到最小命令

不要从 -A 开始再凭感觉删 flag。先在隔离测试环境列出应用预期能力:入口和配置文件、只读数据目录、监听地址、上游 host、必需环境变量、是否启动子进程、是否加载 FFI。为每项写允许范围和拒绝后的用户行为。然后以默认权限运行,逐个根据明确的 PermissionDenied 补最小范围;每补一项都记录调用者和业务理由。

得到可运行命令后再写负例:读取同目录外文件应失败;访问非 allow host 应失败;读取未列出的 secret 应失败;启动另一个 executable 应失败。deny 优先级可用于“允许大范围但明确排除敏感子范围”的少数场景,不过更小 allowlist 通常更容易审计。CI 固定 --no-prompt,否则开发者点击授权得到的行为无法复现。

第三方包升级时重跑权限负例。若一个原本纯计算包突然请求环境或网络,权限失败是供应链行为变化的信号,不应立即扩大权限。查看 release、diff 与调用栈,确认功能是否必要。对必须执行的 npm lifecycle script,记录包名、版本、脚本、下载目标、hash 和允许原因;新版本重新审查,不能永久信任名称。

权限清单还应和容器/平台策略对照。Deno 只允许某个 host,而容器 egress 允许全网时仍有一层纵深;反过来,Deno allow 不能越过平台 network policy。文件只读 permission 也不能阻止 native/FFI 绕过高层 API,所以 --allow-ffi 应视为权限模型的高风险出口,并配合进程隔离。

模块供应链演练

从空 cache 和断网两种状态验证依赖。联网安装阶段应按 lock 解析且拒绝漂移;离线测试阶段不访问 registry/CDN;生产镜像启动不下载模块。列出所有 file/jsr/npm/http/node 来源,检查远程 URL 是否固定版本和完整性。全局 cache 可加速 CI,但 cache key 应包含 Deno 版本、lock 和平台,且 cache 恢复后仍执行 lock 校验。

故意破坏一个 lock entry 或替换 vendor 内容,预期验证步骤失败;如果仍成功,说明流程只命中旧 cache 而未证明输入完整性。再模拟 registry 503,安装阶段应清楚失败,运行阶段在依赖已准备时应正常。这样才能区分供应链可用性和应用可用性。

Node 项目迁移工作表

对存量项目按 package 建表:是否 CJS;使用哪些 node: APIs;是否读 process.binding/V8 internal;是否有 install script/.node;是否要求 local node_modules 布局;是否注入 loader/agent;运行需要哪些 Deno permission。先迁移无宿主依赖的叶子包和测试,再到 HTTP/数据库边界。

每个不兼容项只有三种处理:升级/替换依赖;在薄 adapter 中改用 Deno/Web API;保留 Node。不要 fork 大型依赖只为消除一个临时兼容缺口,除非组织愿意长期维护。Deno minor 改善后重新验证,删除 adapter 比永久兼容层更好。双 runtime 期间确保同一数据契约和 error code,避免迁移同时改变业务语义。

Deno 服务容量与恢复

与 Node 一样记录 event-loop/CPU、RSS、连接、worker queue 和上游;另记录 permission 配置、module cache 状态与 Deno 精确 minor。deno compile 产物要在目标平台演练启动、配置缺失、信号和回滚。部署到 Deno 云平台时重新建立生命周期表,不能把本地 SIGTERM 行为当作平台驱逐保证。关键后台任务必须持久化,所有读取 cache 可重建。

上一篇:Node.js · 返回专栏导读 · 下一篇:Bun

在 GitHub 上编辑此页

本页目录