# Deno 中文生态中心 Source: /docs/ Deno 是一个 JavaScript、TypeScript 与 WebAssembly 运行时。它直接运行 TypeScript,默认隔离文件、网络、环境变量和子进程访问,并在单个 CLI 中提供依赖管理、格式化、Lint、测试、文档和编译工具。 本站不镜像 [Deno 官方文档](https://docs.deno.com),而是为中文开发者整理迁移路线、框架选型、生产边界与可直接实践的项目蓝图。 ## 选择你的任务 ## 能力地图 | 目标 | 首选入口 | 文档 | | --- | --- | --- | | 运行 TypeScript | `deno run main.ts` | [运行时](/docs/core/runtime) | | 收紧系统访问 | `--allow-*` / `--deny-*` | [权限](/docs/core/permissions) | | 添加 JSR 或 npm 包 | `deno install ` | [依赖](/docs/core/dependencies) | | 格式化、Lint、类型检查 | `deno fmt && deno lint && deno check` | [内置工具](/docs/core/tooling) | | 执行测试 | `deno test` | [测试](/docs/core/testing) | | 可复现安装与安全扫描 | `deno ci && deno audit` | [供应链安全](/docs/core/supply-chain) | | 统一项目命令 | `deno task ` | [CLI 与配置](/docs/reference/cli-and-config) | | 构建全栈页面 | Fresh 2 | [Fresh 教程](/docs/web/fresh) | | 构建跨 runtime API | Hono | [Hono 教程](/docs/web/hono) | | 连接 PostgreSQL | postgres.js / Drizzle | [数据库](/docs/database/postgres-drizzle) | | 暴露 AI 工具 | MCP TypeScript SDK | [MCP Server](/docs/ai/mcp-server) | | 分发单文件程序 | `deno compile` | [编译分发](/docs/core/compile) | | 搭建 CI 流水线 | `setup-deno` + `deno ci` | [CI/CD 实战](/docs/deploy/ci-cd) | | 管理环境变量 | `--env-file` / `--allow-env` | [环境变量](/docs/core/env-variables) | | 在三个运行时之间选型 | 维度对比 | [Deno vs Node vs Bun](/docs/reference/deno-vs-node-bun) | 本文以 Deno 2.x 为稳定基线。涉及小版本、预览 API 或托管平台行为时,先运行 `deno --version`,再核对官方对应页面与发布说明。 每个页面 URL 都可追加 `.md` 获取 Markdown;全站另有 [`/llms.txt`](/llms.txt) 与 [`/llms-full.txt`](/llms-full.txt)。 --- # 关于本站 Source: /docs/about/ 本站是一套中文优先、英文完整覆盖的 Deno 生态实战手册,不隶属于 Deno Land Inc.,也不替代 [Deno 官方文档](https://docs.deno.com)。它重点回答官方 API 参考之外的迁移顺序、框架选型、项目结构、生产边界与中文开发者常见问题。 ## 事实来源 基础内容复核基于 [`denoland/docs`](https://github.com/denoland/docs) 提交 [`e0241c6`](https://github.com/denoland/docs/commit/e0241c6ffd7fffafcf707e898a038be5685e5a97),并在页面标注日期重新查询 Fresh、Hono、Oak、MCP、OpenAI 与部署平台的一手文档。运行时 API 以官方文档、Deno CLI 帮助和 [`denoland/deno`](https://github.com/denoland/deno) 为准;框架与云平台行为以各自官方文档为准。 ## 编辑原则 - 不镜像完整 API 参考,只维护任务路径、选择边界和验证方法。 - 易变化行为写明版本、日期或“发布前核对”。 - 示例使用最小权限,不用 `-A` 掩盖授权需求。 - 中英文保持相同 slug 和语义结构,但分别按语言习惯撰写。 - 安全、云平台与 AI SDK 陈述只采用一手来源。 - 页面事实复核日期记录在 `lastVerified` frontmatter。 ## 机器可读访问 单页追加 `.md`,目录使用 `/llms.txt`,全量语料使用 `/llms-full.txt`。这些端点带 `noindex`,避免与 HTML 页面竞争搜索索引。 发现错误时,请携带 Deno 版本、页面 URL、一手来源和可复现命令提交修改。 --- # AI / Agent 入口 Source: /docs/ai/ ## 读取顺序 1. 先读取 [`/llms.txt`](/llms.txt) 获取页面索引。 2. 根据任务读取单页 `.md`,例如 [`/docs/core/permissions.md`](/docs/core/permissions.md)。 3. 只有跨模块任务才读取 [`/llms-full.txt`](/llms-full.txt)。 4. 修改前读取 `deno.json(c)`、`deno.lock`、`package.json`、现有 imports、tasks 和 CI。 5. 修改后运行 `fmt --check`、`lint`、`check` 与相关测试,并报告实际命令。 ## 任务路由 | 目标 | 首读 | 追加 | | --- | --- | --- | | 运行/调试 TypeScript | [运行时](/docs/core/runtime) | [权限](/docs/core/permissions) | | 安装依赖 | [依赖](/docs/core/dependencies) | [CLI 与配置](/docs/reference/cli-and-config) | | 修改 Monorepo | [Workspaces](/docs/core/workspaces) | [生产基线](/docs/deploy/production-baseline) | | 新增测试 | [测试](/docs/core/testing) | [内置工具](/docs/core/tooling) | | 迁移 Node 项目 | [从 Node.js 迁移](/docs/migration/from-node) | [常见错误](/docs/reference/errors) | | 构建 AI 服务 | [AI 应用](/docs/ai/building-ai-apps) | [权限](/docs/core/permissions) | | 调用 OpenAI | [Deno + OpenAI](/docs/ai/openai) | [AI 应用](/docs/ai/building-ai-apps) | | 构建 MCP 工具 | [MCP Server](/docs/ai/mcp-server) | [Agent 架构](/docs/ai/agent-loop) | | 设计 Agent 循环 | [Agent 架构](/docs/ai/agent-loop) | [Agent 规则](/docs/ai/agent-rules) | | 运行不可信 Agent 代码 | [Deno Sandbox](/docs/ai/sandbox) | [权限](/docs/core/permissions) | | 发布服务 | [部署决策](/docs/deploy) | 目标平台官方文档 | ## 不变量 ```md - Do not replace the repository's package or task conventions before reading them. - Do not grant -A to silence a permission error; identify the exact resource. - Running TypeScript does not replace `deno check` in CI. - Keep deno.lock unless the user explicitly approves dependency resolution changes. - Verify current Deno, framework, and platform behavior from primary sources. - Never print environment values or pass untrusted text through a shell command. ``` 官方也提供面向 Agent 的 [deno.com/agents.md](https://deno.com/agents.md) 与 [denoland/skills](https://github.com/denoland/skills)。本站的规则更偏向任务路由,不替代官方技能。 --- # Deno Agent 架构 Source: /docs/ai/agent-loop/ Agent 不是一个特殊 runtime,而是“模型决策 → 工具执行 → 结果回传 → 停止判断”的循环。Deno 的价值在于给工具执行显式权限,并能用 Web API、npm SDK、MCP 与 Sandbox 组合边界。 ## 先选复杂度 | 需求 | 起点 | | --- | --- | | 一个模型、少量函数工具 | 供应商官方 SDK,自己写短循环 | | 工具要被多个客户端复用 | MCP Server | | 多供应商、graph、tracing、现成 integration | 评估 LangChain.js / LangGraph | | 文档索引、RAG 与数据 connector 为主 | 评估 LlamaIndex.TS | | 执行模型生成代码 | Deno Sandbox,不是普通 `Deno.Command` | 框架通过 npm 兼容层能否安装,不等于每个 integration 在 Deno 上都可用。选型前为实际用到的 loader、vector store、native dependency 和 streaming path 写 smoke test。 ## 可控循环必须有的边界 ```text 用户请求 → 模型(只看到允许的 tool schema) → 校验 tool name + arguments → 授权 / 人工确认 → 执行有超时的工具 → 返回结构化结果 → 达到停止条件或最大步数 ``` - 固定 `maxSteps`、总 deadline、token/成本预算和重试次数。 - 工具 handler 不接受任意 shell、SQL、URL 或文件路径。 - 读写分级;删除、付款、发布和生产变更必须单独授权。 - prompt injection 可能来自网页、数据库和 MCP resource;检索内容不是指令,也不是授权。 - 每步记录 tool 名、参数摘要、耗时、结果类型和错误,不记录秘密。 继续阅读:[OpenAI](/docs/ai/openai)、[MCP Server](/docs/ai/mcp-server)、[Deno Sandbox](/docs/ai/sandbox)。 --- # 可复制的 Agent 规则 Source: /docs/ai/agent-rules/ 将下面规则按仓库实际情况裁剪后放进 `AGENTS.md`: ```md ## Deno workflow - Read deno.json/deno.jsonc, deno.lock, package.json, and CI before changing commands. - Reuse existing imports and tasks. Do not invent configuration keys. - Add packages with the repository's Deno CLI workflow; keep one reviewed lockfile diff. - Grant only scoped --allow-* permissions. Never use -A merely to make a command pass. - Treat --allow-run and --allow-ffi as sandbox escape boundaries. - After changes run: deno fmt --check, deno lint, deno check, and relevant deno test targets. - Report exact commands and failures; do not claim tests you did not run. - Ask before deleting lockfiles, changing major versions, publishing, or deploying. ``` ## 仓库需要补充的事实 - Deno 固定版本和升级流程; - 权威配置文件与 workspace 根; - dev、test、check、build、deploy 的现有 task 名; - 哪些网络主机、路径和环境变量允许授权; - 是否兼容 `package.json` / `node_modules`; - 生产平台与回滚流程。 规则不能代替执行权限控制。CI、容器和部署平台仍要实施最小权限与审批。 --- # 用 Deno 构建 AI 应用 Source: /docs/ai/building-ai-apps/ ## 最小安全边界 AI 服务通常需要监听端口、读取一个供应商密钥并访问指定 API: ```bash deno run \ --allow-net=0.0.0.0:8000,api.example.com:443 \ --allow-env=MODEL_API_KEY \ --no-prompt main.ts ``` 模型名、API 路径和 SDK 方法变化很快;实现前查询对应供应商当前官方文档,不在共享代码里猜默认模型。 ## 流式代理 ```ts Deno.serve(async (request) => { const upstream = await fetch("https://api.example.com/v1/responses", { method: "POST", headers: { authorization: `Bearer ${Deno.env.get("MODEL_API_KEY")}`, "content-type": "application/json", }, body: await request.text(), signal: request.signal, }); return new Response(upstream.body, { status: upstream.status, headers: { "content-type": upstream.headers.get("content-type") ?? "text/event-stream" }, }); }); ``` 生产代码还要限制请求体、校验结构、设置超时、映射错误,并在客户端断开时取消上游请求。 ## 工具调用 - 工具名映射到固定函数,不把模型文本拼进 shell。 - 用 schema 校验参数,并在授权前显示影响范围。 - 文件工具限制到工作目录;网络工具限制 host allowlist。 - 读操作与写操作分级;删除、发布、付款和生产变更需人类确认。 - 日志记录工具名、耗时、结果类型和 request ID,但不记录密钥或完整敏感输入。 ## RAG 与评测 文档分块保留来源 URL、版本、更新时间与权限标签。检索结果是上下文,不是授权。发布前固定评测集,覆盖拒答、提示注入、超时、供应商 429/5xx 与敏感信息泄露。 官方入口:[Deno AI entrypoint](https://docs.deno.com/ai/)、[Deno LLM tutorial](https://docs.deno.com/examples/llm_tutorial/)。 --- # 用 Deno 构建 MCP Server Source: /docs/ai/mcp-server/ MCP Server 把工具、资源与 prompt 通过协议暴露给 AI 客户端。Deno 很适合运行本地 stdio server:单文件 TypeScript、明确权限、无需单独编译。 ```bash deno add npm:@modelcontextprotocol/sdk npm:zod ``` ```ts title="mcp_server.ts" import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const server = new McpServer({ name: "project-info", version: "1.0.0" }); server.registerTool( "read_project_note", { description: "Read one approved note by its short name", inputSchema: { name: z.string().regex(/^[a-z0-9-]+$/) }, }, async ({ name }) => ({ content: [{ type: "text", text: await Deno.readTextFile(`./notes/${name}.md`), }], }), ); await server.connect(new StdioServerTransport()); ``` 客户端配置只授予 notes 目录读取权限: ```json { "mcpServers": { "project-info": { "command": "deno", "args": ["run", "--allow-read=./notes", "mcp_server.ts"] } } } ``` 不要向 stdout 打印调试日志;它会破坏 JSON-RPC。日志写 stderr,并确保不包含工具参数中的秘密。 远程 server 优先使用 Streamable HTTP;旧 HTTP+SSE transport 只用于兼容。公开部署时还要实现认证、Origin/DNS rebinding 防护、速率限制与每次调用的授权判断。 官方参考:[Deno MCP server example](https://docs.deno.com/examples/mcp_server/)、[MCP TypeScript SDK server](https://ts.sdk.modelcontextprotocol.io/server)、[Model Context Protocol](https://modelcontextprotocol.io/docs/)。 --- # Deno + OpenAI Source: /docs/ai/openai/ OpenAI 官方 JavaScript SDK 可以通过 Deno 的 npm 兼容层直接使用。当前新文本生成项目优先采用 Responses API。 ```bash deno add npm:openai ``` ```ts title="main.ts" import OpenAI from "openai"; const model = Deno.env.get("OPENAI_MODEL"); if (!model) throw new Error("OPENAI_MODEL is required"); const client = new OpenAI(); // 默认读取 OPENAI_API_KEY const response = await client.responses.create({ model, instructions: "Answer accurately and say when evidence is missing.", input: "Explain Deno permissions in two sentences.", }); console.log(response.output_text); ``` ```bash OPENAI_MODEL=<已验证的模型名> \ OPENAI_API_KEY=<密钥> \ deno run --allow-env=OPENAI_MODEL,OPENAI_API_KEY \ --allow-net=api.openai.com:443 main.ts ``` ## 为什么不硬编码模型 模型可用性、价格与能力会变化。把模型作为部署配置,基于自己的质量、延迟、成本与安全评测固定值;升级时对同一评测集做对照。不要让示例代码中的临时默认值变成生产决策。 ## 服务端边界 - API key 只能留在服务端,不传给浏览器。 - 记录 request ID、模型、耗时、token 用量与错误类别,不记录密钥和完整敏感输入。 - 为外部请求设置 deadline,并在客户端断开时取消上游。 - 将模型输出视作不可信数据;渲染 HTML、执行工具或写数据库前继续校验。 - `response.output` 可能包含多种 item;只要文本时用 SDK 的 `output_text` 聚合属性。 官方参考:[OpenAI text generation](https://developers.openai.com/api/docs/guides/text)、[OpenAI JavaScript SDK](https://github.com/openai/openai-node)、[Deno AI examples](https://docs.deno.com/examples/?category=ai)。 --- # Deno Sandbox Source: /docs/ai/sandbox/ Deno Sandbox 通过 `@deno/sandbox` 创建按需 Linux microVM,适合让 AI Agent 执行代码、运行插件或构建用户项目。它与 Deno 运行时权限不是同一层:Sandbox 提供独立虚拟机和资源边界。 ## 创建并自动清理 ```ts import { Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create({ memoryMb: 2048, timeout: "10m", allowNet: ["jsr.io", "registry.npmjs.org"], labels: { workload: "agent-build" }, }); const result = await sandbox.sh`deno --version`; console.log(result.stdout); ``` 使用 `await using` 或 `finally` 保证连接清理。`close()` 是断开连接;需要提前终止 VM 时用 `kill()`,不要混淆生命周期语义。 ## 安全默认值 官方页面截至 2026-08-03 对省略 `allowNet` 的默认值存在不一致描述:创建页写“无出站网络”,安全页写“允许全部出站”。因此不要依赖默认值,始终显式传入最小 `allowNet`。默认内存约 1280 MB;内存、区域、生命周期和预发布配额都需按当前页面核对。 - 网络使用 allowlist,密钥绑定到需要访问的 host。 - 上传前检查路径,下载产物前限制大小和类型。 - 命令使用参数化 API/tagged template,不拼接用户文本进 shell。 - 设置 wall-clock timeout、输出上限、并发和磁盘预算。 - 把 sandbox ID、agent ID、用户/租户和 request ID 写入 labels 与审计日志。 - 稳定的长服务迁移为 Deploy app,不把临时 sandbox 当永久主机。 模型建议运行某段代码不等于用户授权执行。删除、发布、联网、读取客户数据和产生费用的动作仍需业务授权与审批。 官方依据:[Deno Sandbox](https://docs.deno.com/sandbox/)、[Create a sandbox](https://docs.deno.com/sandbox/create/)、[Security](https://docs.deno.com/sandbox/security/)。 --- # 编译为单文件可执行程序 Source: /docs/core/compile/ `deno compile` 把入口模块图与精简运行时 `denort` 打成一个自包含可执行文件,目标机器不需要安装 Deno。 ## 基本用法 ```bash deno compile --output=server main.ts ./server ``` 产物是一个与平台绑定的二进制文件。运行时参数必须在编译时声明,包括权限: ```bash deno compile --allow-net --allow-env=PORT --output=server main.ts ``` 编译后的二进制不接受 `--allow-*` 等运行时标志。权限范围在 `deno compile` 时确定并写入产物,事后无法收紧或放宽;按最小权限编译,不要把 `-A` 烧进要分发的文件。 ## 交叉编译 `--target` 可从任意主机平台交叉编译。当前支持五个目标三元组: | 目标 | 平台 | | --- | --- | | `x86_64-pc-windows-msvc` | Windows x86_64 | | `x86_64-apple-darwin` | macOS x86_64 | | `aarch64-apple-darwin` | macOS ARM64 | | `x86_64-unknown-linux-gnu` | Linux x86_64 | | `aarch64-unknown-linux-gnu` | Linux ARM64 | ```bash deno compile --target=aarch64-unknown-linux-gnu --output=server-linux-arm64 main.ts ``` 对应目标的 `denort` 会下载到 `DENO_DIR` 缓存。交叉产物必须在目标平台真实启动一次,不要只验证“编译通过”。 ## 嵌入资源 自 Deno 2.1 起,`--include` 可把文件或目录嵌进二进制,并通过 `import.meta` 相对路径读取: ```bash deno compile --include=./data --include=worker.ts main.ts ``` ```ts const csv = await Deno.readTextFile(import.meta.dirname + "/data/names.csv"); ``` - `--include` 可多次传入;只支持本地文件,远程模块无法嵌入。 - 被嵌入的 `.js` / `.ts` 会作为模块图根参与解析;预构建的前端产物用 `--include-as-is` 原样嵌入,避免被再次转译。 - `deno.json` 的 `compile` 块可声明 `include` / `exclude` 数组,与 CLI 标志合并。 - 默认嵌入整个已解析的 `node_modules`;实验性的 `--exclude-unused-npm` 只嵌入可达的 npm 包。 ## 与 Docker / Deploy 的边界 | 场景 | 优先方案 | | --- | --- | | 分发 CLI 工具、单文件后台程序 | `deno compile` | | 服务需要系统库、CA、shell 或多个进程协作 | [Docker 与容器](/docs/deploy/docker) | | HTTP 服务需要托管 TLS、多区域部署与自动扩缩 | [Deno Deploy](/docs/deploy/deno-deploy) | `deno compile` 解决“分发形态”,不解决进程管理、滚动升级或证书。服务长期运行仍需要 systemd、容器编排或托管平台。 ## 已知局限 - 动态导入只有字符串字面量会被静态纳入;计算出的 specifier 需要 `--include` 显式带上。 - Worker 代码默认不进产物,用 `--include` 或静态 `import "./worker.ts"` 引入。 - 原生插件(FFI、`.node` 加载项)依赖自解压模式,首次运行更慢且占用额外磁盘;按目标平台逐一验证。 - 二进制与 Deno 版本绑定,运行时升级需要重新编译分发。 官方参考:[deno compile](https://docs.deno.com/runtime/reference/cli/compile/)。 --- # 调试与编辑器配置 Source: /docs/core/debugging/ ## VS Code 扩展 安装官方扩展 `denoland.vscode-deno`,然后在命令面板执行 **Deno: Initialize Workspace Configuration**。它会在工作区的 `.vscode/settings.json` 写入: ```json { "deno.enable": true } ``` - 只在工作区级启用,不要写进用户级设置,否则每个项目都会被当作 Deno 项目。 - 启用后扩展用 Deno 语言服务器接管,并关闭 VS Code 内置的 TS/JS 诊断。 - 混合仓库用 `deno.enablePaths` 只对子目录(如 `./supabase/functions`)启用。 其他编辑器通过 LSP 接入同一个语言服务器: ```bash deno lsp ``` 遇到异常先用命令面板的 **Deno: Language Server Status** 确认当前生效的配置。 ## Inspector 断点调试 Deno 支持 V8 Inspector 协议,三个标志对应三种启动方式: | 标志 | 行为 | | --- | --- | | `--inspect` | 启动调试服务端,代码立即执行 | | `--inspect-wait` | 等待调试器连接后再执行 | | `--inspect-brk` | 等待连接并在第一行断住 | 默认监听 `127.0.0.1:9229`。用 Chromium 内核浏览器打开 `chrome://inspect`,点击目标旁的 **Inspect** 即可断点、单步,并通过 sourcemap 直接看到原始 TypeScript。 VS Code 用 attach 配置连接: ```json title=".vscode/launch.json" { "version": "0.2.0", "configurations": [ { "name": "Attach to dev server", "type": "node", "request": "attach", "port": 9229 } ] } ``` ```bash deno run --inspect-wait --allow-net main.ts ``` 暴露在可路由地址上的 Inspector 等价于远程任意代码执行。容器内调试通过端口转发或 exec 进入容器进行,不要把 `--inspect=0.0.0.0:9229` 带进生产镜像。 ## 权限与泄漏排错 权限报错方向不明时,让运行时给出触发点的栈: ```bash DENO_TRACE_PERMISSIONS=1 deno run main.ts ``` 测试报资源或异步操作泄漏时,追踪泄漏来源: ```bash deno test --trace-leaks ``` `--trace-leaks` 会拖慢测试执行,定位完就移除,不要常驻 CI。两个手段都只用于诊断,输出本身不包含修复方案。 ## 日志分层 - 开发期用 `console.log` / `console.error`,它们走 stdout/stderr,容器平台天然收集。 - 生产日志输出结构化 JSON(每行一个对象,带 `level`、`msg`、`requestId` 字段),避免正则解析自由文本。 - Deno 运行时自身的诊断(模块解析、网络、权限决策)用 `--log-level=debug` 打开,定位问题后关闭;第三方库的日志等级由库自身配置控制,该 flag 管不到。 - 日志不包含密钥与个人数据;见[环境变量与 .env 管理](/docs/core/env-variables)。 官方参考:[Debugging](https://docs.deno.com/runtime/fundamentals/debugging/)、[VS Code](https://docs.deno.com/runtime/reference/vscode/)、[deno test](https://docs.deno.com/runtime/reference/cli/test/)。 --- # 依赖、JSR 与 npm Source: /docs/core/dependencies/ ## 添加依赖 ```bash deno install jsr:@std/assert deno install npm:express ``` Deno 2.8 起,CLI 中不带前缀的包名默认按 npm 处理,所以 `deno install express` 等价于 `deno install npm:express`;代码里的 import 仍需 `npm:`,除非由 `imports` 或 `package.json` 映射。JSR 包在 CLI 仍写明 `jsr:`。 命令会更新 `deno.json` 的 `imports`,代码可使用映射后的名称: ```ts import { assertEquals } from "@std/assert"; import express from "express"; import { readFile } from "node:fs/promises"; ``` ## 项目中的四个状态来源 | 位置 | 职责 | | --- | --- | | `deno.json(c)` | 导入映射、tasks、workspace 和工具设置 | | `package.json` | npm 脚本/依赖与 Node 生态元数据,可与 Deno 共存 | | `deno.lock` | 锁定解析与完整性,应提交 | | Deno cache / `node_modules` | 本地物化结果,不作为评审事实源 | CI 使用项目已提交的锁文件,并让锁文件不一致直接失败。不要通过删除 lockfile 掩盖解析冲突。 ```bash deno ci # Deno 2.8+:要求 lockfile、清理 node_modules、frozen install deno ci --prod # 跳过 package.json devDependencies ``` ## 选择来源 - 优先使用项目已有来源与导入约定。 - TypeScript 原生库与标准库通常优先查 JSR。 - 依赖 Node 生态或只有 npm 发布时使用 npm;内置模块写明 `node:`。 - URL 导入适合明确版本的 Web 模块,但团队项目通常用 `imports` 集中管理。 - Deno 2.9 的 `deno list` 查看项目声明依赖;`deno link` / `unlink` 管理本地包链接。 排错时先运行 `deno info` 查看解析图与缓存位置,再检查代理、私有 registry 和证书配置。 官方参考:[Modules and dependencies](https://docs.deno.com/runtime/fundamentals/modules/)、[Packages](https://docs.deno.com/runtime/packages/)。 --- # Deno Desktop Source: /docs/core/desktop/ `deno desktop` 从 Deno 2.9 起可用。它把 TypeScript/JavaScript 应用、Deno 运行时和 Web 渲染后端打成每个平台的自包含桌面产物。 ## 最短流程 ```bash deno --version # 需要 2.9+ deno desktop --hmr main.ts deno desktop main.ts ``` 入口可以是小型 `Deno.serve` 应用,也可以是 Next.js、Astro、Fresh、SvelteKit 等框架项目。具体 adapter、静态资源和开发命令按框架官方指南配置。 ## 能力与边界 | 能力 | 当前含义 | | --- | --- | | 窗口 | `Deno.BrowserWindow` 管理生命周期与多窗口 | | 前后端通信 | 显式 bindings,不把任意 renderer 输入当可信代码 | | 桌面集成 | 菜单、托盘、Dock、对话框和系统通知 | | 调试 | `--hmr` 与统一 DevTools | | 分发 | 支持目标 triple 和 `--all-targets` 交叉构建 | | 更新 | `Deno.autoUpdate()` + bsdiff + 失败回滚 | ```bash deno desktop --target aarch64-apple-darwin main.ts deno desktop --all-targets main.ts ``` ## 发布检查 - 为 macOS/Windows 配置真实代码签名;ad-hoc 签名不足以消除 Gatekeeper 警告。 - 在每个目标 OS 验证通知、文件对话框、GPU、字体和自动更新。 - 自动更新清单使用 Ed25519 签名;更新服务走 HTTPS。 - 官方文档说明 Windows 当前尚不能应用已下载的 auto-update patch,发布前再次核对。 - 错误报告可能含 stack 与运行时上下文,只发送到 HTTPS,并先做数据分级。 官方依据:[Desktop apps](https://docs.deno.com/runtime/desktop/)、[Distribution](https://docs.deno.com/runtime/desktop/distribution/)、[Auto-update](https://docs.deno.com/runtime/desktop/auto_update/)。 --- # 环境变量与 .env 管理 Source: /docs/core/env-variables/ Deno 默认不加载 `.env` 文件,需要显式选择加入。读取环境变量本身也受 `--allow-env` 权限约束。 ## 加载 .env 文件 ```bash deno run --env-file main.ts # 从当前目录向上查找第一个 .env deno run --env-file=.env.local main.ts # 显式指定文件 ``` - `--env-file` 可传入多次。同一文件内重复变量以第一次出现为准;多个文件之间,后指定的文件优先。 - 不授权 `.env` 的自动加载:CI 与生产环境应依赖平台注入的真实环境变量,而不是仓库里的文件。 - 需要在代码里手动加载时,可用标准库 `@std/dotenv`,但它仍需 `--allow-read` 与 `--allow-env`。 ## 精确授权 ```bash deno run --allow-env=PORT,API_KEY main.ts ``` ```ts title="main.ts" const port = Number(Deno.env.get("PORT") ?? "8000"); if (!Number.isInteger(port)) throw new Error("PORT must be an integer"); const apiKey = Deno.env.get("API_KEY"); if (!apiKey) throw new Error("API_KEY is required"); ``` 启动时校验变量并立即失败,比请求中途遇到 `undefined` 更容易定位。只把程序真正读取的变量列入 `--allow-env`,第三方依赖无法借此偷看其余环境。 ## 常用 DENO_* 运行时变量 | 变量 | 作用 | | --- | --- | | `DENO_DIR` | 依赖与编译缓存目录 | | `DENO_NO_PACKAGE_JSON` | 关闭 `package.json` 自动解析 | | `DENO_NO_PROMPT` | 禁止权限交互提示,等价于 `--no-prompt` | | `DENO_NO_UPDATE_CHECK` | 关闭新版本检查 | | `DENO_TLS_CA_STORE` | 证书来源,`system` / `mozilla`,默认 `mozilla` | | `DENO_CERT` | 从 PEM 文件加载额外 CA,等价于 `--cert` | | `DENO_AUTH_TOKENS` | 私有模块源的 Bearer 令牌 | | `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` | 模块下载与 `fetch` 的代理设置 | | `NO_COLOR` | 关闭彩色输出 | 完整列表以官方文档为准;不要凭记忆在脚本里假设某个变量存在。 ## 配置分层 按十二要素应用的原则,把随环境变化的值全部放进环境变量: 1. 代码里只允许非敏感的默认值(如 `PORT` 的 8000)。 2. 本地开发用 `.env`,并加入 `.gitignore`。 3. 仓库里提交 `.env.example`,列出键名与格式说明,不含真实值。 4. CI 与生产由平台注入变量,不读仓库中的 `.env`。 泄露过的密钥按“已泄露”处理:轮换并清理 git 历史,而不是只删最新提交。日志脱敏优先在输出层做——不要打印整个 `Deno.env.toObject()`,错误信息里也不要拼接令牌。 ## 与 Deno Deploy 衔接 部署到 Deno Deploy 时,环境变量在控制台 app 的 **Environment Variables** 中维护,或用 `deno deploy env` 子命令管理(Classic 的 `deployctl` 已随平台关停,不要再用): ```bash deno deploy env add API_KEY "sk-..." --secret --org --app deno deploy env load .env.production --org --app ``` `env add` 的 `--secret` 让值在 dashboard 与列表输出中不可见;`env load` 批量导入 `.env` 文件。代码侧仍然只读 `Deno.env.get()`,本地 `.env` 与平台变量使用同一组键名,代码就不感知部署形态。详见 [Deno Deploy](/docs/deploy/deno-deploy)。 官方参考:[Environment variables](https://docs.deno.com/runtime/reference/env_variables/)。 --- # 模块与 Import Maps Source: /docs/core/imports/ Deno-first 代码使用标准 ESM,本地导入写真实扩展名: ```ts import { add } from "./math.ts"; import { join } from "jsr:@std/path"; import React from "npm:react"; ``` 项目中不要在每个文件散落版本号。用 `deno add` 写入 `deno.json` 的 `imports`: ```bash deno add jsr:@std/path npm:react ``` ```json { "imports": { "@std/path": "jsr:@std/path@^1.1.2", "react": "npm:react@^19.2.0", "@/": "./src/" } } ``` 然后使用稳定别名: ```ts import { join } from "@std/path"; import { loadConfig } from "@/config.ts"; ``` ## 选择顺序 1. Web 标准或 Deno 内置 API; 2. JSR 上的 TypeScript-first 包,尤其 `@std/*`; 3. npm 包,用 `npm:` 或 `deno add` 管理; 4. 仅在确有需要时直接使用 HTTPS import,并锁定来源。 计算得到的动态导入不属于静态模块图:加载本地路径需要 `--allow-read`,加载远程 URL 需要 `--allow-import`。这与普通静态 import 不同。 官方参考:[Modules](https://docs.deno.com/runtime/fundamentals/modules/)、[Import maps example](https://docs.deno.com/examples/import_maps_tutorial/)。 --- # OpenTelemetry 可观测性 Source: /docs/core/observability/ Deno 内置 OpenTelemetry 集成,可以通过 OTLP 导出运行时指标、HTTP traces、`console` 日志和应用自定义 telemetry。 ## 最短验证 ```bash OTEL_DENO=true \ OTEL_EXPORTER_OTLP_PROTOCOL=console \ deno run --allow-net main.ts ``` `console` exporter 适合确认信号是否产生;生产通常把 OTLP 发送到 Collector: ```bash OTEL_DENO=true \ OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 \ OTEL_SERVICE_NAME=orders-api \ deno run --allow-net=0.0.0.0:8000,otel-collector:4318 main.ts ``` 运行时读取 OTEL 配置与应用调用 `Deno.env.get()` 是不同路径;后者仍受 `--allow-env` 控制。无论哪种方式,都把 telemetry endpoint、headers 和 service name 纳入明确配置契约,不在日志中输出 header 值。 ## 自定义 spans ```ts import { trace } from "npm:@opentelemetry/api"; const tracer = trace.getTracer("orders"); await tracer.startActiveSpan("create-order", async (span) => { try { span.setAttribute("order.channel", "web"); // business operation } catch (error) { span.recordException(error as Error); throw error; } finally { span.end(); } }); ``` ## 生产边界 - 不把 token、Cookie、Authorization 或完整 prompt 写入 span attributes。 - 为请求、数据库和工具调用传递 trace context。 - 采样率、导出超时和队列大小必须有上限。 - 监控 exporter 自身失败,不能让 telemetry 阻塞核心请求。 - 本地、CI、预览和生产使用不同 service/environment 属性。 官方依据:[OpenTelemetry](https://docs.deno.com/runtime/fundamentals/open_telemetry/)。 --- # 权限与安全边界 Source: /docs/core/permissions/ Deno 默认禁止敏感 I/O。权限属于整个执行线程,不会为每个依赖自动建立独立沙箱。 ## 常用权限 | 能力 | 精确授权示例 | | --- | --- | | 读文件 | `--allow-read=./config,./public` | | 写文件 | `--allow-write=./data` | | 网络 | `--allow-net=api.example.com:443` | | 环境变量 | `--allow-env=PORT,API_KEY` | | 子进程 | `--allow-run=git` | | 动态导入 | `--allow-import=jsr.io` | `--deny-*` 优先于对应的 `--allow-*`,可从宽授权中排除路径或主机。非交互环境用 `--no-prompt`,让缺失权限立即失败。 ```bash deno run \ --allow-net=api.example.com:443 \ --allow-env=API_KEY \ --no-prompt main.ts ``` ## 高风险权限 - `-A` / `--allow-all` 关闭沙箱。 - `--allow-run` 启动的外部进程不受当前 Deno 权限集约束;尤其不要让受限代码启动 shell 或新的 `deno -A`。 - `--allow-ffi` 加载原生机器码,JavaScript 层权限无法约束它的系统调用。 - 初始静态模块图的加载与程序运行时 I/O 是不同边界;不要把“能 import”理解成“运行后拥有网络权限”。 ## 验证清单 1. 无权限运行一次,确认失败位置。 2. 一次只增加一个受限授权。 3. 在 CI 使用 `--no-prompt`。 4. 对工具调用、插件和用户代码使用 OS/容器级隔离,不只依赖 Deno 权限。 官方参考:[Security and permissions](https://docs.deno.com/runtime/fundamentals/security/)、[Permissions reference](https://docs.deno.com/runtime/reference/permissions/)。 --- # 运行时与 HTTP Source: /docs/core/runtime/ ## 运行文件与任务 ```bash deno run main.ts deno run --watch --allow-net main.ts deno task dev ``` `deno run` 适合直接执行入口;`deno task` 适合把参数和权限固化到 `deno.json`。对团队项目,优先让 README 和 CI 调用 task,避免每个人记一组不同参数。 ## 使用 Web 标准 API ```ts title="main.ts" const controller = new AbortController(); Deno.serve({ port: 8000, signal: controller.signal }, (request) => { const url = new URL(request.url); if (url.pathname === "/health") return Response.json({ ok: true }); return new Response("Not found", { status: 404 }); }); Deno.addSignalListener("SIGTERM", () => controller.abort()); ``` ```bash deno run --allow-net main.ts ``` 请求与响应使用标准 `Request`、`Response`、`URL`、Streams 和 `fetch`。`Deno.serve` 负责监听;它不是完整的认证、限流或部署平台。 ## 环境变量与退出 ```ts const port = Number(Deno.env.get("PORT") ?? "8000"); if (!Number.isInteger(port)) throw new Error("PORT must be an integer"); ``` 只授予需要的变量:`--allow-env=PORT`。进程退出码、未处理异常和信号处理必须在部署前用目标环境验证。 官方参考:[Run code](https://docs.deno.com/runtime/run/)、[HTTP server](https://docs.deno.com/runtime/fundamentals/http_server/)。 --- # Deno Standard Library Source: /docs/core/standard-library/ Deno Standard Library 不是运行时全局 API 的集合,而是一组发布在 JSR `@std` scope 下、独立版本化的模块。只安装项目实际使用的包: ```bash deno add jsr:@std/path jsr:@std/fs jsr:@std/assert ``` ```ts import { join } from "@std/path"; import { ensureDir } from "@std/fs"; import { assertEquals } from "@std/assert"; await ensureDir(join("data", "cache")); assertEquals(join("data", "file.txt"), "data/file.txt"); ``` ## 常用包 | 任务 | 包 | 备注 | | --- | --- | --- | | 跨平台路径 | `@std/path` | 不要手拼 `/` | | 文件系统辅助 | `@std/fs` | 仍受 Deno 文件权限控制 | | 测试断言 | `@std/assert` | 与 `Deno.test` 配合 | | 日期时间 | `@std/datetime` | JSR 标注 UNSTABLE 且未随 std 1.0 稳定化;优先 Web `Temporal` / `Intl` | | UUID | Web `crypto.randomUUID()` | 通常不需要额外包 | | HTTP | `Deno.serve` | 路由、session 等再选框架 | 标准库包没有第三方依赖,并尽量跨 Deno、Node、Bun、Workers 和浏览器工作;真正可移植范围仍取决于底层平台 API。 官方参考:[Deno Standard Library](https://docs.deno.com/runtime/reference/std/)、[JSR @std](https://jsr.io/@std)。 --- # 供应链安全 Source: /docs/core/supply-chain/ 依赖安全不是一次扫描,而是从解析、安装、脚本执行到升级发布的连续控制。 ## 可复现安装 Deno 2.8 起,`deno ci` 提供类似 `npm ci` 的严格安装:要求已有 `deno.lock`、清理旧 `node_modules`,并按 frozen lockfile 安装。 ```bash deno ci deno ci --prod deno test ``` 日常开发用 `deno install`;CI 和生产构建用 `deno ci`。锁文件过期时在开发分支显式更新并评审 diff,不在 CI 自动修复。 ## 漏洞与生命周期脚本 ```bash deno audit deno audit --socket deno audit --fix deno approve-scripts ``` `deno audit --fix` 会修改 manifest 并重建锁文件,应作为需要代码评审的升级操作。Deno 默认不执行 npm `preinstall` / `postinstall`;确有需要时只批准具体包: ```bash deno install --allow-scripts=npm:better-sqlite3 ``` ## 最小依赖年龄 Deno 2.9 默认跳过发布不足 24 小时的 npm 版本。项目可以把窗口调大: ```json title="deno.json" { "minimumDependencyAge": "P3D" } ``` ```ini title=".npmrc" min-release-age=3 trust-policy=no-downgrade ``` `trust-policy=no-downgrade` 用于阻止已锁定包从可信发布方式静默降级到更弱方式;当前默认关闭,应先评估依赖生态的 provenance 覆盖。 ## Lockfile 与 vendor 锁文件固定版本和完整性,但远程源消失时仍无法离线构建;`vendor: true` 可以把源码物化进仓库。高保障环境同时提交 `deno.lock` 与 `vendor/`,CI 再使用 frozen 模式。 官方依据:[Supply chain management](https://docs.deno.com/runtime/packages/supply_chain/)、[`deno ci`](https://docs.deno.com/runtime/reference/cli/ci/)、[`deno audit`](https://docs.deno.com/runtime/reference/cli/audit/)。 --- # 测试、Mock 与覆盖率 Source: /docs/core/testing/ ## 最小测试 ```ts title="user_test.ts" import { assertEquals } from "jsr:@std/assert"; Deno.test("normalizes a user name", () => { assertEquals(" Ada ".trim(), "Ada"); }); ``` ```bash deno test deno test --filter "user" deno test --watch ``` Deno 2.9 的 test context 内置 snapshot,无需额外导入: ```ts Deno.test("renders a card", async (t) => { await t.assertSnapshot(renderCard({ title: "Deno" })); }); ``` 用 `deno test --update-snapshots` 创建或更新,并评审 `__snapshots__/*.snap` diff;CI 只运行普通 `deno test`,绝不自动更新预期值。 测试默认也受权限沙箱约束。只给需要网络或临时目录的测试相应权限;不要因为一条集成测试使用 `-A` 运行整个套件。 ## 异步与资源清理 ```ts Deno.test("fetches health", async () => { const controller = new AbortController(); try { // start resource, assert behavior } finally { controller.abort(); } }); ``` Deno 的 sanitizer 可以帮助发现泄漏的异步操作和资源。注意默认状态:自 Deno 2.8 起,只有 exit sanitizer 默认开启;op 和 resource sanitizer 是 opt-in。需要泄漏检测时在单个测试或测试步骤上显式打开,而不是依赖默认值: ```ts Deno.test({ name: "closes every handle", sanitizeOps: true, sanitizeResources: true, fn: async () => { // ... }, }); ``` 反之,如果某个测试确有意保留后台操作,在该测试上显式关闭对应 sanitizer 并说明原因,不要全局关闭。 ## 覆盖率 ```bash deno test --coverage=coverage deno coverage --lcov --output=coverage.lcov coverage/ ``` 覆盖率不能替代关键失败路径、权限拒绝、超时和取消测试。CI 至少运行 `fmt --check`、`lint`、`check` 和 `test`。 ## 大型套件 ```bash deno test --changed=origin/main deno test --shard=1/3 deno test --retry=2 deno test --repeats=3 deno test --trace-leaks ``` `retry` 用于已知偶发失败,成功一次即可通过;`repeats` 要求每次都通过,用来主动发现不稳定性。不要用 retry 长期掩盖确定性缺陷。 官方参考:[Testing](https://docs.deno.com/runtime/test/)、[Coverage](https://docs.deno.com/runtime/test/coverage/)、[Mocking](https://docs.deno.com/runtime/test/mocking/)。 --- # 内置开发工具 Source: /docs/core/tooling/ ## 常用命令 | 目标 | 命令 | | --- | --- | | 格式化 / 检查格式 | `deno fmt` / `deno fmt --check` | | 静态规则检查 | `deno lint` | | 类型检查模块图 | `deno check main.ts` | | 生成 API 文档 | `deno doc --html --name=my-lib mod.ts` | | 基准测试 | `deno bench` | | 编译可执行文件 | `deno compile --allow-net main.ts` | | 实验性打包模块图 | `deno bundle main.ts -o bundle.js` | `deno compile` 会把入口与运行时打成可执行文件,但权限仍需在编译时或运行时按官方规则声明。交叉编译、动态资源和原生依赖需要单独验证。当前官方文档把 `deno bundle` 标记为实验性;构建前端应用时优先遵循框架或 Vite 的构建流程。 ## 推荐任务 ```json title="deno.json" { "tasks": { "dev": "deno run --watch --allow-net main.ts", "check": "deno fmt --check && deno lint && deno check **/*.ts && deno test" } } ``` 不要把 Deno formatter 与另一套 formatter 同时作用于同一批文件。迁移仓库时先确定唯一权威工具,再做机械格式化提交。 官方参考:[Lint and format](https://docs.deno.com/runtime/lint_and_format/)、[Bundling](https://docs.deno.com/runtime/reference/bundling/)。 --- # 内置 TypeScript Source: /docs/core/typescript/ Deno 直接执行 `.ts` 与 `.tsx` 文件,类型检查器也随二进制分发。你不需要安装 `typescript`、`ts-node` 或 `tsx` 才能启动项目。 ```bash deno run main.ts # 执行 deno check main.ts # 静态类型检查 deno test --check # 测试并检查测试模块图 ``` 当前工作流应显式执行 `deno check`。不要把 `deno run` 成功当作完整类型验证,也不要等到生产启动时才发现类型问题。 ## 配置原则 Deno-first 项目通常把少量 TypeScript 设置写在 `deno.json`: ```json { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true } } ``` 既有 Node 项目的 `tsconfig.json` 可以继续被读取;如果同时存在带 `compilerOptions` 的 `deno.json`,后者优先。Deno 不负责输出 JavaScript,因此 emit 相关选项没有意义。 ## Web、Worker 与 DOM 类型 默认类型环境面向 Deno runtime,不自动包含浏览器 `document`。共享浏览器代码时显式配置 `lib`,而不是用全局声明掩盖环境差异。 官方参考:[TypeScript support](https://docs.deno.com/runtime/fundamentals/typescript/)、[Configuring TypeScript](https://docs.deno.com/runtime/reference/ts_config_migration/)。 --- # Workspaces 与 Monorepo Source: /docs/core/workspaces/ ## 最小结构 ```json title="deno.json" { "workspace": ["./apps/api", "./packages/core"], "tasks": { "check": "deno task --recursive check" } } ``` 成员可以拥有自己的 `deno.json` 与任务。工作区让成员共享锁文件和根级配置,同时保留包边界。 ```text repo/ ├── deno.json ├── deno.lock ├── apps/api/deno.json └── packages/core/deno.json ``` ## 设计原则 - 根任务只做编排,成员任务负责本包验证。 - 内部包通过 workspace 解析,不用发布版本绕一圈。 - 公开包明确 `exports`、许可证和发布文件。 - 不在根配置里放所有成员都不需要的宽权限。 ## 验证 ```bash deno task --recursive check deno test ``` 从 workspace 根运行 `deno test` 会发现成员测试;`deno task --recursive` 用于在成员中执行同名 task。过滤参数与 npm workspace 混用行为会随版本演进,复杂任务图先运行 `deno help task` 并核对当前官方文档。 官方参考:[Workspaces](https://docs.deno.com/runtime/fundamentals/workspaces/)。 --- # Deno 数据库选型 Source: /docs/database/ Deno 能通过 npm 兼容层使用成熟数据库驱动,也能使用 Web/JSR 生态模块。选择依据应是数据模型、事务和部署环境,而不是“哪个包最像 Deno”。 | 数据层 | 适合 | Deno 接入 | | --- | --- | --- | | PostgreSQL | 关系数据、事务、复杂查询 | `npm:postgres`、`npm:pg`、Drizzle、Kysely | | [SQLite](/docs/database/sqlite) | 单机、嵌入式、测试、轻服务 | `node:sqlite` 或兼容库;核对运行版本 | | MongoDB | 文档模型、既有 Mongo 体系 | 官方 npm driver / Mongoose | | Redis | cache、rate limit、短期状态 | 官方 npm client 或兼容服务 | | [Supabase](/docs/database/supabase) | 托管 Postgres + Auth/Storage | 官方 npm SDK 或直接 Postgres | | [Deno KV](/docs/database/kv) | 简单 key-value、原子操作、Deno 平台集成 | `Deno.openKv()`;先核对部署平台支持 | ## 通用连接边界 ```bash deno run \ --allow-env=DATABASE_URL \ --allow-net=db.example.com:5432 \ src/main.ts ``` - 在模块或应用生命周期内复用连接池,不要每次请求新建。 - 密钥只来自环境或 secret store,不进入源码、日志和错误响应。 - migration 是发布步骤;应用启动时自动改 schema 会放大并发风险。 - serverless 实例会并发扩容,连接池上限要结合实例数计算。 - 测试至少覆盖连接失败、事务回滚、唯一约束与超时。 新 Deno Deploy 提供 PostgreSQL 关联、timeline 隔离和 Deno KV,但不同 timeline 的数据与迁移策略必须单独设计。详见 [Deploy 数据与 Cron](/docs/deploy/data-and-cron)。 官方参考:[Connecting to databases](https://docs.deno.com/examples/connecting_to_databases_tutorial/)、[Deno database examples](https://docs.deno.com/examples/?category=databases)。 --- # Deno KV Source: /docs/database/kv/ Deno KV 是内置于运行时的 key-value 存储,本地通过 `Deno.openKv()` 使用,在新 Deno Deploy 上作为可关联的数据库引擎提供。截至 2026-08-03,KV 官方仍标注为开发中、API 可能变化,本地运行需要 `--unstable-kv`。 ```ts const kv = await Deno.openKv(); await kv.set(["sessions", sid], { userId, createdAt: new Date() }); const entry = await kv.get(["sessions", sid]); const result = await kv.atomic() .check(entry) // versionstamp 未变才提交 .set(["sessions", sid], { userId, rotatedAt: new Date() }) .commit(); ``` 本地开发:`Deno.openKv("./data/app.kv")` 持久化到文件,`Deno.openKv(":memory:")` 用于测试。 ```bash # KV 的底层磁盘访问不需要 --allow-read/--allow-write(官方权限文档明确列出), # 只为应用自身的其他 I/O 授权 deno run --unstable-kv main.ts ``` Deploy Classic 已于 2026-07-20 关停。新 Deno Deploy 支持将 Deno KV 作为数据库引擎关联到 app,`Deno.openKv()` 自动连接当前 timeline 的隔离逻辑数据库;但官方迁移指南明确:Classic 的 KV 数据不会自动迁移,需联系 support@deno.com 协助;KV Queues(`kv.enqueue()` / `kv.listenQueue()`)在新平台不支持;单个 app 目前只能关联一个数据库实例,不能同时挂 KV 和 PostgreSQL。依赖这些能力的架构要先改设计再迁移。 ## API 要点 - key 是数组:`["users", userId]`。组成部分可以是 string、number、boolean、bigint、Uint8Array,按字典序排序,类型序优先于值序。 - value 必须兼容 structured clone(对象、数组、Map、Set、Date、Uint8Array 等);类实例、函数、Symbol 不支持。 - `get` / `getMany` / `list`:读取。`list` 按 prefix 或 range 分批返回,默认每批 500,批间不保证同一快照。 - `set` / `delete` 与 `sum` / `min` / `max`(后者仅 atomic 内、仅 `Deno.KvU64`)。 - `atomic()`:乐观并发控制,`.check()` 断言 versionstamp,`.commit()` 失败时重读重试;不使用锁式交互事务。 - `watch(keys)`:返回 `ReadableStream`,key 变更时推送;快速连续变更可能被合并,不保证看到每个中间状态。 ## 一致性模型 - 写入始终强一致。 - 读取可选 strong(保证返回最近写入值)或 eventual(更快,可能返回旧值);所有一致性级别下 `get` 都是快照读。 - 每次写入获得单调递增、非顺序的 12 字节 versionstamp,同一事务内的写入共享同一个。 主要限制(官方事务文档):key 最大 2 KiB,value 最大 64 KiB;`getMany` 最多 10 个 key;`list` 单批最多 1000;单个 atomic 最多 100 个 check、1000 个 mutation、总大小 800 KiB;`watch` 最多 10 个 key。 ## 新 Deno Deploy 上的 KV - KV 通过组织 dashboard 的 databases 功能开通并指派给 app;生产、分支、preview timeline 自动获得隔离的逻辑数据库。 - 从 Deploy 之外访问(如本地连远端库):连接 URL 为 `https://api.deno.com/v2/databases//connect`,用 `DENO_KV_ACCESS_TOKEN` 环境变量放个人或组织 token。 - 数据驻留:官方注明 KV 主区域为美国 Northern Virginia,在欧洲和亚洲有只读副本,写入会经美国存储与传输,因此需要严格欧盟数据驻留的场景不适用,官方建议改用其 Postgres 方案。 - 当前限制:每个 app 只能关联一个数据库实例;preview 数据库由所有 preview deployment 共用;database explorer 暂不支持 KV。 ## 何时不该用 KV - 需要队列:新平台不支持 `enqueue` / `listenQueue`,改用外部队列或以数据库实现任务表。 - 需要关系查询、join、临时 SQL 分析:KV 只有 key 查找与前缀扫描,二级索引要自己维护。 - 单值超过 64 KiB,或单事务超过官方 atomic 限制。 - 严格欧盟数据驻留合规。 - 同一 app 同时需要 KV 和 PostgreSQL(当前平台限制)。 - 要求 API 长期冻结:KV 官方仍标注为可能变化。 官方参考:[Deno KV 概览](https://docs.deno.com/deploy/kv/)、[KV 操作与一致性](https://docs.deno.com/deploy/kv/operations/)、[KV 事务与限制](https://docs.deno.com/deploy/kv/transactions/)、[Key space 与值类型](https://docs.deno.com/deploy/kv/key_space/)、[新 Deploy 的 Deno KV](https://docs.deno.com/deploy/reference/deno_kv/)、[Classic 迁移指南](https://docs.deno.com/deploy/migration_guide/)。 --- # PostgreSQL 与 Drizzle CRUD Source: /docs/database/postgres-drizzle/ 简单查询可以直接使用 `postgres`;当项目需要类型化 schema 与 migration 时再引入 Drizzle。 ```bash deno install npm:drizzle-orm npm:drizzle-kit npm:postgres ``` ## Schema ```ts title="src/db/schema.ts" import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; export const posts = pgTable("posts", { id: integer().primaryKey().generatedAlwaysAsIdentity(), title: text().notNull(), body: text().notNull(), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }); ``` ## 连接与查询 ```ts title="src/db/client.ts" import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema.ts"; const url = Deno.env.get("DATABASE_URL"); if (!url) throw new Error("DATABASE_URL is required"); const client = postgres(url, { max: 5 }); export const db = drizzle(client, { schema }); export const closeDb = () => client.end(); ``` ```ts import { eq } from "drizzle-orm"; import { db } from "./client.ts"; import { posts } from "./schema.ts"; const [created] = await db.insert(posts) .values({ title: "Hello Deno", body: "First post" }) .returning(); const result = await db.select().from(posts).where(eq(posts.id, created.id)); ``` ## Migration 与权限 `drizzle-kit` 这类 Node 工具可能需要本地 `node_modules`。把 generate/migrate 放到独立 task 或 CI job,审核 SQL 后再应用,不要给线上 HTTP 进程 schema 修改权限。 ```bash deno run --allow-env=DATABASE_URL --allow-net=db.example.com:5432 src/script.ts ``` 官方参考:[Deno Drizzle tutorial](https://docs.deno.com/examples/drizzle_tutorial/)、[Postgres example](https://docs.deno.com/examples/postgres/)、[Drizzle PostgreSQL](https://orm.drizzle.team/docs/get-started-postgresql)。 --- # SQLite 与 node:sqlite Source: /docs/database/sqlite/ Deno 自 v2.2 起在 Node 兼容层中提供 `node:sqlite`,官方 Node API 文档将其列为完整支持模块。上游 Node.js 中该模块加入于 v22.5.0,截至 2026-08-03 上游稳定性标注为 1.2(release candidate),正式稳定前仍可能有小幅 API 调整,升级 Deno 时应复查。 ```ts title="src/db.ts" import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync("./data/app.db"); db.exec(` PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY, title TEXT NOT NULL ) STRICT; `); const insert = db.prepare("INSERT INTO posts (title) VALUES (?)"); insert.run("Hello Deno"); const rows = db.prepare("SELECT id, title FROM posts").all(); ``` `DatabaseSync` 的 API 全部同步执行,适合脚本、CLI 和单写者服务;不要把同步长查询放进高并发请求路径。 ## 备选库 | 库 | 形态 | 注意 | | --- | --- | --- | | `node:sqlite` | 内置 Node 兼容模块 | 无需安装依赖;上游仍是 release candidate | | `jsr:@db/sqlite` | FFI 加载预编译原生库 | 官方 README 要求 `--allow-ffi`、`--allow-env`,且需要网络与文件权限下载缓存原生库 | | `npm:better-sqlite3` | Node 原生 addon | 依赖 Deno 对原生 addon 的支持,使用前按所用 Deno 版本核对 | FFI 与原生 addon 加载机器码,JavaScript 层权限无法约束其系统调用;选型时把这一点计入信任边界。纯 WASM 方案(如 `npm:sql.js`)不触碰 FFI,但数据库在内存中、持久化要自己处理。 ## 何时选 SQLite - 单机服务、CLI 工具、桌面/边缘单实例应用。 - 测试与本地开发:用 `:memory:` 或临时文件隔离每个测试。 - 读多写少的嵌入式场景,配合 WAL 让读写并发。 - 多区域/边缘副本属于 LiteFS、libsql 一类方案的领域,超出 `node:sqlite` 范围,选型时单独评估。 ## 最小权限 ```bash deno run --allow-read=./data --allow-write=./data src/main.ts ``` - WAL 模式会创建 `app.db-wal` 与 `app.db-shm` 旁车文件,只授权精确到 `app.db` 会在 checkpoint 或首次写入时失败;授权目录,或把三个文件全部列出。 - 只读工具用 `new DatabaseSync(path, { readOnly: true })` 配合 `--allow-read`,不授写权限。 - `jsr:@db/sqlite` 等 FFI 方案本质上需要接近 `-A` 的信任级别,不要用权限收窄自我安慰。 ## WAL、备份与并发写边界 - SQLite 是单写者:WAL 下读写可并发,写与写仍互斥;写事务要短,`busy_timeout` 决定锁竞争时等待多久,超时抛 `SQLITE_BUSY`。 - 多进程打开同一文件可行,但写吞吐不会因此提高;网络文件系统(NFS 等)上不要使用 SQLite。 - 备份前先做 `PRAGMA wal_checkpoint(TRUNCATE)`,否则直接拷贝主文件会丢失仍在 WAL 中的提交;或使用 SQLite 官方 CLI 的 `.backup`。 - 上游 `node:sqlite` 自 Node v23.8.0 / v22.16.0 起提供 `sqlite.backup()`,Deno 各版本的支持情况以所用版本实测为准。 - migration 与 PostgreSQL 同理:作为发布步骤执行,应用启动时自动改 schema 在多实例下会放大并发风险。 官方参考:[Deno Node API 支持列表](https://docs.deno.com/runtime/reference/node_apis/)、[Node.js node:sqlite 文档](https://nodejs.org/api/sqlite.html)、[jsr:@db/sqlite](https://jsr.io/@db/sqlite)、[SQLite WAL 模式](https://sqlite.org/wal.html)。 --- # Supabase Source: /docs/database/supabase/ Supabase 的两条接入路径:把它当托管 Postgres 直连(配合 Drizzle/postgres.js,见 [PostgreSQL 与 Drizzle](/docs/database/postgres-drizzle)),或用官方 SDK `@supabase/supabase-js` 走 PostgREST/Auth/Storage API。SDK 同时发布在 npm 和 JSR(`jsr:@supabase/supabase-js`)上。 ```ts title="src/supabase.ts" import { createClient } from "jsr:@supabase/supabase-js@2"; const url = Deno.env.get("SUPABASE_URL"); const key = Deno.env.get("SUPABASE_PUBLISHABLE_KEY"); if (!url || !key) throw new Error("SUPABASE_URL / SUPABASE_PUBLISHABLE_KEY required"); export const supabase = createClient(url, key); ``` ```bash deno run \ --allow-env=SUPABASE_URL,SUPABASE_PUBLISHABLE_KEY \ --allow-net=.supabase.co \ src/main.ts ``` ## 直连 Postgres 与连接池 Supabase 提供多种连接方式,端口与适用场景(官方连接文档): | 方式 | 主机:端口 | 适用 | | --- | --- | --- | | Direct connection | `db..supabase.co:5432` | 常驻服务、migration、`pg_dump`;IPv6,IPv4 需付费 add-on | | Supavisor session 模式 | `aws-.pooler.supabase.com:5432` | IPv4-only 网络的直连替代 | | Supavisor transaction 模式 | `aws-.pooler.supabase.com:6543` | serverless / 边缘函数等大量短连接 | - Deno Deploy 一类无常驻进程的环境用 transaction 模式(6543);官方明确 transaction 模式不支持 prepared statements,客户端要禁用(`postgres.js`:`postgres(url, { prepare: false })`)。 - serverless 并发扩容时按最大实例数核算总连接数,池上限别按单实例拍。 - migration 用 direct connection 或 session 模式,走独立发布步骤,不给线上 HTTP 进程 schema 修改权限。 ```bash deno run \ --allow-env=DATABASE_URL \ --allow-net=aws-.pooler.supabase.com:6543 \ src/main.ts ``` ## Auth 与 RLS 边界 - SDK 默认按调用者身份执行:新 publishable key(对应 legacy anon JWT key,两者是可并存的不同密钥类型而非简单改名)+ 用户 JWT 时,PostgREST 以该用户角色访问,RLS 策略生效。 - 官方 RLS 文档要求暴露 schema(默认 `public`)中的表必须启用 RLS;启用后没有策略则 publishable key 下 API 不可读任何数据。Table Editor 建表默认开启,手写 SQL 建表要自己 `enable row level security`。 - 新 secret key(对应 legacy service_role JWT key)会绕过 RLS,官方明确禁止出现在浏览器或交付给客户的代码里;只在服务端、从环境变量注入、不进日志。 - Deno 后端如果只做受信服务间调用,直连 Postgres 往往比 service key + PostgREST 更直观;需要按终端用户鉴权时才走 SDK + RLS。 ## Edge Functions 与本地 Deno 的差异 Supabase Edge Functions 运行在 Supabase Edge Runtime 上——官方称其为 Deno 兼容、TypeScript 优先的运行时(开源,github.com/supabase/edge-runtime)。与本地 Deno 的实际差异: - 依赖导入支持 `npm:`、`jsr:` 和 Node 内置模块;官方推荐每个函数配自己的 `deno.json`,部署时不要共享 `/supabase/functions` 下的全局配置。 - 本地用 `supabase functions serve` 获得与生产接近的运行时,部署用 `supabase functions deploy`。 - 函数面向短生命周期、幂等操作设计,可能有冷启动;长任务交给后台 worker,不要塞进函数。 - 它是 Deno 兼容运行时而非 Deno 本体,不要假设与本地 `deno` 版本特性完全对齐;用到新 API 前在 Edge Runtime 仓库核对。 ## 密钥管理 - 只需要 publishable 级别的权限就不要引入 secret key;两者用不同环境变量名分开,避免误用。 - secret 只从环境或 secret store 读取,不进源码、日志和错误响应;轮换时按 Supabase dashboard 的密钥滚动流程走。 - `--allow-env` 精确到变量名,`--allow-net` 精确到项目主机;CI 加 `--no-prompt`。 官方参考:[Connecting to your database](https://supabase.com/docs/guides/database/connecting-to-postgres)、[Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security)、[Edge Functions](https://supabase.com/docs/guides/functions)、[Function dependencies](https://supabase.com/docs/guides/functions/dependencies)、[Deno 官方 Supabase 示例](https://docs.deno.com/examples/supabase/)。 --- # 部署决策 Source: /docs/deploy/ ## 先选运行模型 | 目标 | 优先考虑 | 必须核对 | | --- | --- | --- | | Git 驱动、多区域(US/EU)运行、托管数据库 | Deno Deploy | 当前区域、配额、构建与运行时 API | | 自定义系统包、Kubernetes、可移植镜像 | Docker | 镜像变体、信号、文件权限、健康检查 | | 已有 VM/PaaS | 原生二进制或容器 | 平台是否安装 Deno、持久卷与网络策略 | | 单文件 CLI | `deno compile` | 目标架构、动态资源、权限与原生依赖 | ## 平台无关契约 应用至少应明确: - 入口和启动命令; - 监听地址与 `PORT` 行为; - 所需 Deno 权限; - 环境变量名,不在镜像或日志中写密钥; - `/health` 或平台等效健康信号; - SIGTERM、超时、重试与幂等边界; - 锁文件、Deno 版本与回滚产物。 平台可以用 Deno 安装和构建,却在另一种隔离环境执行请求。部署前同时核对构建命令、请求运行时、Web/Node API 支持与持久化语义。 下一步:[Deno Deploy](/docs/deploy/deno-deploy)、[数据与 Cron](/docs/deploy/data-and-cron)、[Classic 迁移](/docs/deploy/classic-migration)、[Docker](/docs/deploy/docker) 或 [生产基线](/docs/deploy/production-baseline)。 --- # CI/CD 实战 Source: /docs/deploy/ci-cd/ 前提:仓库已提交 `deno.json` 与 `deno.lock`,本地门禁已通过。本页只讲流水线编排;门禁本身与安全基线见[生产工程基线](/docs/deploy/production-baseline)。 ## 安装与固定 Deno GitHub Actions 使用官方 action,并固定 major 版本: ```yaml - uses: denoland/setup-deno@v2 with: deno-version: v2.x ``` - `deno-version` 接受 `v2.x`、`v2.1.x`、精确版本或 `lts`;追求严格可复现时固定到精确版本,并把升级作为独立变更评审。 - 也可用 `deno-version-file` 从 `.tool-versions` 等文件读取,保证 CI 与本地一致。 ## 缓存依赖 `setup-deno` 内置缓存,无需手写 `actions/cache`: ```yaml - uses: denoland/setup-deno@v2 with: deno-version: v2.x cache: true ``` `cache: true` 缓存 Deno 下载的依赖(即 `DENO_DIR` 内容),缓存键由 job id、runner OS/arch 和 `deno.lock` 哈希组成;需要自定义哈希时用 `cache-hash`(设置它即隐含开启缓存)。若 workflow 自行设置了 `DENO_DIR` 环境变量,保证 action 与后续步骤使用同一目录。 ## 安装依赖:deno ci ```bash deno ci ``` `deno ci`(Deno 2.8+)是 CI 与 Dockerfile 的可复现安装命令:`deno.lock` 缺失时报错,删除已有 `node_modules`,并以 frozen 语义安装——lockfile 必须与配置文件精确匹配,任何漂移都会失败而不是静默更新。构建生产产物时加 `--prod` 跳过 devDependencies;要连 `@types/*` 一起排除需另加 `--skip-types`——它按包名启发式判断,可能误跳过附带运行时代码的包,用前核对产物是否仍完整。 ## 门禁顺序 按"廉价且快失败在前"排序: ```bash deno ci deno fmt --check deno lint deno check "**/*.ts" "**/*.tsx" deno test ``` 格式与 lint 秒级完成,先拦住机械性问题;类型检查再拦住接口错误;测试最贵,放最后。不要把门禁合并成一条命令,分开才能在 CI 日志里一眼定位失败层级。glob 要加引号交给 Deno 展开(裸 `**/*.ts` 由 shell 展开,各 runner 行为不一,而且漏掉 `.tsx`);Fresh 等有自带 `check` task 的项目直接跑 `deno task check`。 ## 跨 OS/arch 矩阵 库与 CLI 至少跑三个系统: ```yaml strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} ``` Windows 注意 CRLF:在 checkout 前设置 `git config --system core.autocrlf false`,避免 `deno fmt --check` 因换行符误报。可用 `continue-on-error` 加一个 canary Deno 版本任务,提前发现上游变更,但不阻塞合并。覆盖率报告等只需一次的步骤用 `if: matrix.os == 'ubuntu-latest'` 限定。 ## 构建与发布产物 - 静态站点:`deno task build` 生成产物目录,用 `actions/upload-artifact` 传递或直接交给部署步骤。 - 单文件二进制:`deno compile --target ` 交叉编译各平台产物,随 release 上传。编译参数以当前 `deno help compile` 为准。 - 发布 JSR 包:不要在每个 push 上发布。按 tag 触发,用 OIDC 发布以获得 provenance,完整配置见[发布 JSR 包](/docs/reference/publishing-jsr)。 ## 部署到 Deno Deploy 两条路径,按团队习惯选一条: 1. **内置 GitHub 集成(默认路径)**:在 Deno Deploy 控制台把 app 关联到 GitHub 仓库,推送即触发构建,不需要自己维护部署 YAML。 2. **外部 CI 部署**:需要自定义流水线(例如先跑完整矩阵再发布)时,用 `deno deploy` CLI: ```bash deno deploy --org --app --prod ``` CLI 在 CI 中的认证方式是组织 token:创建 organization token,存入 GitHub 仓库 secret,通过 `DENO_DEPLOY_TOKEN` 环境变量传入。注意 Deno Deploy 文档中的 OIDC 页面讲的是运行中的应用向第三方服务(AWS、Vault 等)认证,不是 CLI 部署的认证方式——不要把两者混淆。Deploy Classic 已被官方宣布于 2026-07-20 停用,不要在新项目中使用 deployctl。 ## 完整示例 ```yaml title=".github/workflows/ci.yml" name: ci on: push: branches: [main] pull_request: permissions: contents: read jobs: test: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} # Windows runner 上先关掉 CRLF 转换,否则 fmt --check 会误报 steps: - run: | git config --system core.autocrlf false git config --system core.eol lf - uses: actions/checkout@v7 - uses: denoland/setup-deno@v2 with: deno-version: v2.x cache: true - run: deno ci - run: deno fmt --check - run: deno lint - run: deno check "**/*.ts" "**/*.tsx" - run: deno test deploy: needs: test if: github.ref == 'refs/heads/main' && github.event_name == 'push' runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v7 - uses: denoland/setup-deno@v2 with: deno-version: v2.x - run: deno ci --prod - run: deno task build # 需要构建步骤的项目 - run: deno deploy --org my-org --app my-app --prod env: DENO_DEPLOY_TOKEN: ${{ secrets.DENO_DEPLOY_TOKEN }} ``` 要点:`environment: production` 配合 GitHub 环境保护规则做人工审批;token 只授予目标组织;deploy job 与测试矩阵使用同一 Deno major 版本。 `deno deploy --prod` 与 `deno publish` 都会立刻影响外部用户。Agent 与自动化脚本不应绕过 environment 审批直接触发生产部署。 官方参考:[Continuous integration](https://docs.deno.com/runtime/reference/continuous_integration/)、[setup-deno](https://github.com/denoland/setup-deno)、[Deno 2.8 发布说明(deno ci)](https://deno.com/blog/v2.8)、[deno deploy CLI 参考](https://docs.deno.com/runtime/reference/cli/deploy/)、[Deno Deploy changelog](https://docs.deno.com/deploy/changelog/)。 --- # 从 Deploy Classic 迁移 Source: /docs/deploy/classic-migration/ Deno 官方把 Deploy Classic 与 Subhosting v1 的关停日期标为 **2026-07-20**。该日期已经过去;仍有 Classic 资源时,不要假设项目已自动迁移,立即在 `console.deno.com` 与当前官方状态中确认,并按下面清单处理。 ## 不是原地升级 - Classic 项目不会自动转成新 app;新平台需要 organization。 - `deployctl` 迁移为内置 `deno deploy`。 - GitHub integration 使用新平台集成构建,不再沿用旧 Action 配置假设。 - Classic 一套环境变量要拆成 Production、Development 与 Build contexts。 - 自定义域名需要重新配置并保留 DNS 传播窗口。 ## 代码与数据差异 ```diff - import { serve } from "https://deno.land/std/http/server.ts"; - serve(() => new Response("hello")); + Deno.serve(() => new Response("hello")); ``` - 旧 std `serve()` 在新 Deploy warmup 可能超时;升级依赖并使用 `Deno.serve()`。 - `Deno.cron()` 可以继续使用,但要复核 UTC、重试与 timeline 行为。 - Classic KV 数据不会自动迁移,需要联系官方支持并验证数据完整性。 - 新 Deploy 当前不支持 `Deno.Kv.enqueue()` / `listenQueue()`;改用外部消息队列或数据库 job queue。 - Subhosting v1 的 project/deployment 模型迁移为 v2 的 app/revision,并需要新的 API/SDK 映射。 ## 切流清单 1. 盘点项目、域名、变量、cron、KV、queues、region 和 Subhosting 调用。 2. 在新 organization 创建 app,用非生产 timeline 验证构建与运行。 3. 迁移 secret/context,验证不泄漏。 4. 迁移数据库与后台任务,做数量、hash 或业务级对账。 5. 配置域名证书挑战和新 DNS,保留回滚窗口。 6. 验证 logs、traces、metrics、告警与成本,再停旧资源。 官方依据:[Migration guide](https://docs.deno.com/deploy/migration_guide/)、[About Deno Deploy](https://docs.deno.com/deploy/)。 --- # 数据库、Cron 与 Timelines Source: /docs/deploy/data-and-cron/ 新 Deno Deploy 的核心抽象是 app、revision、timeline 与 context。生产、Git 分支和 preview 可以运行不同 revision,并取得不同环境变量和逻辑数据库。 ## 数据库 平台当前支持关联 PostgreSQL 或 Deno KV。每个 app 的生产、分支和 preview timeline 默认获得隔离的逻辑数据库。 ```ts const kv = await Deno.openKv(); Deno.serve(async () => { await kv.set(["health", "lastSeen"], new Date().toISOString()); return Response.json({ ok: true }); }); ``` PostgreSQL 连接会注入标准 `DATABASE_URL`、`PGHOST`、`PGPORT`、`PGDATABASE`、`PGUSER`、`PGPASSWORD`。迁移命令可以配置为 pre-deploy command,在 revision 接流量前运行。 官方文档截至 2026-08-03 表示:单个 app 暂不能同时关联多个数据库实例,因此不能同时关联一套 Deno KV 和一套 PostgreSQL 实例。每个 app 目前还有一个由所有 preview deployment 共用的 preview 数据库。发布前再次核对。 ## Cron ```ts Deno.cron("daily cleanup", "0 3 * * *", async () => { await runCleanup(); }); ``` Cron 使用 UTC。Deploy 在发布时发现任务并处理调度,执行显示在 dashboard/logs/traces。失败默认不重试;需要重试时在任务上显式配置 `backoffSchedule`(最多 5 次,单次延迟上限 1 小时)。任务必须幂等;重试与下一次计划重叠时可能跳过后一个执行。 ## Context 与环境变量 - Production:生产 timeline。 - Development:分支与 preview timeline。 - Build:只在构建阶段可见,不自动进入运行时。 - Secret 创建后不再在 UI 显示;不要通过日志回显。 使用 `DENO_TIMELINE`、`DENO_DEPLOY_APP_ID` 和 deployment 标识做日志维度,而不是把它们当访问控制。 官方依据:[Databases](https://docs.deno.com/deploy/reference/databases/)、[Cron](https://docs.deno.com/deploy/reference/cron/)、[Timelines](https://docs.deno.com/deploy/reference/timelines/)、[Environment contexts](https://docs.deno.com/deploy/reference/env_vars_and_contexts/)。 --- # Deno Deploy Source: /docs/deploy/deno-deploy/ Deno Deploy 是 Deno 的托管部署平台,也支持把 region 自托管到自己的基础设施。新平台控制台位于 `console.deno.com`,数据模型是 organization → app → revision → timeline;它不是旧 Deploy Classic 的原地升级。 ## 创建与部署 可以连接 GitHub 让平台执行集成构建,也可以使用 Deno 内置 CLI: ```bash deno deploy deno deploy --prod deno deploy logs --org my-org --app my-app ``` 首次命令会通过系统 keyring 管理认证。自动化环境使用平台认可的 token 流程,不把 deploy token 写进命令历史。 | 新平台能力 | 截至 2026-08-03 的官方说明 | | --- | --- | | 环境 | Production、Development、Build contexts 分离 | | 数据 | PostgreSQL 或 Deno KV,按 timeline 创建逻辑隔离 | | 调度 | `Deno.cron()`,运行记录进入日志与 traces | | 可观测性 | dashboard 提供 logs、traces 与 metrics | | 缓存 | CDN caching 与 Web Cache API | | 区域 | 托管 US/EU,另可自托管 region;发布前核对最新列表 | | Queue | 新 Deploy 当前不支持旧 Deno KV Queue API | ## 发布前准备 1. 服务通过 `Deno.serve` 或所选框架公开 HTTP 入口。 2. `deno.json` 中有可复现的 install/build/start 约定。 3. `deno.lock` 已提交,CI 已通过。 4. 密钥只声明变量名,不写入仓库。 5. 对 KV、数据库、cron 等能力分别确认一致性与配额;使用 queue 的 Classic 项目先设计替代方案。 ## 上线验证 ```text GET /health → 200 + 稳定小响应 无密钥请求 → 明确失败,不泄露配置 超时或取消 → 中止上游请求 重复写入 → 幂等或可检测 回滚 → 前一产物可以重新激活 ``` 检查构建日志与请求日志是否含 token、Authorization header 或用户输入。对外部 API 使用超时;对可重试写入使用幂等键。 ## Revision 与回滚 每个 timeline 维护 revision 历史,active revision 承接流量。发布前验证非生产 timeline,再切换生产;回滚是重新激活上一 revision,不等于自动回滚数据库 migration。 ## Deno KV `Deno.openKv()` 在本地与托管环境的后端和持久化语义不同。键使用结构化数组;事务依赖 versionstamp。开发测试优先使用内存或隔离数据库,生产前核对当前 Deploy 配额和一致性说明。 继续阅读:[数据库、Cron 与 Timelines](/docs/deploy/data-and-cron) 和 [Classic 迁移](/docs/deploy/classic-migration)。 官方参考:[About Deno Deploy](https://docs.deno.com/deploy/)、[`deno deploy` CLI](https://docs.deno.com/runtime/reference/cli/deploy/)、[Timelines](https://docs.deno.com/deploy/reference/timelines/)。 --- # Docker 与容器 Source: /docs/deploy/docker/ ## 多阶段示例 ```dockerfile FROM denoland/deno:alpine AS build WORKDIR /app # 先复制 manifest 与 lockfile 安装依赖,源码变化不使依赖层失效 COPY deno.json deno.lock ./ RUN deno ci COPY . . RUN deno install --entrypoint main.ts FROM denoland/deno:distroless WORKDIR /app COPY --from=build /app /app # 依赖缓存在 DENO_DIR(/deno-dir),不随 /app 复制,必须显式带过来 COPY --from=build /deno-dir /deno-dir USER deno EXPOSE 8000 CMD ["run", "--cached-only", "--allow-net", "--allow-env=PORT", "main.ts"] ``` `deno install` 缓存的是全局 `DENO_DIR`(官方镜像中为 `/deno-dir`),而不是项目目录。第二阶段漏掉这一步再叠加 `--cached-only`,容器启动时会因缓存缺失直接报错,而不是重新下载依赖。 根据依赖选择 `debian`、`alpine` 或 `distroless`。需要 shell、CA、字体或系统库时,不要只为镜像更小而使用缺少调试能力的变体。 ## 生产检查 - 固定 Deno 镜像标签或 digest,不用漂移的 `latest` 发布。 - `.dockerignore` 排除 `.git`、密钥、覆盖率与本地缓存。 - 使用非 root 用户并保持只读根文件系统;只挂载需要写入的目录。 - `0.0.0.0` 监听和 `PORT` 行为由应用明确实现。 - 让平台发送 SIGTERM,测量优雅退出时长。 - 如果运行时禁止联网解析依赖,构建阶段预缓存并用 `--cached-only` 验证。 官方参考:[Deno and Docker](https://docs.deno.com/runtime/reference/docker/)。 --- # 生产工程基线 Source: /docs/deploy/production-baseline/ ## CI 门禁 ```bash deno --version deno ci deno audit deno fmt --check deno lint deno check **/*.ts deno test ``` CI 固定 Deno 版本,提交 `deno.lock`,不在失败后自动更新依赖。依赖升级作为独立变更评审。 ## 运行时基线 - 使用 task 或容器 `CMD` 固化最小权限;生产加入 `--no-prompt`。 - 输入、环境变量和外部响应都在边界校验。 - 所有外部请求有超时、取消和有限重试。 - 日志包含请求 ID、状态、耗时和错误分类,不包含密钥与敏感正文。 - 健康检查区分“进程存活”和“可以接收流量”。 - 容器/进程具有内存、CPU、并发和请求体限制。 ## 供应链 检查 JSR/npm 包的维护状态、发布者、许可和安装脚本。原生扩展与 FFI 会绕过 JavaScript 权限层,需要额外审计和 OS 隔离。 官方参考:[Continuous integration](https://docs.deno.com/runtime/reference/continuous_integration/)、[Security](https://docs.deno.com/runtime/fundamentals/security/)。 --- # 第一个 Deno 项目 Source: /docs/getting-started/first-project/ 初始化项目: ```bash deno init hello-deno cd hello-deno ``` ```text hello-deno/ ├── deno.json ├── main.ts └── main_test.ts ``` 将 `main.ts` 改成一个可测试的处理函数: ```ts export function handler(request: Request): Response { const url = new URL(request.url); return Response.json({ message: "Hello Deno", path: url.pathname }); } if (import.meta.main) { Deno.serve({ port: 8000 }, handler); } ``` `deno.json` 记录团队命令: ```json { "tasks": { "dev": "deno run --watch --allow-net=0.0.0.0:8000 main.ts", "check": "deno fmt --check && deno lint && deno check main.ts main_test.ts", "test": "deno test" } } ``` ```bash deno task check deno task test deno task dev ``` `Deno.serve` 监听端口需要网络权限;单元测试直接调用 `handler()` 时不监听网络,因此测试不必使用 `-A`。 下一步:[权限模型](/docs/core/permissions)、[Web 开发选型](/docs/web)、[项目蓝图](/docs/projects)。 --- # 安装 Deno Source: /docs/getting-started/installation/ ## 选择安装方式 ```bash brew install deno ``` ```bash curl -fsSL https://deno.land/install.sh | sh ``` ```powershell irm https://deno.land/install.ps1 | iex ``` 如果团队使用多版本或 CI,优先由版本管理器、容器镜像或 Action 固定版本,不要依赖每台机器“碰巧安装的最新版”。 ## 验证环境 ```bash deno --version deno info deno help ``` 升级官方脚本安装的版本: ```bash deno upgrade ``` Homebrew、Scoop 或其他包管理器安装的版本应由对应包管理器升级。 ## 编辑器 VS Code 安装官方 Deno 扩展后,在项目里执行 `Deno: Initialize Workspace Configuration`。如果同一仓库还包含 Node 项目,只在 Deno 目录启用扩展,避免两套 TypeScript language server 同时诊断同一文件。 验收:`deno --version` 成功,编辑器能识别 `Deno.serve`,并且终端和编辑器使用同一个 Deno 二进制。 官方参考:[Installation](https://docs.deno.com/runtime/getting_started/installation/)、[Set up your environment](https://docs.deno.com/runtime/getting_started/setup_your_environment/)。 --- # 心智模型 Source: /docs/getting-started/mental-model/ ## 一张图记住 Deno ```text 源代码 (.ts/.js/.wasm) ├─ 模块:本地 / JSR / npm / URL ├─ 配置:deno.json(c) + deno.lock ├─ 工具:fmt / lint / check / test / doc / compile └─ 运行:Web API + Deno API + Node 兼容层 ↓ 权限边界 (--allow-* / --deny-*) ↓ 文件、网络、环境变量、子进程、FFI ``` ## 与 Node.js 最不同的三点 1. **安全默认值**:敏感 I/O 默认拒绝,权限可以限制到路径、主机或变量名。 2. **TypeScript 直接运行**:执行时会转译但不会自动完成完整类型检查;CI 仍应运行 `deno check`。 3. **一体化工具链**:格式化、Lint、测试和文档生成属于同一版本的 CLI。 ## 依赖不等于一种来源 - JSR 包使用 `jsr:`,适合 TypeScript 原生包与 Deno 标准库。 - npm 包使用 `npm:`,Node 内置模块使用 `node:`。 - `deno.json` 的 `imports` 可以把完整 specifier 映射为稳定的裸导入名。 - `deno.lock` 固定解析结果并记录完整性信息,应提交到版本库。 “能运行 `.ts`”不等于“每次运行前都执行完整类型检查”。把 `deno check` 放进 CI,才能把类型错误变成发布门禁。 继续阅读:[权限模型](/docs/core/permissions) 与 [依赖管理](/docs/core/dependencies)。 --- # 5 分钟上手 Source: /docs/getting-started/quickstart/ ## 1. 安装并验证 ```bash curl -fsSL https://deno.land/install.sh | sh deno --version ``` ```powershell irm https://deno.land/install.ps1 | iex deno --version ``` ```bash docker run --rm denoland/deno:latest deno --version ``` ## 2. 初始化并运行 ```bash deno init --serve hello-deno cd hello-deno deno task dev ``` `deno init --serve` 会创建 `deno.json`、服务入口和测试。模板服务需要监听网络;如果你手写入口,可明确运行: ```ts title="main.ts" Deno.serve((_request) => Response.json({ ok: true })); ``` ```bash deno run --allow-net main.ts curl http://localhost:8000 # {"ok":true} ``` ## 3. 添加测试 ```ts title="math_test.ts" import { assertEquals } from "jsr:@std/assert"; Deno.test("adds two numbers", () => { assertEquals(2 + 3, 5); }); ``` ```bash deno test ``` ## 4. 提交前门禁 ```bash deno fmt --check deno lint deno check **/*.ts deno test ``` `-A` 等于 `--allow-all`,会关闭权限沙箱。服务只需要监听端口时,使用 `--allow-net`;需要读取特定密钥时,使用 `--allow-env=KEY_NAME`。 官方依据:[Get started](https://docs.deno.com/runtime/)、[Installation](https://docs.deno.com/runtime/getting_started/installation/)、[Testing](https://docs.deno.com/runtime/test/)。 --- # Deno 是什么 Source: /docs/getting-started/what-is-deno/ Deno 是由 Node.js 创始人 Ryan Dahl 发起的开源 JavaScript、TypeScript 与 WebAssembly 运行时。它没有试图发明另一门语言,而是把现代 Web API、安全权限、包管理和工程工具收进一个 CLI。 ## 为什么会有 Deno Node.js 诞生时,CommonJS、npm 和前后端 JavaScript 都还很早期。Deno 重新选择了 ES Modules、URL/注册表依赖、Web 标准 API 和默认隔离,并把格式化、Lint、测试与类型检查做成内置能力。Deno 2 又加强了 Node 与 npm 兼容,因此今天的迁移通常可以渐进完成。 ```text Node.js 项目 Deno-first 项目 ├── package.json ├── deno.json ├── package-lock.json ├── deno.lock ├── node_modules ├── 全局缓存(默认) ├── tsc / eslint / test runner └── deno fmt / lint / check / test └── 默认拥有系统访问 └── 默认拒绝敏感系统访问 ``` ## 应该选哪个运行时 | 场景 | 更自然的选择 | 先核对什么 | | --- | --- | --- | | 大量既有 Node 包与团队流程 | Node.js 或渐进采用 Deno | 原生扩展、loader、生命周期脚本 | | TypeScript API、CLI、自动化 | Deno | 权限清单与 npm 兼容性 | | 极致启动速度、Bun 专属工具链 | Bun | Node API 与生产平台支持 | | Cloudflare 原生绑定和全球边缘 | Workers | Node 兼容子集、CPU 与平台限制 | | Fresh、Deno Deploy、MCP 工具服务 | Deno | 框架和托管平台当前版本 | Deno 提供好用的默认值,但真实项目仍应提交 `deno.json`、`deno.lock`、tasks 和最小权限命令。少配置不等于没有工程约束。 官方参考:[Deno runtime](https://docs.deno.com/runtime/)、[Node and npm compatibility](https://docs.deno.com/runtime/fundamentals/node/)、[Cloudflare Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/)。 --- # 从 Node.js 迁移到 Deno Source: /docs/migration/ Deno 2 能读取 `package.json`、解析 npm 包并运行大量 Node API。迁移的正确起点不是重写 imports,而是证明现有应用在另一 runtime 下仍然满足测试与生产约束。 ## 四段路线 ## 每阶段验收 ```bash deno install deno check src/main.ts deno test deno task start ``` 在 CI、开发机和目标生产环境都通过之前,不删除旧 lockfile、旧 runtime 命令或部署路径。一次提交只改变运行时、包管理、测试、框架或部署中的一层。 关键依赖只有未兼容的 Node-API 二进制、系统深度依赖自定义 loader,或团队没有生产回归测试时,先补证据和隔离层;不要为了“完成迁移”重写稳定业务。 官方参考:[Migrate to Deno](https://docs.deno.com/runtime/migrate/)、[Node and npm compatibility](https://docs.deno.com/runtime/fundamentals/node/)。 --- # Node API 替换表 Source: /docs/migration/api-mapping/ Deno 支持大量 `node:` API,所以迁移不等于把每个调用都重写。先用兼容 API 跑通,再在能减少依赖或改善可移植性时采用 Web/Deno API。 | Node.js | Deno / Web API | 建议 | | --- | --- | --- | | `fs.promises.readFile(path, "utf8")` | `Deno.readTextFile(path)` | 两者都需要文件权限 | | `fs.promises.writeFile` | `Deno.writeTextFile` | 收紧到具体目录 | | `http.createServer` | `Deno.serve` | 新 HTTP 服务首选 Web Request/Response | | `process.env.NAME` | `Deno.env.get("NAME")` | 只授权指定变量 | | `process.argv.slice(2)` | `Deno.args` | CLI 参数可直接迁移 | | `child_process.spawn` | `new Deno.Command()` | 需要 `--allow-run=` | | `__dirname` | `new URL(".", import.meta.url)` | 保持 URL 语义,必要时转路径 | | `crypto.randomUUID()` | `crypto.randomUUID()` | Web API 可直接复用 | | `Buffer` | `Uint8Array` / `TextEncoder` | 协议边界优先 Web 类型 | ## 一个文件读取示例 ```ts // 兼容优先:保留 Node API import { readFile } from "node:fs/promises"; const a = await readFile("config.json", "utf8"); // Deno-first:更短的文本 API const b = await Deno.readTextFile("config.json"); ``` 两种代码都应以同样的最小权限运行: ```bash deno run --allow-read=config.json main.ts ``` 迁移测试至少覆盖路径、编码、stream/backpressure、信号、超时与错误类型;函数名相似不代表边界行为完全相同。 官方参考:[Node APIs](https://docs.deno.com/api/node/)、[Deno APIs](https://docs.deno.com/api/deno/)、[Web APIs](https://docs.deno.com/api/web/)。 --- # 从 Node.js 迁移 Source: /docs/migration/from-node/ Deno 2 可以直接运行许多现有 Node 项目。迁移目标不是一次性改写全部 import,而是建立可回滚的兼容性证据。 ## 分阶段采用 1. 固定 Deno 版本,在现有分支运行 `deno --version`。 2. 保留 `package.json`、当前 lockfile 和 `node_modules` 策略,先运行一个无副作用脚本。 3. 执行现有测试;记录失败的 Node API、原生扩展、loader 与生命周期脚本。 4. 引入 `deno.json` tasks,将 Deno 命令与原命令并行运行。 5. 只有 CI、开发机和生产验证都通过后,才删除旧工具或锁文件。 ## 兼容约定 ```ts import { readFile } from "node:fs/promises"; import express from "npm:express"; ``` Deno 也能根据 `package.json` 解析常见裸 npm 导入。是否启用/需要本地 `node_modules` 由项目配置和依赖行为决定,不要假定一种模式适合全部包。 ## 高风险区域 - Node-API 原生扩展、postinstall 脚本与二进制下载; - 自定义 ESM loader、模块解析 hack 与隐式扩展名; - Jest/Vitest 全局、fake timers、snapshot 差异; - 依赖未显式声明的环境变量、文件路径和网络访问; - `process`、Buffer、streams 与边缘 Node API 的行为差异。 不要在同一个迁移提交里同时更换运行时、测试框架、包管理器、格式化规则和部署平台。否则失败时无法定位变量。 官方参考:[Node compatibility](https://docs.deno.com/runtime/fundamentals/node/)、[Migrate to Deno](https://docs.deno.com/runtime/migrate/)。 --- # npm、package.json 与 deno.json Source: /docs/migration/packages-and-config/ Deno 2 可以同时读取 `package.json` 与 `deno.json`。迁移初期不必删掉 Node 配置;先让现有项目在 Deno 下通过安装、检查和测试。 | Node 工作流 | Deno 对应方式 | | --- | --- | | `npm install` | `deno install` | | `npm install lodash` | `deno install lodash` | | `npm run dev` | `deno task dev`(也能执行 package scripts) | | `package-lock.json` | `deno.lock`;迁移期可并存 | | 裸导入 `lodash` | package.json 项目可解析;Deno-first 也可用 `npm:lodash` | ## node_modules 三种模式 | 模式 | 适用场景 | | --- | --- | | `none` | 新 Deno 项目,默认使用全局缓存 | | `auto` | bundler、Node-API 或工具要求本地目录 | | `manual` | 既有 `package.json` 工作流,显式执行安装 | ```json { "nodeModulesDir": "auto", "tasks": { "dev": "deno run --watch -N -E src/main.ts", "verify": "deno fmt --check && deno lint && deno check src/main.ts && deno test" } } ``` 生命周期脚本默认不会无条件执行。依赖确实需要 install script 时,运行 `deno approve-scripts` 交互式审查并批准(批准项持久化到 deno.json 的 `allowScripts`,Deno 2.6+ 首推);也可以用 `deno install --allow-scripts=` 一次性精确批准。无论哪种方式,lockfile 变化后都要重新审计。 删除 npm lockfile 和 node_modules 是迁移的收尾动作,不是第一步。先保留可工作的回滚路径,再决定是否转为 Deno-first 依赖布局。 官方参考:[Dependency management](https://docs.deno.com/runtime/packages/)、[Node and npm compatibility](https://docs.deno.com/runtime/fundamentals/node/)。 --- # Node 迁移常见问题 Source: /docs/migration/troubleshooting/ ## Cannot find module 先执行 `deno install`。如果依赖由 `package.json` 声明但工具需要实体目录,选择 `nodeModulesDir: "auto"` 或保留 manual 模式,不要盲目复制依赖。 ```bash deno info src/main.ts deno check src/main.ts ``` ## require is not defined 优先把自有代码改成 ESM。必须保留 CommonJS 时使用 `.cjs`,或让最近的 `package.json` 声明 `"type": "commonjs"`。不要用全局伪造的 `require` 隐藏模块边界。 ## npm 包能解析但运行失败 检查顺序: 1. 是否依赖 Node-API 原生扩展; 2. 是否要求 install/postinstall script; 3. 是否假定存在可写 `node_modules`; 4. 是否读取未授权环境变量、证书或配置文件; 5. 是否依赖尚未实现的 Node API 行为。 ## 测试在 Node 通过、Deno 失败 先保留原测试 runner,在 Deno 下执行它;不要同时迁移到 `Deno.test`。重点检查全局对象、fake timers、snapshot 路径、环境变量和临时目录。建立兼容基线后,再逐个测试文件转换。 ## PermissionDenied 这是缺权限的证据,不是让你直接加 `-A` 的理由。用 `DENO_TRACE_PERMISSIONS=1` 或 permission audit 找到资源,再授权具体 host、变量、命令或目录。 完整运行时错误见[常见错误](/docs/reference/errors),官方参考:[Node compatibility](https://docs.deno.com/runtime/fundamentals/node/)、[Migrating from Node](https://docs.deno.com/runtime/migrate/)。 --- # Deno 实战项目蓝图 Source: /docs/projects/ 这里不是只展示 “Hello World”,而是给出能逐步实现、测试和部署的项目骨架。先完成垂直切片,再添加功能。 ## 1. [Fresh 博客](/docs/projects/fresh-blog) ```text fresh-blog/ ├── routes/posts/[slug].tsx ├── routes/admin/posts.tsx ├── islands/PostEditor.tsx ├── components/ ├── src/db/{client,schema}.ts └── deno.json ``` 技术:Fresh 2 + PostgreSQL + Drizzle。第一阶段只做文章列表、详情和受保护的创建接口;第二阶段才加 Markdown、草稿、图片与搜索。 验收:无 JavaScript 时文章可阅读;slug 唯一约束生效;编辑 island 之外没有多余 hydration;migration 在独立发布步骤运行。 ## 2. [Deno MCP Server](/docs/projects/mcp-server-project) ```text mcp-project/ ├── main.ts ├── tools/{search,read}.ts ├── schemas.ts ├── fixtures/ └── deno.json ``` 从 stdio 和只读 tool 开始。使用 Zod 校验参数、fixtures 做协议测试,再决定是否增加 Streamable HTTP、认证与写操作。参考 [MCP Server 教程](/docs/ai/mcp-server)。 ## 3. [Cron 服务](/docs/projects/cron-service) ```text cron-service/ ├── jobs/daily-report.ts ├── services/report.ts ├── main.ts └── deno.json ``` 把 job 逻辑写成普通可测试函数,再由 `Deno.cron()` 触发。任务必须幂等,使用数据库唯一键或 execution key 防止重复副作用,并为延迟、失败和补跑提供观测入口。 ## 4. [图片处理 API](/docs/projects/image-api) ```text image-api/ ├── routes/transform.ts ├── services/image.ts ├── storage.ts └── main.ts ``` 用 Hono + `npm:sharp` 接收受限尺寸的图片,验证 MIME 与像素上限,转换后写入对象存储。`sharp` 涉及原生依赖,部署前必须在目标 Deno 版本和容器/Deploy 环境做 smoke test。 ## 每个项目共用的完成标准 - `deno fmt --check && deno lint && deno check && deno test` 全部通过; - `deno.lock` 提交,安装脚本有显式 allowlist; - 开发、迁移、测试和线上进程分别使用最小权限; - health check、结构化日志、超时、错误映射和 secret 管理已接入; - README 写明本地启动、数据库准备、部署与回滚。 --- # 实战:Deno Cron 服务 Source: /docs/projects/cron-service/ 本教程把 [项目蓝图 3](/docs/projects) 展开为可执行步骤。Deno Deploy 的 timeline、数据库与 Cron 平台行为见 [数据库、Cron 与 Timelines](/docs/deploy/data-and-cron),本文聚焦服务本身的写法。 ## 目标与最终形态 ```text cron-service/ ├── jobs/daily-report.ts # 调度入口:认领 execution key、调用 service、记录结果 ├── services/report.ts # 纯业务逻辑,不感知 cron ├── main.ts # Deno.cron 注册(模块顶层) ├── migrations/ # job_runs 表 └── deno.json ``` 核心纪律:**`Deno.cron()` 的 handler 里不放业务逻辑**。job 是普通 async 函数,测试直接调用它;cron 只负责按 UTC 时间触发。 ## 初始化 ```bash deno init cron-service cd cron-service deno add npm:postgres ``` 示例用 PostgreSQL 存执行记录(`postgres` 是纯 JS 驱动,Deno 直接可用)。连接串放环境变量 `DATABASE_URL`。如果用 Deno KV 代替,思路相同——用 atomic 操作抢占唯一 key。 ## 里程碑一:可测试的 job ### 业务逻辑与调度无关 ```ts title="services/report.ts" import type { Sql } from "postgres"; export async function buildDailyReport(sql: Sql, day: string) { const rows = await sql` select count(*)::int as orders, coalesce(sum(total), 0)::numeric as revenue from orders where created_at >= ${day}::date and created_at < (${day}::date + interval '1 day')`; return { day, orders: rows[0].orders, revenue: rows[0].revenue }; } ``` 入参显式传入 `day`,测试时可以重放任意一天,不必等待真实调度。 ### 幂等:唯一 execution key 关键问题:cron 可能重试,补跑也会再次执行同一逻辑日期。用一张执行登记表,以 `(job_name, execution_key)` 唯一约束做"认领": ```sql title="migrations/0001_job_runs.sql" create table job_runs ( id serial primary key, job_name text not null, execution_key text not null, status text not null default 'running', started_at timestamptz not null default now(), finished_at timestamptz, error text, unique (job_name, execution_key) ); ``` ```ts title="jobs/daily-report.ts" import type { Sql } from "postgres"; import { buildDailyReport } from "../services/report.ts"; export async function runDailyReport(sql: Sql, day: string) { const claimed = await sql` insert into job_runs (job_name, execution_key) values ('daily-report', ${day}) on conflict (job_name, execution_key) do update set status = 'running', started_at = now(), finished_at = null, error = null where job_runs.status = 'failed' returning id`; if (claimed.length === 0) { return { skipped: true, day }; // 已成功或正在执行,直接返回 } try { const report = await buildDailyReport(sql, day); // 业务写入同样以 day 为幂等键:delete + insert 或 upsert await sql` insert into daily_reports (day, orders, revenue) values (${day}, ${report.orders}, ${report.revenue}) on conflict (day) do update set orders = excluded.orders, revenue = excluded.revenue`; await sql` update job_runs set status = 'ok', finished_at = now() where id = ${claimed[0].id}`; return report; } catch (err) { await sql` update job_runs set status = 'failed', finished_at = now(), error = ${String(err)} where id = ${claimed[0].id}`; throw err; } } ``` 两层幂等缺一不可:`job_runs` 防止重复执行,`daily_reports` 的 upsert 保证即使重试发生在业务写入之后、状态更新之前,重跑也只覆盖同一行。 注意认领语句的 `do update ... where status = 'failed'`:用 `on conflict do nothing` 会让失败记录永远占住唯一键,平台重试和人工补跑都会被当成“已执行”跳过。另一个边界是进程在状态更新前崩溃留下的永久 `running`——生产中再加一条“`started_at` 超过阈值视为租约过期”的回收条件。 ## 里程碑二:注册 cron ```ts title="main.ts" import postgres from "postgres"; import { runDailyReport } from "./jobs/daily-report.ts"; const sql = postgres(Deno.env.get("DATABASE_URL")!); function utcYesterday(): string { const d = new Date(Date.now() - 86_400_000); return d.toISOString().slice(0, 10); } Deno.cron( "daily-report", "0 3 * * *", { backoffSchedule: [60_000, 300_000, 900_000] }, async () => { console.log(JSON.stringify({ job: "daily-report", day: utcYesterday() })); await runDailyReport(sql, utcYesterday()); }, ); ``` 要点(平台行为细节见 [deploy 文档](/docs/deploy/data-and-cron)): - 调度时间是 **UTC**;`0 3 * * *` 是 UTC 03:00,不是本地时区。 - **失败默认不重试**。需要重试必须显式给 `backoffSchedule`:数组每个元素是下一次重试前的毫秒延迟,最多 5 次重试,单次延迟上限 1 小时。 - 同一 job 不会并发执行:上一次还在跑(或重试与下一次调度重叠)时,后一次直接跳过。job 跑得比调度间隔还久时不会堆积。 - 注册必须在**模块顶层**、server 启动前完成;写在请求 handler 或条件分支里的 job 不会被 Deploy 发现。 ## 测试策略 本地不要靠等待真实调度来测试——直接测函数: ```ts title="jobs/daily-report_test.ts" import { assertEquals } from "jsr:@std/assert"; import postgres from "postgres"; import { runDailyReport } from "./daily-report.ts"; const sql = postgres(Deno.env.get("TEST_DATABASE_URL")!); Deno.test("重复执行同一天只跑一次", async () => { // 先清理 execution key,避免测试库里的旧数据让用例假通过 await sql`delete from job_runs where job_name = 'daily-report' and execution_key = '2026-08-01'`; await sql`delete from daily_reports where day = '2026-08-01'`; const first = await runDailyReport(sql, "2026-08-01"); const second = await runDailyReport(sql, "2026-08-01"); assertEquals((first as { skipped?: boolean }).skipped, undefined); assertEquals((second as { skipped?: boolean }).skipped, true); const rows = await sql` select count(*)::int as n from daily_reports where day = '2026-08-01'`; assertEquals(rows[0].n, 1); }); ``` 再补两类用例:业务写入中途失败时 `job_runs` 记录 `failed` 并可补跑;补跑后 `daily_reports` 仍只有一行。测试库用独立 `TEST_DATABASE_URL`,和开发库分开。 ## 部署与观测 - Deploy 在发布时评估顶层模块代码、发现 `Deno.cron()` 定义并接管调度;回滚 revision 会重新注册该 revision 的 cron。生产与各 Git 分支 timeline 各自独立触发,注意 preview 环境也会跑——用 `DENO_TIMELINE` 区分日志维度,必要时在非生产 timeline 跳过外部副作用。 - 执行记录出现在 dashboard 的 Cron 标签页与日志/traces 中(可按 `kind:cron`、`cron.name:` 过滤);`job_runs` 表则回答业务层问题:哪一天没跑、哪次失败、补跑结果。 - 每次 cron 执行按一次入站 HTTP 请求计费;免费组织每个 revision 最多 10 个 cron job。 - 权限:cron 服务需要 `--allow-net`(数据库)与 `--allow-env=DATABASE_URL`,不要给 `-A`。 ## 验收清单 官方参考:[Deno Deploy Cron](https://docs.deno.com/deploy/reference/cron/)、[Timelines](https://docs.deno.com/deploy/reference/timelines/)、[postgres.js](https://github.com/porsager/postgres)。 --- # 实战:Fresh 2 + PostgreSQL + Drizzle 博客 Source: /docs/projects/fresh-blog/ 本教程把 [项目蓝图 1](/docs/projects) 展开为可执行步骤。基础路由、Island 与中间件语法见 [Fresh 2 全栈开发](/docs/web/fresh),本文只写博客特有的部分。 ## 目标与最终形态 ```text fresh-blog/ ├── routes/posts/[slug].tsx # 文章详情(服务端渲染) ├── routes/index.tsx # 文章列表 ├── routes/admin/posts.tsx # 受保护的创建页 + POST ├── routes/admin/_middleware.ts # admin 区域访问控制 ├── routes/admin/login.tsx # 登录表单(写入 cookie) ├── islands/PostEditor.tsx # 唯一需要 hydration 的组件 ├── components/ ├── src/db/{client,schema}.ts # Drizzle 连接与表结构 ├── drizzle.config.ts └── deno.json ``` 阶段一交付:列表、详情、受保护的创建。阶段二再加 Markdown 渲染、草稿、图片与搜索。创建表单用原生 `
` 提交,无 JavaScript 时全站可读可写。 ## 初始化 ```bash deno run -Ar jsr:@fresh/init cd fresh-blog deno install npm:drizzle-orm npm:drizzle-kit npm:pg npm:@types/pg ``` 本地 PostgreSQL 用 Docker 起一个即可,连接串写入 `.env`: ```bash docker run --name blog-pg -e POSTGRES_PASSWORD=dev-only -p 5432:5432 -d postgres ``` ```bash title=".env" DATABASE_URL=postgresql://postgres:dev-only@localhost:5432/postgres ``` drizzle-kit 在 Deno 下运行时要加 `--node-modules-dir`(官方 Deno + Drizzle 教程的做法),生成和迁移命令见下面的 task 定义。 ## 里程碑一:列表、详情、受保护创建 ### 表结构与迁移 ```ts title="src/db/schema.ts" import { boolean, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; export const posts = pgTable("posts", { id: serial().primaryKey(), slug: text().notNull().unique(), title: text().notNull(), body: text().notNull(), published: boolean().notNull().default(false), createdAt: timestamp().notNull().defaultNow(), }); ``` `slug` 上的 `unique()` 是验收点之一:重复 slug 必须在数据库层被拒绝,而不是靠应用层查重。 ```ts title="src/db/client.ts" import { drizzle } from "drizzle-orm/node-postgres"; import { Pool } from "pg"; const pool = new Pool({ connectionString: Deno.env.get("DATABASE_URL") }); export const db = drizzle(pool); ``` ```ts title="drizzle.config.ts" import { defineConfig } from "drizzle-kit"; export default defineConfig({ out: "./drizzle", schema: "./src/db/schema.ts", dialect: "postgresql", dbCredentials: { url: Deno.env.get("DATABASE_URL")! }, }); ``` 在 `deno.json` 的 `tasks` 中追加(模板自带的 `dev`、`check` 保持不变): ```json title="deno.json(节选)" { "tasks": { "db:generate": "deno run -A --node-modules-dir npm:drizzle-kit generate", "db:migrate": "deno run -A --env --node-modules-dir npm:drizzle-kit migrate" } } ``` `-A` 只授给本地受信的迁移工具(drizzle-kit 需要读配置、连数据库、写 SQL 文件),不是应用运行时权限;线上进程的权限单独按最小集合配置。 `--env` 让 Deno 加载 `.env`。先 `deno task db:generate` 产出 `drizzle/` 下的 SQL,审查后再 `deno task db:migrate` 应用。 ### 数据访问函数 把查询写成普通函数,路由和测试共用: ```ts title="src/db/posts.ts" import { desc, eq } from "drizzle-orm"; import { db } from "./client.ts"; import { posts } from "./schema.ts"; export function listPublishedPosts() { return db.select().from(posts) .where(eq(posts.published, true)) .orderBy(desc(posts.createdAt)); } export function getPostBySlug(slug: string) { return db.select().from(posts).where(eq(posts.slug, slug)).limit(1); } export async function createPost(input: { slug: string; title: string; body: string }) { // 阶段一提交即发布;草稿流程在里程碑二引入 await db.insert(posts).values({ ...input, published: true }); } ``` ### 路由 ```tsx title="routes/posts/[slug].tsx" import { define } from "@/utils.ts"; import { HttpError } from "fresh"; import { getPostBySlug } from "@/src/db/posts.ts"; export default define.page(async (ctx) => { const [post] = await getPostBySlug(ctx.params.slug); if (!post || !post.published) { // 交给 Fresh 的错误处理,响应状态是真正的 404,而不是 200 的“不存在”页面 throw new HttpError(404); } return (

{post.title}

{post.body}
); }); ``` 列表页 `routes/index.tsx` 同样用 `define.page` 调 `listPublishedPosts()` 渲染 `
    `。两者都是服务端组件,浏览器不收到对应 JavaScript。 创建页用原生表单加 POST handler,提交成功后 303 跳转: ```tsx title="routes/admin/posts.tsx" import { define } from "@/utils.ts"; import { createPost } from "@/src/db/posts.ts"; export const handlers = define.handlers({ async POST(ctx) { const form = await ctx.req.formData(); const slug = form.get("slug")?.toString() ?? ""; const title = form.get("title")?.toString() ?? ""; const body = form.get("body")?.toString() ?? ""; if (!/^[a-z0-9-]+$/.test(slug) || !title || !body) { return new Response("invalid input", { status: 400 }); } try { await createPost({ slug, title, body }); } catch (err) { // 只把唯一约束冲突(SQLSTATE 23505)映射为 409,连接失败等按 500 处理 if (err && typeof err === "object" && "code" in err && err.code === "23505") { return new Response("slug already exists", { status: 409 }); } console.error("createPost failed", err); return new Response("internal error", { status: 500 }); } return new Response(null, { status: 303, headers: { location: `/posts/${slug}` }, }); }, }); export default define.page(function NewPost() { return (

    新文章