# Deno Documentation - Full Content > This document contains the full content of the Deno documentation website. # 可接受使用政策 > Deno Deploy Classic 的可接受使用定义。 URL: https://docs.deno.com/deploy/acceptable_use_policy Deno Deploy 服务包含资源(CPU 时间,请求次数),这些资源受此可接受使用政策的约束。本文档可以大致估算我们认为的“可接受使用”以及我们不接受的使用情况。 ### 可接受使用的例子 - ✅ 服务器端渲染的网站 - ✅ Jamstack 网站和应用 - ✅ 单页面应用程序 - ✅ 查询数据库或外部 API 的 API - ✅ 个人博客 - ✅ 公司网站 - ✅ 电子商务网站 - ✅ 反向代理 ### 不可接受的使用 - ❌ 加密货币挖矿 - ❌ 高 CPU 密集型负载(例如机器学习) - ❌ 外部网站的媒体托管 - ❌ 网络爬虫 - ❌ 正向代理 - ❌ 虚拟私人网络 (VPN) ## 指导方针 我们期望大多数项目能够很好地控制在使用限制范围内。如果您的项目使用情况明显偏离正常水平,我们将通知您。在采取任何措施解决我们基础设施上不合理负担之前,我们会在可能的情况下与您联系。 --- # Deno Deploy 更新日志 > 列出 Deno Deploy 开发与演进过程中的显著进展 URL: https://docs.deno.com/deploy/changelog ## 2026年1月27日 ### 新功能 - Deno Deploy 应用现在可以通过 `deno.json` / `deno.jsonc` 文件配置,除了仪表盘设置之外。 - 仪表盘中可用的所有应用配置选项也可以在 `deno.json` 文件的 `deploy` 部分找到,包括安装和构建命令、运行时配置和框架预设。 - 当同时存在仪表盘和 `deno.json` 配置时,`deno.json` 的配置优先,完全覆盖仪表盘中的应用配置。 - 这使得以代码管理应用配置更加方便,并且可以将其与应用代码一同纳入版本控制。 - [在文档中了解更多。](/deploy/reference/builds/#editing-app-configuration-from-source-code) - 现在可以从组织设置页面重命名组织并更改其别名(slug)。 - [重命名组织](/deploy/reference/organizations/#rename-an-organization) 会更新仪表盘和邀请邮件中的显示名称。 - [更改组织别名](/deploy/reference/organizations/#update-the-organization-slug) 会更新该组织所有应用的默认域名,注意这将移除旧的默认域名。 - 详细信息请参见文档。 - 现在可以从 Deno Deploy 仪表盘查看支持工单。 - 通过发送邮件到 support@deno.com 提交支持请求后,会收到一封自动回复邮件,包含认领工单的链接。 - 认领工单后,该工单会关联到您的 Deno Deploy 账户,您可以通过[tickets 仪表盘](https://console.deno.com/tickets)跟踪支持请求状态。 - 运行时现可通过环境变量[`DENO_TIMELINE`](/deploy/reference/env-vars-and-contexts/#built-in-environment-variables)访问应用当前运行所在的时间线信息。 ### Bug 修复 - 修复了有时链接的 Postgres 数据库的数据库浏览器无法加载的问题。 - 修复了一些构建因无效构建缓存而卡住的问题,这需要通过支持手动干预解决。 ## 2025年8月27日 ### 新功能 - Deno Deploy 现在可以检测 Deno 和 NPM 的工作区 / 单仓库配置, 允许您部署位于大型仓库子目录中的应用。 - 在创建应用时,会扫描仓库中的工作区配置,允许您选择要部署的工作区成员。 - 构建期间,工作目录设置为工作区成员目录。 - 应用目录可在应用配置设置中自定义(创建后可修改)。 - 构建日志新增专门的“Deploy”部分,取代之前的“Warmup”和“Routing”阶段,提升部署时的清晰度。 - “Deploy”部分包含每个时间线的子部分,包括生产环境、Git 分支和预览部署。 - “Warmup”子部分显示与应用预热相关的日志。 - “Pre-deploy”子部分显示运行用户定义的预部署命令(如执行迁移)的日志。 - “Database creation”子部分显示为时间线创建链接数据库的日志。 - 顶部导航栏重新设计,新增当前仪表盘部分的面包屑下拉菜单,方便在应用和域名等间快速导航。 - GitHub 集成中,提交信息包含 `[skip ci]` 或 `[skip deploy]` 字符串时,可跳过该提交的部署。 - 超过30天未收到流量的修订版本将自动禁用,7天后无活动进一步删除。 - 禁用的修订版本可在删除前在修订页面重新启用。 - `deno deploy` CLI 及 `deno run` 和 `deno task` 的 `--tunnel` 选项现支持使用组织令牌认证,除用户令牌外。 - 使用组织令牌时,将其按惯例传入 `DENO_DEPLOY_TOKEN` 环境变量。 - 发布了多项运行时安全补丁,修复近期公开的 React 和 Next.js 漏洞: [CVE-2025-55182](https://deno.com/blog/react-server-functions-rce)(远程代码执行)及 [CVE-2025-55184/CVE-2025-67779](https://deno.com/blog/cve-2025-55184)(拒绝服务)。 - 另外,我们悄然为所有 Deno Deploy 用户启用了新的 Deno Sandbox 基础设施,欢迎体验。 - Deno Sandbox 提供完全隔离的 Linux 微型虚拟机,运行不受信任代码。 - 适合运行第三方代码(如插件、扩展、用户生成或大型语言模型生成代码),无需担心应用安全。 - 明年将公布更多关于 Deno Sandbox 的详情,敬请期待! - 可从 Deno Deploy 控制台的“Sandboxes”标签页体验。 - [了解更多关于 Deno Sandbox。](https://deno.com/deploy/sandbox) ### Bug 修复 - 在允许数据库实例链接到组织前,检查 Postgres 数据库实例是否支持数据库动态配置。 - 确保已删除的 Deno Deploy 应用在推送至之前关联的仓库时不会触发 GitHub 状态检查。 - playground HTTP 探索器在发送请求时正确带上设置的请求头。 - playground 不再因为顶层 `await` 报错。 - 现在可以向 Deno Deploy 应用添加名为 `GOOGLE_APPLICATION_CREDENTIALS` 的环境变量。 - 批量导入环境变量时,正确将它们导入到对应应用,而非错误地导入组织环境变量。 - 一些不支持 `using` 声明的 Next.js 版本现能正确构建。 - 构建步骤中的 `npm install` 现在更稳定,不会再因证书问题失败。 ## 2025年7月23日 ### 新功能 - 新增:Deno Deploy 应用支持数据库功能,轻松连接和使用 Postgres 数据库。 - 可在 AWS RDS、Neon、Supabase 或其他供应商处配置 Postgres 数据库实例,然后将其链接到您的 Deno Deploy 组织。 - 将数据库实例分配给某个应用,使其可在该应用环境中使用。 - 每个时间线(生产环境、每个 git 分支及预览)拥有独立数据库,使用单独模式和数据,允许您在不影响生产数据的情况下测试迁移和更改。 - 可使用任何 Postgres 客户端库连接,包括 `npm:pg`、`npm:drizzle` 或 `npm:kysely`。 - 应用和 playground 现支持重命名。注意,重命名后旧的 `deno.net` URL 将不再有效,但自定义域名仍然可用。 - 应用和 playground 现支持删除。 - playground 新增 HTTP 探索者标签页,允许您向 playground 服务的任意 URL 发起 HTTP 请求,便于测试不提供网页的 API 或其他服务。 - 您现在可通过点击文件夹名称旁的删除按钮,删除 playground 文件浏览器中的整个文件夹。 - playground 文件浏览器现支持拖放 ZIP 文件,自动上传 ZIP 中所有文件。 - playground 中可启用“保存时自动格式化”,保存文件时自动格式化代码。 ### Bug 修复 - 以 `DENO_` 前缀的环境变量如 `DENO_CONDITIONS`、`DENO_COMPAT` 和 `DENO_AUTH_TOKENS` 现在可以设置且不会出错。 - `DENO_REVISION_ID` 环境变量现已正确暴露给应用和 playground。 - 自定义域名分配抽屉中,已被其他应用或 playground 占用的自定义域名现显示为禁用状态。 - 监控页面上的网络使用图现在正确显示进出流量,之前显示的数据不正确。 - 新建组织时,首次构建会等待 `.deno.net` 域名被配置完成后再进行路由步骤。 - 在 playground 中按 `Ctrl-S` / `Cmd-S` 会保存当前文件并触发构建,而不是打开浏览器保存对话框。 - 查看某些特定追踪时之前会导致追踪查看器卡死,现在能正确显示。 ## 2025年7月9日 ### 新功能 - 新增:Cloud Connect 允许您安全地将 Deno Deploy 应用连接至 AWS 和 GCP,使您能够使用 AWS S3、Google Cloud Storage 等服务,无需管理凭据。 - 通过不存储任何长期有效静态凭据,而是使用短期令牌及 OIDC(OpenID Connect)在 Deno Deploy 与云提供商之间建立信任关系。 - 在应用设置页面或 playground 抽屉中提供设置流程,指引您连接应用至 AWS 或 GCP。 - 可以使用标准的 AWS 和 GCP SDK 访问服务,无需重写代码使用不同 API。 - [查看文档以了解更多。](https://docs.deno.com/deploy/early-access/reference/cloud-connections/) - 应用监控页面现显示更多指标,包括 V8 内存指标(如堆大小与垃圾回收统计)及进程级指标(如 CPU 使用率和整体内存占用)。 - 组织概览中新增“监控”标签页,展示组织内所有应用的整体指标,包括请求数、CPU 使用率和内存使用。 - 您现在可以通过编辑 playground 预览 iframe 上方显示的“地址栏”来修改预览 URL。 - 当环境变量键名包含 `SECRET`、`KEY`、`TOKEN`、`PRIVATE` 或 `PASSWORD` 时,默认环境变量被视为机密,您仍可手动切换为纯文本。 - 环境变量值最大长度限制从 1024 字符提升至 4096 字符。 ### Bug 修复 - playground 不再在尝试部署空文件时卡死。 - playground 抽屉调整大小更可靠,尤其是在某些抽屉已折叠时。 - 构建时间显著缩短,特别是大型项目中,之前超过 10 秒的“预热”和“路由”步骤现在通常少于 1 秒。 - 构建过程中的“排队”和“路由”步骤可取消。 - 创建组织页面现可正确反馈组织别名是否已被占用,提交前即提示。 - `npm install` 现能正常安装 `esbuild`,避免之前的通用错误。 ## 2025年6月24日 ### 新功能 - playground 现在支持实时日志和追踪面板: - 显示当前修订过去一小时内的日志和追踪。 - 日志和追踪可过滤,类似专用观察页面。 - 框架自动检测支持更多项目,包括许多基于 Vite 的项目。 - 组织下拉菜单中,当前选中的组织高亮更明显。 ### Bug 修复 - 监控概览中的小面积图形现正常工作。 - 错误率指标正确显示。 - 由 GitHub 触发的构建不再重复运行。 - Next.js 构建在较旧版本中更稳定。 ## 2025年6月12日 ### 新功能 - Deno Deploy 现支持 playground! - playground 可在组织概览的 playgrounds 标签页中创建和访问 - playground 支持多文件和构建步骤 - playground 界面提供已部署应用的预览 iframe - 目前提供三种模板:hello world、Next.js 和 Hono - 移动设备新增浮动导航栏,不会遮挡页面内容 ## 2025年6月9日 ### 新功能 - Deno Deploy 拥有了新徽标! - 任何人均可在 [console.deno.com](https://console.deno.com) 注册加入早期访问计划 - 构建相关 - 构建存储空间上限从 2 GB 提升至 8 GB - 构建可使用组织或应用设置中配置的环境变量和密钥(位于新的“构建”上下文中) - 构建最大运行时间为 5 分钟 - 监控页面全面重构,图表渲染部分重新编写: - 拖拽图表可缩放选中区域 - 可显示更多数据且页面加载不卡顿 - 工具提示跟随鼠标指针,并新增十字线以便精准分析 - 改进字体大小与颜色,提升可读性 ### Bug 修复 - 构建不再卡在待处理状态。 - 仪表板页面加载速度显著提升。 - 追踪中的跨度有未导出的父级时可正确显示。 - 切换时间范围时监控页面正常刷新。 - 遥测搜索栏的“清除搜索”按钮正常工作。 - 旧版 Next.js 版本(如 Next.js 13)现能正确构建。 - 环境变量抽屉已全局应用,修复了同名环境变量在不同上下文间冲突的问题。 - 构建器中使用绝对路径运行 `node ` 不再失败。 - 构建器中提供了 `npx`。 - Astro 构建不再偶发因 `--unstable-vsock` 错误失败。 - 明确指定使用 `@deno/svelte-adapter` 的 Svelte 部署正常。 ## 2025年5月26日 ### 新功能 - 手动触发构建时可以选择部署的分支。 - 支持部署 Astro 静态站点,无需手动安装 Deno 适配器。 - 提供了[参考文档](/deploy/),方便查阅。 ### Bug 修复 - 使用 `npm` 作为包管理器时,SvelteKit 自动检测正常工作。 - 预热阶段不再随机发送 POST 请求至您的应用。 - 访问带尾部斜杠的页面不再导致 404。 - 抽屉不会因拖动背景并释放或点击内部而关闭。 ## 2025年5月22日 ### 新功能 - 现在可以在创建应用时通过粘贴 `.env` 文件批量导入环境变量,位于环境变量抽屉中。 - SvelteKit 现开箱即用,无需手动安装 Deno 适配器。 - 新增对 Lume 静态站点生成器的预设支持。 ### Bug 修复 - 时间线页面中环境变量现正确显示。 - 生产时间线页面正确显示所有构建历史。 - console.deno.com 在旧版 Firefox 上能正常使用。 - console.deno.com 各页面标题现反映当前所在页面。 - “申请证书”按钮在 DNS 验证失败后不再卡死。 - 曾申请证书或绑定过应用的域名现可删除。 --- # 压缩响应体 URL: https://docs.deno.com/deploy/classic/api/compression :::info Legacy Documentation 您正在查看 Deno Deploy Classic 的旧版文档。我们建议迁移到新的 Deno Deploy 平台。 ::: 压缩响应体以节省带宽是一种常见做法。为了减轻您的负担,我们将此功能直接构建到 Deploy 中。 Deno Deploy Classic 支持 brotli 和 gzip 压缩。当满足以下条件时,将应用压缩。 1. 对您的部署的请求具有 [`Accept-Encoding`][accept-encoding] 头,设置为 `br` (brotli) 或 `gzip`。 2. 您的部署的响应包含 [`Content-Type`][content-type] 头。 3. 提供的内容类型是可压缩的;我们使用 [这个数据库](https://github.com/jshttp/mime-db/blob/master/db.json) 来确定该内容类型是否可压缩。 4. 响应体大小大于 20 字节。 当 Deploy 压缩响应体时,它将根据所使用的压缩算法将 `Content-Encoding: gzip` 或 `Content-Encoding: br` 头设置到响应中。 ### 何时跳过压缩? Deno Deploy Classic 在以下情况下跳过压缩: - 响应具有 [`Content-Encoding`][content-encoding] 头。 - 响应具有 [`Content-Range`][content-range] 头。 - 响应的 [`Cache-Control`][cache-control] 头具有 [`no-transform`][no-transform] 值(例如,`cache-control: public, no-transform`)。 ### 我的 `Etag` 头会发生什么? 当您设置响应的 Etag 头时,如果我们对您的响应体应用了压缩,我们会将头的值转换为弱 Etag。如果它已经是弱 Etag,我们不会修改该头。 [accept-encoding]: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept-Encoding [cache-control]: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control [content-encoding]: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Encoding [content-type]: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type [no-transform]: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control#other [content-range]: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Range --- # 动态导入 URL: https://docs.deno.com/deploy/classic/api/dynamic-import ### 规范符必须是静态确定的字符串字面量 在普通的动态导入中,规范符不需要在构建时确定。因此以下所有形式都是有效的: ```ts title="Deno CLI 中有效的动态导入" // 1. 静态确定的字符串字面量 await import("jsr:@std/assert"); // 2. 静态确定,但通过变量 const specifier = "jsr:@std/assert"; await import(specifier); // 3. 静态确定,但模板字面量 const stdModuleName = "path"; await import(`jsr:@std/${stdModuleName}`); // 4. 动态确定 const rand = Math.random(); const mod = rand < 0.5 ? "npm:cowsay" : "npm:node-emoji"; await import(mod); ``` 然而,在 Deno Deploy 中,规范符必须是没有字符串插值的字符串字面量。因此在上述三个例子中,只有第一个在 Deno Deploy 中有效。 ```ts title="仅静态字符串字面量在 Deno Deploy 中有效" // 1. ✅ 在 Deno Deploy 中正常工作 await import("jsr:@std/assert"); // 2. ❌ 在 Deno Deploy 中不工作 // 因为传递给 `import` 的是一个变量 const specifier = "jsr:@std/streams"; await import(specifier); // 3. ❌ 在 Deno Deploy 中不工作 // 因为这有一个插值 const stdModuleName = "path"; await import(`jsr:@std/${stdModuleName}`); // 4. ❌ 在 Deno Deploy 中不工作 // 因为这是动态的 const rand = Math.random(); const mod = rand < 0.5 ? "npm:cowsay" : "npm:node-emoji"; await import(mod); ``` ### 一个例外 - 动态规范符适用于同项目文件 如果目标文件(模块)包含在同一个项目中,动态确定的规范符是支持的。 ```ts title="动态规范符适用于同项目中的文件" // ✅ 在 Deno Deploy 中正常工作 await import("./my_module1.ts"); // ✅ 在 Deno Deploy 中正常工作 const rand = Math.random(); const modPath = rand < 0.5 ? "dir1/moduleA.ts" : "dir2/dir3/moduleB.ts"; await import(`./${modPath}`); ``` 请注意,以 `./` 开头的模板字面量告诉模块解析器目标模块在同一个项目中。相反,如果规范符不以 `./` 开头,可能的目标模块将不会包含在生成的 [eszip] 中,这将导致动态导入在运行时失败,即使最终评估的规范符以 `./` 开头。 ```ts // ❌ 不工作,因为分析器无法静态确定此情况下的规范符是否以 `./` 开头。 // 与之前的例子相比,唯一的区别是是否在模板字面量中或在变量中放置 `./`。 const rand = Math.random(); const modPath = rand < 0.5 ? "./dir1/moduleA.ts" : "./dir2/dir3/moduleB.ts"; await import(modPath); ``` 我们将考虑是否可以在未来放宽这一约束。 :::tip 什么是 eszip? 当您在 Deno Deploy 上进行新部署时,系统会分析您的代码,通过递归遍历构建模块图,并将所有依赖项打包到一个单独的文件中。我们称之为 [eszip](https://github.com/denoland/eszip)。由于其创建是完全静态完成的,因此 Deno Deploy 上的动态导入功能受到限制。 ::: ### 数据 URL [数据 URL] 可以用作传递给动态导入的规范符。 ```ts title="静态数据 URL" // ✅ 在 Deno Deploy 中正常工作 const { val } = await import( "data:text/javascript,export const val = 42;" ); console.log(val); // -> 42 ``` 对于数据 URL,完全动态的数据是支持的。 ```ts title="动态数据 URL" function generateDynamicDataUrl() { const moduleStr = `export const val = ${Math.random()};`; return `data:text/javascript,${moduleStr}`; } // ✅ 在 Deno Deploy 中正常工作 const { val } = await import(generateDynamicDataUrl()); console.log(val); // -> 打印随机值 ``` 将此技术应用于从网络获取的 JavaScript 代码,您甚至可以模拟真正的动态导入: ```js title="external.js" export const name = "external.js"; ``` ```ts title="从获取的源生成的动态数据 URL" import { assert } from "jsr:@std/assert/assert"; const res = await fetch( "https://gist.githubusercontent.com/magurotuna/1cacb136f9fd6b786eb8bbad92c8e6d6/raw/56a96fd0d246fd3feabbeecea6ea1155bdf5f50d/external.js", ); assert(res.ok); const src = await res.text(); const dataUrl = `data:application/javascript,${src}`; // ✅ 在 Deno Deploy 中正常工作 const { name } = await import(dataUrl); console.log(`来自 ${name} 的问候`); // -> "来自 external.js 的问候" ``` 然而,请注意传递给 `import` 的数据 URL 必须是 JavaScript;如果传递 TypeScript,将在运行时抛出 [TypeError]。 [动态导入]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import [eszip]: https://github.com/denoland/eszip [数据 URL]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs [TypeError]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError --- # API 参考 URL: https://docs.deno.com/deploy/classic/api/ :::info 旧版本文档 您正在查看 Deno Deploy Classic 的旧版本文档。我们建议您迁移至新的 Deno Deploy 平台。 ::: 这是 Deno Deploy Classic 上可用的运行时 API 参考。此 API 与标准 [runtime API](/runtime/manual/runtime) 非常相似,但由于 Deno Deploy Classic 是无服务器环境, 某些 API 并不以相同的方式提供。 请使用文档的这一部分来探索 Deno Deploy 上可用的 API。 ### Web APIs - [`console`](https://developer.mozilla.org/zh-CN/docs/Web/API/console) - [`atob`](https://developer.mozilla.org/zh-CN/docs/Web/API/WindowOrWorkerGlobalScope/atob) - [`btoa`](https://developer.mozilla.org/zh-CN/docs/Web/API/WindowOrWorkerGlobalScope/btoa) - [Fetch API](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API) - `fetch` - `Request` - `Response` - `URL` - `File` - `Blob` - [TextEncoder](https://developer.mozilla.org/zh-CN/docs/Web/API/TextEncoder) - [TextDecoder](https://developer.mozilla.org/zh-CN/docs/Web/API/TextDecoder) - [TextEncoderStream](https://developer.mozilla.org/zh-CN/docs/Web/API/TextEncoderStream) - [TextDecoderStream](https://developer.mozilla.org/zh-CN/docs/Web/API/TextDecoderStream) - [Performance](https://developer.mozilla.org/zh-CN/docs/Web/API/Performance) - [Web Crypto API](https://developer.mozilla.org/zh-CN/docs/Web/API/Crypto) - `randomUUID()` - `getRandomValues()` - [SubtleCrypto](https://developer.mozilla.org/zh-CN/docs/Web/API/SubtleCrypto) - [WebSocket API](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket) - [Timers](https://developer.mozilla.org/zh-CN/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) (`setTimeout`, `clearTimeout`, 和 `setInterval`) - [Streams API](https://developer.mozilla.org/zh-CN/docs/Web/API/Streams_API) - `ReadableStream` - `WritableStream` - `TransformStream` - [URLPattern API](https://developer.mozilla.org/zh-CN/docs/Web/API/URLPattern) - [Import Maps](https://docs.deno.com/runtime/manual/basics/import_maps/) - 注意:`import maps` 目前仅可以通过 [deployctl](https://github.com/denoland/deployctl) 或 [deployctl GitHub Action](https://github.com/denoland/deployctl/blob/main/action/README.md) 工作流使用。 ### Deno APIs > 注意:仅提供 Deno 的稳定 API 在 Deploy 中。 - [`Deno.env`](https://docs.deno.com/api/deno/~/Deno.env) - 与 环境变量(机密)进行交互。 - `get(key: string): string | undefined` - 获取环境变量的值。 - `toObject(): { [key: string]: string }` - 将所有环境变量作为对象获取。 - [`Deno.connect`](https://docs.deno.com/api/deno/~/Deno.connect) - 连接到 TCP 套接字。 - [`Deno.connectTls`](https://docs.deno.com/api/deno/~/Deno.connectTls) - 使用 TLS 连接到 TCP 套接字。 - [`Deno.startTls`](https://docs.deno.com/api/deno/~/Deno.startTls) - 从现有的 TCP 连接开始 TLS 握手。 - [`Deno.resolveDns`](https://docs.deno.com/api/deno/~/Deno.resolveDns) - 进行 DNS 查询。 - 文件系统 API - [`Deno.cwd`](https://docs.deno.com/api/deno/~/Deno.cwd) - 获取当前 工作目录。 - [`Deno.readDir`](https://docs.deno.com/api/deno/~/Deno.readDir) - 获取 目录列表。 - [`Deno.readFile`](https://docs.deno.com/api/deno/~/Deno.readFile) - 读取一个 文件到内存中。 - [`Deno.readTextFile`](https://docs.deno.com/api/deno/~/Deno.readTextFile) - 读取一个文本文件到内存中。 - [`Deno.open`](https://docs.deno.com/api/deno/~/Deno.open) - 打开一个文件以 进行流式读取。 - [`Deno.stat`](https://docs.deno.com/api/deno/~/Deno.stat) - 获取文件系统 条目信息。 - [`Deno.lstat`](https://docs.deno.com/api/deno/~/Deno.lstat) - 不跟随符号链接, 获取文件系统条目信息。 - [`Deno.realPath`](https://docs.deno.com/api/deno/~/Deno.realPath) - 在解析符号链接后获取 文件的真实路径。 - [`Deno.readLink`](https://docs.deno.com/api/deno/~/Deno.readLink) - 获取给定符号链接的 目标路径。 ## 未来支持 在未来,这些 API 也将被添加: - [Cache API](https://developer.mozilla.org/zh-CN/docs/Web/API/Cache) - UDP API: - `Deno.connectDatagram` 用于出站的 UDP 套接字。 - 使用 `Deno.createHttpClient` 来定制 `fetch` 选项。 ## 限制 与 Deno CLI 一样,我们不按 ECMA Script 附录 B 中的规定实现 `__proto__` 对象字段。 --- # BroadcastChannel URL: https://docs.deno.com/deploy/classic/api/runtime-broadcast-channel :::info 旧版文档 您正在查看 Deno Deploy Classic 的旧版文档。我们建议您迁移到新的 Deno Deploy 平台。 ::: 在 Deno Deploy Classic 中,代码在全球不同的数据中心运行,以减少延迟,通过在离客户端最近的数据中心处理请求。在浏览器中,[`BroadcastChannel`](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API) API 允许相同源的不同标签页交换消息。在 Deno Deploy 中,BroadcastChannel API 为各个实例之间提供了一种通信机制;这是一个简单的消息总线,连接全球各地的 Deploy 实例。 ## 构造函数 `BroadcastChannel()` 构造函数创建一个新的 `BroadcastChannel` 实例,并连接到(或创建)提供的通道。 ```ts let channel = new BroadcastChannel(channelName); ``` #### 参数 | 名称 | 类型 | 描述 | | ----------- | -------- | ------------------------------------------------------------ | | channelName | `string` | 用于基础广播通道连接的名称。 | 构造函数的返回类型是一个 `BroadcastChannel` 实例。 ## 属性 | 名称 | 类型 | 描述 | | ---------------- | ---------------------- | ----------------------------------------------------------------------------------------------------- | | `name` | `string` | 基础广播通道的名称。 | | `onmessage` | `function` (或 `null`) | 当通道接收到新消息时执行的函数 ([`MessageEvent`][messageevent])。 | | `onmessageerror` | `function` (或 `null`) | 当到达的消息无法反序列化为 JavaScript 数据结构时执行的函数。 | ## 方法 | 名称 | 描述 | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `close()` | 关闭与基础通道的连接。关闭后,您将无法再向通道发送消息。 | | `postMessage(message)` | 向基础通道发送消息。消息可以是字符串、对象字面量、数字或任何类型的 [`Object`][object]。 | `BroadcastChannel` 继承自 [`EventTarget`][eventtarget],这使您可以在 `BroadcastChannel` 实例上使用 `EventTarget` 的方法,如 `addEventListener` 和 `removeEventListener`。 ## 示例:跨实例更新内存缓存 像 `BroadcastChannel` 这样启用的消息总线的一个用例是,在跨网络不同数据中心运行的隔离体之间更新数据的内存缓存。在下面的示例中,我们展示了如何配置一个简单的服务器,使用 `BroadcastChannel` 在所有运行的服务器实例之间同步状态。 ```ts import { Hono } from "jsr:@hono/hono"; // 内存消息缓存 const messages = []; // 所有隔离体使用的 BroadcastChannel const channel = new BroadcastChannel("all_messages"); // 当其他实例发送新消息时,将其加入缓存 channel.onmessage = (event: MessageEvent) => { messages.push(event.data); }; // 创建一个服务器以添加和获取消息 const app = new Hono(); // 将消息添加到列表中 app.get("/send", (c) => { // 新消息通过包含 "message" 查询参数添加 const message = c.req.query("message"); if (message) { messages.push(message); channel.postMessage(message); } return c.redirect("/"); }); // 获取消息列表 app.get("/", (c) => { // 返回当前消息列表 return c.json(messages); }); Deno.serve(app.fetch); ``` 您可以使用 [这个游乐场](https://dash.deno.com/playground/broadcast-channel-example) 在 Deno Deploy Classic 上自行测试此示例。 [eventtarget]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget [messageevent]: https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent [object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object --- # HTTP 请求 (fetch) URL: https://docs.deno.com/deploy/classic/api/runtime-fetch :::info 旧文档说明 您正在查看 Deno Deploy Classic 的旧文档。我们推荐您迁移到新的 Deno Deploy 平台。 ::: [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) 允许您在 Deno Deploy Classic 中发起外部 HTTP 请求。它是一个 Web 标准,并且包含以下接口: - `fetch()` - 允许您发起外部 HTTP 请求的方法 - [`Request`](./runtime-request) - 表示 fetch() 的请求资源 - [`Response`](./runtime-response) - 表示 fetch() 的响应资源 - [`Headers`](./runtime-headers) - 表示请求和响应的 HTTP 头部。 本页面展示了 fetch() 方法的使用示例。您可以点击上面的其他接口以了解更多信息。 Fetch 还支持从文件 URL 获取静态文件。有关静态文件的更多信息,请参阅 [文件系统 API 文档](./runtime-fs)。 ## `fetch()` `fetch()` 方法初始化对提供资源的网络请求,并返回一个在响应可用后解析的 Promise。 ```ts function fetch( resource: Request | string, init?: RequestInit, ): Promise; ``` #### 参数 | 名称 | 类型 | 可选 | 描述 | | -------- | ------------------------------------------------------------- | ---- | ------------------------------------------------------------------ | | resource | [`Request`](./runtime-request)
[`USVString`][usvstring] | `false` | 资源可以是请求对象或 URL 字符串。 | | init | [`RequestInit`](./runtime-request#requestinit) | `true` | init 对象允许您为请求应用可选参数。 | `fetch()` 的返回类型是一个解析为 [`Response`](./runtime-response) 的 Promise。 ## 示例 下面这个 Deno Deploy Classic 脚本会针对每个传入请求发起对 GitHub API 的 `fetch()` 请求, 然后从处理函数返回该响应。 ```ts async function handler(req: Request): Promise { const resp = await fetch("https://api.github.com/users/denoland", { // 这里的 init 对象包含一个 headers 对象,其中包含指示我们接受的响应类型的头部。 // 我们没有指定 method 字段,因为默认情况下 fetch 发起的是 GET 请求。 headers: { accept: "application/json", }, }); return new Response(resp.body, { status: resp.status, headers: { "content-type": "application/json", }, }); } Deno.serve(handler); ``` [usvstring]: https://developer.mozilla.org/en-US/docs/Web/API/USVString --- # 文件系统 API URL: https://docs.deno.com/deploy/classic/api/runtime-fs :::info 旧版文档 您正在查看 Deno Deploy Classic 的旧版文档。我们建议 迁移到新的 Deno Deploy 平台。 ::: Deno Deploy Classic 支持Deno中的有限文件系统 API 集。这些文件系统 API 可以访问您部署中的静态文件。静态文件例如: - 如果您通过 GitHub 集成进行部署,则为您的 GitHub 存储库中的文件。 - 在 playground 部署中的入口文件。 可用的 API 有: - [Deno.cwd](#deno.cwd) - [Deno.readDir](#deno.readdir) - [Deno.readFile](#deno.readfile) - [Deno.readTextFile](#deno.readtextfile) - [Deno.open](#deno.open) - [Deno.stat](#deno.stat) - [Deno.lstat](#deno.lstat) - [Deno.realPath](#deno.realpath) - [Deno.readLink](#deno.readlink) ## Deno.cwd `Deno.cwd()` 返回您部署的当前工作目录。它位于您部署根目录的根部。例如,如果您通过 GitHub 集成进行部署,则当前工作目录是您 GitHub 存储库的根部。 ## Deno.readDir `Deno.readDir()` 允许您列出目录的内容。 该函数与 [Deno](https://docs.deno.com/api/deno/~/Deno.readDir) 完全兼容。 ```ts function Deno.readDir(path: string | URL): AsyncIterable ``` 路径可以是相对路径或绝对路径。它也可以是 `file:` URL。 ### 示例 此示例列出目录的内容,并将此列表作为 JSON 对象返回到响应正文中。 ```js async function handler(_req) { // 列出位于存储库根目录中的 `blog` 目录中的文章。 const posts = []; for await (const post of Deno.readDir(`./blog`)) { posts.push(post); } // 返回 JSON。 return new Response(JSON.stringify(posts, null, 2), { headers: { "content-type": "application/json", }, }); } Deno.serve(handler); ``` ## Deno.readFile `Deno.readFile()` 允许您将文件完全读入内存。 该函数的定义与 [Deno](https://docs.deno.com/api/deno/~/Deno.readFile) 类似,但目前不支持 [`ReadFileOptions`](https://docs.deno.com/api/deno/~/Deno.ReadFileOptions)。未来将添加支持。 ```ts function Deno.readFile(path: string | URL): Promise ``` 路径可以是相对路径或绝对路径。它也可以是 `file:` URL。 ### 示例 此示例将文件内容读入内存作为字节数组,然后将其作为响应正文返回。 ```js async function handler(_req) { // 让我们读取位于存储库根目录中的 README.md 文件,以探索可用的方法。 // 相对路径是相对于存储库的根部 const readmeRelative = await Deno.readFile("./README.md"); // 绝对路径。 // 存储库的内容可以在 Deno.cwd() 下获得。 const readmeAbsolute = await Deno.readFile(`${Deno.cwd()}/README.md`); // 文件 URL 也受到支持。 const readmeFileUrl = await Deno.readFile( new URL(`file://${Deno.cwd()}/README.md`), ); // 将 Uint8Array 解码为字符串。 const readme = new TextDecoder().decode(readmeRelative); return new Response(readme); } Deno.serve(handler); ``` > 注意:要使用此功能,您必须将 GitHub 存储库链接到您的项目。 Deno Deploy Classic 支持 `Deno.readFile` API 从文件系统中读取静态资源。这对于提供图像、样式表和 JavaScript 文件等静态资源非常有用。本指南演示了如何使用此功能。 假设在 GitHub 存储库中有以下文件结构: ```console ├── mod.ts └── style.css ``` `mod.ts` 的内容: ```ts async function handleRequest(request: Request): Promise { const { pathname } = new URL(request.url); // 服务器的工作方式: // 1. 针对特定资产的请求到达。 // 2. 我们从文件系统中读取该资产。 // 3. 我们将资产发送回客户端。 // 检查请求是否是针对 style.css。 if (pathname.startsWith("/style.css")) { // 从文件系统中读取 style.css 文件。 const file = await Deno.readFile("./style.css"); // 用 style.css 文件响应请求。 return new Response(file, { headers: { "content-type": "text/css", }, }); } return new Response( `

示例

`, { headers: { "content-type": "text/html; charset=utf-8", }, }, ); } Deno.serve(handleRequest); ``` 提供给 [`Deno.readFile`](https://docs.deno.com/api/deno/~/Deno.readFile) API 的路径是相对于存储库的根部。您也可以指定绝对路径,前提是它们位于 `Deno.cwd` 内部。 ## Deno.readTextFile 此函数类似于 [Deno.readFile](#Deno.readFile),不同之处在于它将文件内容解码为 UTF-8 字符串。 ```ts function Deno.readTextFile(path: string | URL): Promise ``` ### 示例 此示例将文本文件读入内存,并将内容作为响应正文返回。 ```js async function handler(_req) { const readme = await Deno.readTextFile("./README.md"); return new Response(readme); } Deno.serve(handler); ``` ## Deno.open `Deno.open()` 允许您打开一个文件,返回一个文件句柄。该文件句柄随后可用于读取文件的内容。有关文件句柄上可用方法的信息,请参见 [`Deno.File`](#deno.file)。 该函数的定义与 [Deno](https://docs.deno.com/api/deno/~/Deno.open) 类似,但目前不支持 [`OpenOptions`](https://docs.deno.com/api/deno/~/Deno.OpenOptions)。未来将添加支持。 ```ts function Deno.open(path: string | URL): Promise ``` 路径可以是相对路径或绝对路径。它也可以是 `file:` URL。 ### 示例 此示例打开一个文件,并将内容作为响应主体进行流式传输。 ```js async function handler(_req) { // 打开位于存储库根部的 README.md 文件。 const file = await Deno.open("./README.md"); // 使用 `readable` 属性,这是一个 `ReadableStream`。这将在响应完成发送时自动关闭文件句柄。 return new Response(file.readable); } Deno.serve(handler); ``` :::note 当您按如下所示迭代文件流时,文件描述符将在迭代结束时自动关闭。无需手动关闭文件描述符:`const iterator = fd.readable[Symbol.asyncIterator]();` ::: ## Deno.File `Deno.File` 是通过 [`Deno.open()`](#deno.open) 返回的文件句柄。它可以用于使用 `read()` 方法读取文件的块。可以使用 `close()` 方法关闭文件句柄。 该接口与 [Deno](https://docs.deno.com/api/deno/~/Deno.File) 类似,但不支持写入文件或寻址。对后者的支持将在未来添加。 ```ts class File { readonly rid: number; close(): void; read(p: Uint8Array): Promise; } ``` 路径可以是相对路径或绝对路径。它也可以是 `file:` URL。 ## Deno.File#read() read 方法用于读取文件的一块。它应传递一个缓冲区以读取数据。它返回读取的字节数或 `null`(如果已到达文件末尾)。 ```ts function read(p: Uint8Array): Promise; ``` ### Deno.File#close() close 方法用于关闭文件句柄。关闭句柄将中断所有正在进行的读取。 ```ts function close(): void; ``` ## Deno.stat `Deno.stat()` 读取文件系统条目的元数据。它返回一个 [`Deno.FileInfo`](#fileinfo) 对象。符号链接会被跟随。 该函数的定义与 [Deno](https://docs.deno.com/api/deno/~/Deno.stat) 相同。它不返回修改时间、访问时间或创建时间值。 ```ts function Deno.stat(path: string | URL): Promise ``` 路径可以是相对路径或绝对路径。它也可以是 `file:` URL。 ### 示例 此示例获取文件的大小,并将结果作为响应主体返回。 ```js async function handler(_req) { // 获取位于存储库根部的 README.md 的文件信息。 const info = await Deno.stat("./README.md"); // 获取文件的字节大小。 const size = info.size; return new Response(`README.md 的大小为 ${size} 字节`); } Deno.serve(handler); ``` ## Deno.lstat `Deno.lstat()` 类似于 `Deno.stat()`,但它不跟随符号链接。 该函数的定义与 [Deno](https://docs.deno.com/api/deno/~/Deno.lstat) 相同。它不返回修改时间、访问时间或创建时间值。 ```ts function Deno.lstat(path: string | URL): Promise ``` 路径可以是相对路径或绝对路径。它也可以是 `file:` URL。 ## Deno.FileInfo `Deno.FileInfo` 接口用于表示文件系统条目的元数据。它是由 [`Deno.stat()`](#deno.stat) 和 [`Deno.lstat()`](#deno.lstat) 函数返回的。它可以表示文件、目录或符号链接。 在 Deno Deploy Classic 中,仅支持文件类型和大小属性。 大小属性的行为与 Linux 上相同。 ```ts interface FileInfo { isDirectory: boolean; isFile: boolean; isSymlink: boolean; size: number; } ``` ## Deno.realPath `Deno.realPath()` 返回解析后的绝对路径,经过符号链接的跟随。 该函数的定义与 [Deno](https://docs.deno.com/api/deno/~/Deno.realPath) 相同。 ```ts function Deno.realPath(path: string | URL): Promise ``` 路径可以是相对路径或绝对路径。它也可以是 `file:` URL。 ### 示例 此示例调用 `Deno.realPath()` 获取存储库根部文件的绝对路径。结果作为响应正文返回。 ```js async function handler(_req) { const path = await Deno.realPath("./README.md"); return new Response(`./README.md 的完全解析路径为 ${path}`); } Deno.serve(handler); ``` ## Deno.readLink `Deno.readLink()` 返回符号链接的目标路径。 该函数的定义与 [Deno](https://docs.deno.com/api/deno/~/Deno.readLink) 相同。 ```ts function Deno.readLink(path: string | URL): Promise ``` 路径可以是相对路径或绝对路径。它也可以是 `file:` URL。 ### 示例 此示例调用 `Deno.readLink()` 获取存储库根部文件的绝对路径。结果作为响应正文返回。 ```js async function handler(_req) { const path = await Deno.readLink("./my_symlink"); return new Response(`./my_symlink 的目标路径为 ${path}`); } Deno.serve(handler); ``` --- # HTTP 头 URL: https://docs.deno.com/deploy/classic/api/runtime-headers :::info Legacy Documentation 您正在查看 Deno Deploy Classic 的遗留文档。我们建议您迁移到新的 Deno Deploy 平台。 ::: [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) 接口是 Fetch API 的一部分。它允许您创建和操作 fetch() 请求和响应资源的 HTTP 头。 - [构造函数](#构造函数) - [参数](#参数) - [方法](#方法) - [示例](#示例) ## 构造函数 Header() 构造函数创建一个新的 `Header` 实例。 ```ts let headers = new Headers(init); ``` #### 参数 | 名称 | 类型 | 可选 | 描述 | | ----- | --------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------- | | init | `Headers` / `{ [key: string]: string }` | `true` | init 选项允许你使用现有的 `Headers` 或对象字面量初始化头对象。 | 构造函数的返回类型是 `Headers` 实例。 ## 方法 | 名称 | 描述 | | --------------------------------------- | ------------------------------------------------------------------ | | `append(name: string, value: string)` | 向 Headers 对象追加一个头部(不会覆盖已有的同名头部)。 | | `delete(name: string)` | 从 Headers 对象删除一个头部。 | | `set(name: string, value: string)` | 在 Headers 对象中设置一个新的头部,若存在同名头部则覆盖。 | | `get(name: string)` | 获取 Headers 对象中指定头部的值。 | | `has(name: string)` | 检查 Headers 对象中是否存在指定的头部。 | | `entries()` | 以键值对形式获取所有头部,结果是可迭代的。 | | `keys()` | 获取 Headers 对象中所有键组成的可迭代对象。 | ## 示例 ```ts // 从对象字面量创建一个新的头部对象。 const myHeaders = new Headers({ accept: "application/json", }); // 向头部对象添加一个头部。 myHeaders.append("user-agent", "Deno Deploy Classic"); // 打印头部对象中的所有头部。 for (const [key, value] of myHeaders.entries()) { console.log(key, value); } // 你可以将头部实例传递给 Response 或 Request 构造函数。 const request = new Request("https://api.github.com/users/denoland", { method: "POST", headers: myHeaders, }); ``` --- # Node.js 内置 API URL: https://docs.deno.com/deploy/classic/api/runtime-node :::info 旧版文档 您正在查看 Deno Deploy Classic 的遗留文档。我们建议迁移到新的 Deno Deploy 平台。 ::: Deno Deploy Classic 原生支持通过 `node:` 前缀导入内置 Node.js 模块,例如 `fs`、`path` 和 `http`。这允许您无需修改即可运行最初为 Node.js 编写的代码。 以下是在 Deno Deploy Classic 上运行的 Node.js HTTP 服务器示例: ```js import { createServer } from "node:http"; import process from "node:process"; const server = createServer((req, res) => { const message = `Hello from ${process.env.DENO_REGION} at ${new Date()}`; res.end(message); }); server.listen(8080); ``` _您可以在这里实时查看此示例: https://dash.deno.com/playground/node-specifiers_ 使用 `node:` 前缀时,Deno Deploy Classic 的所有其他功能依然可用。例如,即便使用 Node.js 模块,您仍可以使用 `Deno.env` 访问环境变量。您也可以像往常一样从外部 URL 导入其他 ESM 模块。 以下 Node.js 模块可用: - `assert` - `assert/strict` - `async_hooks` - `buffer` - `child_process` - `cluster` - `console` - `constants` - `crypto` - `dgram` - `diagnostics_channel` - `dns` - `dns/promises` - `domain` - `events` - `fs` - `fs/promises` - `http` - `http2` - `https` - `module` - `net` - `os` - `path` - `path/posix` - `path/win32` - `perf_hooks` - `process` - `punycode` - `querystring` - `readline` - `stream` - `stream/consumers` - `stream/promises` - `stream/web` - `string_decoder` - `sys` - `timers` - `timers/promises` - `tls` - `tty` - `url` - `util` - `util/types` - `v8` - `vm` - `worker_threads` - `zlib` 这些模块的行为在大多数情况下应与 Node.js 一致。由于 Deno Deploy Classic 的沙箱行为,部分功能不可用: - 使用 `child_process` 执行二进制文件 - 使用 `worker_threads` 生成工作线程 - 使用 `vm` 创建上下文并评估代码 > 注:Node.js 模块的仿真对于大多数用例来说是足够的,但 > 仍然不完美。如果您遇到任何问题,请 > [提出问题](https://github.com/denoland/deno)。 --- # HTTP 请求 URL: https://docs.deno.com/deploy/classic/api/runtime-request :::info 旧版文档 您正在查看 Deno Deploy Classic 的旧版文档。我们建议迁移到新的 Deno Deploy 平台。 ::: [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) 接口是 Fetch API 的一部分,表示 fetch() 的请求。 - [构造函数](#constructor) - [参数](#parameters) - [属性](#properties) - [方法](#methods) - [示例](#example) ## 构造函数 Request() 构造函数创建一个新的 Request 实例。 ```ts let request = new Request(resource, init); ``` #### 参数 | 名称 | 类型 | 可选 | 描述 | | -------- | ----------------------------- | -------- | ----------------------------------------------------------------------- | | resource | `Request` 或 `USVString` | `false` | 资源可以是请求对象或 URL 字符串。 | | init | [`RequestInit`](#requestinit) | `true` | init 对象允许您设置应用于请求的可选参数。 | 返回类型是一个 `Request` 实例。 ##### `RequestInit` | 名称 | 类型 | 默认值 | 描述 | | ---------------------------- | --------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------- | | [`method`][method] | `string` | `GET` | 请求的方法。 | | [`headers`][headers] | `Headers` 或 `{ [key: string]: string }` | 无 | 请求的头部。 | | [`body`][body] | `Blob`、`BufferSource`、`FormData`、`URLSearchParams`、`USVString` 或 `ReadableStream` | 无 | 请求的主体。 | | [`cache`][cache] | `string` | 无 | 请求的缓存模式。 | | [`credentials`][credentials] | `string` | `same-origin` | 请求的凭据模式。 | | [`integrity`][integrity] | `string` | 无 | 请求主体的加密哈希。 | | [`mode`][mode] | `string` | `cors` | 您想使用的请求模式。 | | [`redirect`][redirect] | `string` | `follow` | 处理重定向的模式。 | | [`referrer`][referrer] | `string` | `about:client` | 一个 `USVString`,指定 `no-referrer`、`client` 或一个 URL。 | ## 属性 | 名称 | 类型 | 描述 | | ---------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | [`cache`][cache] | `string` | 缓存模式指示浏览器如何缓存请求(`default`、`no-cache` 等)。 | | [`credentials`][credentials] | `string` | 凭据(`omit`、`same-origin` 等)指示用户代理是否在请求的 CORs 情况下发送 cookies。 | | [`destination`][destination] | [`RequestDestination`][requestdestination] | 字符串指示所请求内容的类型。 | | [`body`][body] | [`ReadableStream`][readablestream] | getter 提供请求主体内容的 `ReadableStream`。 | | [`bodyUsed`][bodyused] | `boolean` | 指示主体内容是否已被读取。 | | [`url`][url] | `USVString` | 请求的 URL。 | | [`headers`][headers] | [`Headers`](runtime-headers) | 与请求相关联的头部。 | | [`integrity`][integrity] | `string` | 请求主体的加密哈希。 | | [`method`][method] | `string` | 请求的方法(`POST`、`GET` 等)。 | | [`mode`][mode] | `string` | 指示请求的模式(例如 `cors`)。 | | [`redirect`][redirect] | `string` | 处理重定向的模式。 | | [`referrer`][referrer] | `string` | 请求的引荐来源。 | | [`referrerPolicy`][referrerpolicy] | `string` | 请求的引荐政策。 | 上述所有属性都是只读的。 ## 方法 | 名称 | 描述 | | ------------------------------ | ----------------------------------------------------------------------------------------- | | [`arrayBuffer()`][arraybuffer] | 读取主体流直到完成并返回一个 `ArrayBuffer` 对象。 | | [`blob()`][blob] | 读取主体流直到完成并返回一个 `Blob` 对象。 | | [`formData()`][formdata] | 读取主体流直到完成并返回一个 `FormData` 对象。 | | [`json()`][json] | 读取主体流直到完成,将其解析为 JSON 并返回一个 JavaScript 对象。 | | [`text()`][text] | 读取主体流直到完成并返回一个 USVString 对象(文本)。 | | [`clone()`][clone] | 克隆请求对象。 | ## 示例 ```ts function handler(_req) { // 创建一个 POST 请求 const request = new Request("https://post.deno.dev", { method: "POST", body: JSON.stringify({ message: "Hello world!", }), headers: { "content-type": "application/json", }, }); console.log(request.method); // POST console.log(request.headers.get("content-type")); // application/json return fetch(request); } Deno.serve(handler); ``` [cache]: https://developer.mozilla.org/en-US/docs/Web/API/Request/cache [credentials]: https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials [destination]: https://developer.mozilla.org/en-us/docs/web/api/request/destination [requestdestination]: https://developer.mozilla.org/en-US/docs/Web/API/RequestDestination [body]: https://developer.mozilla.org/en-US/docs/Web/API/Body/body [bodyused]: https://developer.mozilla.org/en-US/docs/Web/API/Body/bodyUsed [url]: https://developer.mozilla.org/en-US/docs/Web/API/Request/url [headers]: https://developer.mozilla.org/en-US/docs/Web/API/Request/headers [method]: https://developer.mozilla.org/en-US/docs/Web/API/Request/method [integrity]: https://developer.mozilla.org/en-US/docs/Web/API/Request/integrity [mode]: https://developer.mozilla.org/en-US/docs/Web/API/Request/mode [redirect]: https://developer.mozilla.org/en-US/docs/Web/API/Request/redirect [referrer]: https://developer.mozilla.org/en-US/docs/Web/API/Request/referrer [referrerpolicy]: https://developer.mozilla.org/en-US/docs/Web/API/Request/referrerpolicy [readablestream]: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream [arraybuffer]: https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer [blob]: https://developer.mozilla.org/en-US/docs/Web/API/Body/blob [json]: https://developer.mozilla.org/en-US/docs/Web/API/Body/json [text]: https://developer.mozilla.org/en-US/docs/Web/API/Body/text [formdata]: https://developer.mozilla.org/en-US/docs/Web/API/Body/formdata [clone]: https://developer.mozilla.org/en-US/docs/Web/API/Request/clone --- # HTTP 响应 URL: https://docs.deno.com/deploy/classic/api/runtime-response :::info 旧版本文档 您正在查看 Deno Deploy Classic 的旧版本文档。我们建议 迁移到新的 Deno Deploy 平台。 ::: [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) 接口是 Fetch API 的一部分,表示 fetch() 的响应资源。 - [构造函数](#构造函数) - [参数](#参数) - [属性](#属性) - [方法](#方法) - [示例](#示例) ## 构造函数 Response() 构造函数创建一个新的 Response 实例。 ```ts let response = new Response(body, init); ``` #### 参数 | 名称 | 类型 | 可选 | 描述 | | ---- | --------------------------------------------------------------------------------------- | ---- | ---------------------------------------------------------------------- | | body | `Blob`,`BufferSource`,`FormData`,`ReadableStream`,`URLSearchParams` 或 `USVString` | `true` | 响应的主体。默认值为 `null`。 | | init | `ResponseInit` | `true` | 可选对象,允许设置响应的状态和头部信息。 | 返回类型为一个 `Response` 实例。 ##### `ResponseInit` | 名称 | 类型 | 可选 | 描述 | | ------------ | ----------------------------------------------------- | ---- | --------------------------------------------------------- | | `status` | `number` | `true` | 响应的状态码。 | | `statusText` | `string` | `true` | 表示状态码的状态信息。 | | `headers` | `Headers` 或 `string[][]` 或 `Record` | `false` | 响应的 HTTP 头信息。 | ## 属性 | 名称 | 类型 | 只读 | 描述 | | -------------------------- | ---------------- | ---- | ----------------------------------------------------------- | | [`body`][body] | `ReadableStream` | `true` | getter 返回主体内容的 `ReadableStream`。 | | [`bodyUsed`][bodyused] | `boolean` | `true` | 表示主体内容是否已被读取。 | | [`url`][url] | `USVString` | `true` | 响应的 URL。 | | [`headers`][headers] | `Headers` | `true` | 与响应相关的头部信息。 | | [`ok`][ok] | `boolean` | `true` | 表示响应是否成功(状态码在 200-299 之间)。 | | [`redirected`][redirected] | `boolean` | `true` | 表示响应是否是重定向的结果。 | | [`status`][status] | `number` | `true` | 响应的状态码。 | | [`statusText`][statustext] | `string` | `true` | 响应的状态信息。 | | [`type`][type] | `string` | `true` | 响应的类型。 | ## 方法 | 名称 | 描述 | | ---------------------------------------------------- | --------------------------------------------------------------------------------------- | | [`arrayBuffer()`][arraybuffer] | 读取主体流直到完成并返回一个 `ArrayBuffer` 对象。 | | [`blob()`][blob] | 读取主体流直到完成并返回一个 `Blob` 对象。 | | [`formData()`][formdata] | 读取主体流直到完成并返回一个 `FormData` 对象。 | | [`json()`][json] | 读取主体流直到完成,将其解析为 JSON 并返回一个 JavaScript 对象。 | | [`text()`][text] | 读取主体流直到完成并返回一个 USVString 对象(文本)。 | | [`clone()`][clone] | 克隆响应对象。 | | [`error()`][error] | 返回一个与网络错误相关的新响应对象。 | | [`redirect(url: string, status?: number)`][redirect] | 创建一个新的响应,重定向到提供的 URL。 | ## 示例 ```ts function handler(_req) { // 创建一个以 HTML 为主体的响应。 const response = new Response(" Hello ", { status: 200, headers: { "content-type": "text/html", }, }); console.log(response.status); // 200 console.log(response.headers.get("content-type")); // text/html return response; } Deno.serve(handler); ``` [clone]: https://developer.mozilla.org/zh-CN/docs/Web/API/Response/clone [error]: https://developer.mozilla.org/zh-CN/docs/Web/API/Response/error [redirect]: https://developer.mozilla.org/zh-CN/docs/Web/API/Response/redirect [body]: https://developer.mozilla.org/zh-CN/docs/Web/API/Body/body [bodyused]: https://developer.mozilla.org/zh-CN/docs/Web/API/Body/bodyUsed [url]: https://developer.mozilla.org/zh-CN/docs/Web/API/Request/url [headers]: https://developer.mozilla.org/zh-CN/docs/Web/API/Request/headers [ok]: https://developer.mozilla.org/zh-CN/docs/Web/API/Response/ok [redirected]: https://developer.mozilla.org/zh-CN/docs/Web/API/Response/redirected [status]: https://developer.mozilla.org/zh-CN/docs/Web/API/Response/status [statustext]: https://developer.mozilla.org/zh-CN/docs/Web/API/Response/statusText [type]: https://developer.mozilla.org/zh-CN/docs/Web/API/Response/type [method]: https://developer.mozilla.org/zh-CN/docs/Web/API/Request/method [readablestream]: https://developer.mozilla.org/zh-CN/docs/Web/API/ReadableStream [arraybuffer]: https://developer.mozilla.org/zh-CN/docs/Web/API/Body/arrayBuffer [blob]: https://developer.mozilla.org/zh-CN/docs/Web/API/Body/blob [json]: https://developer.mozilla.org/zh-CN/docs/Web/API/Body/json [text]: https://developer.mozilla.org/zh-CN/docs/Web/API/Body/text [formdata]: https://developer.mozilla.org/zh-CN/docs/Web/API/Body/formdata --- # TCP 套接字和 TLS URL: https://docs.deno.com/deploy/classic/api/runtime-sockets :::info 旧文档 您正在查看 Deno Deploy Classic 的旧版文档。我们建议 迁移到新的 Deno Deploy 平台。 ::: Deno Deploy Classic 支持出站 TCP 和 TLS 连接。这些 API 允许 您在 Deploy 中使用诸如 PostgreSQL、SQLite、MongoDB 等数据库。 需要了解如何服务 TCP 的相关信息?请查阅 [`Deno.serve`](/api/deno/~/Deno.serve) 的文档,其中包括对 [TCP 选项](/api/deno/~/Deno.ServeTcpOptions) 的支持说明。 ## `Deno.connect` 建立出站 TCP 连接。 函数定义与 [Deno](https://docs.deno.com/api/deno/~/Deno.connect) 相同,唯一的限制是 `transport` 选项只能为 `tcp`,并且 `hostname` 不能是 localhost 或为空。 ```ts function Deno.connect(options: ConnectOptions): Promise ``` ### 示例 ```js async function handler(_req) { // 与 example.com 建立 TCP 连接 const connection = await Deno.connect({ port: 80, hostname: "example.com", }); // 发送原始 HTTP GET 请求。 const request = new TextEncoder().encode( "GET / HTTP/1.1\nHost: example.com\r\n\r\n", ); const _bytesWritten = await connection.write(request); // 从连接中读取 15 字节。 const buffer = new Uint8Array(15); await connection.read(buffer); connection.close(); // 将字节作为纯文本返回。 return new Response(buffer, { headers: { "content-type": "text/plain;charset=utf-8", }, }); } Deno.serve(handler); ``` ## `Deno.connectTls` 建立出站 TLS 连接。 函数定义与 [Deno](https://docs.deno.com/api/deno/~/Deno.connectTls) 相同,唯一的限制是 `hostname` 不能是 localhost 或为空。 ```ts function Deno.connectTls(options: ConnectTlsOptions): Promise ``` ### 示例 ```js async function handler(_req) { // 与 example.com 建立 TLS 连接 const connection = await Deno.connectTls({ port: 443, hostname: "example.com", }); // 发送原始 HTTP GET 请求。 const request = new TextEncoder().encode( "GET / HTTP/1.1\nHost: example.com\r\n\r\n", ); const _bytesWritten = await connection.write(request); // 从连接中读取 15 字节。 const buffer = new Uint8Array(15); await connection.read(buffer); connection.close(); // 将字节作为纯文本返回。 return new Response(buffer, { headers: { "content-type": "text/plain;charset=utf-8", }, }); } Deno.serve(handler); ``` --- # CI 和 GitHub Actions URL: https://docs.deno.com/deploy/classic/ci_github :::info 旧版文档说明 您正在查看 Deno Deploy Classic 的旧版文档。我们建议迁移至新的 Deno Deploy 平台。 ::: Deno Deploy 的 Git 集成功能允许部署推送到 GitHub 仓库的代码更改。在生产分支上的提交将作为生产部署进行部署。所有其他分支上的提交将作为预览部署进行部署。 Git 集成有两种操作模式: - **自动**:Deno Deploy Classic 会在每次你推送时自动从你的仓库源拉取代码和资产,并进行部署。此模式非常快速,但不允许进行构建步骤。_这是大多数用户推荐的模式。_ - **GitHub Actions**:在此模式下,你可以从 GitHub Actions 工作流将代码和资产推送到 Deno Deploy。这允许你在部署之前执行构建步骤。 根据你的自定义部署配置,Deno Deploy 会选择合适的模式。下面,我们将更详细地介绍 **自动** 和 **GitHub Actions** 模式的不同配置。 ## 自动 如果你的项目不需要任何额外的构建步骤,则系统会选择 **自动** 模式。入口文件仅仅是 Deno Deploy 将运行的文件。 ## GitHub Actions 如果在 **项目配置** 的 **安装步骤** 和/或 **构建步骤** 中输入了命令,Deno Deploy 将创建一个必要的 GitHub Actions 工作流文件并推送到你的仓库。在这个工作流文件中,我们利用 `deployctl` [Github action][deploy-action] 来部署你的项目。在将其部署到 Deno Deploy 之前,你可以执行任何所需的操作,例如运行构建命令。 要配置你想要运行的预处理命令,点击选择你的 git 仓库后出现的 **显示高级选项** 按钮。然后根据需要在输入框中输入相应的值。 :::tip 例如,如果你想为 Fresh 项目启用 [提前构建],你将在 **构建步骤** 框中输入 `deno task build`。 另请参阅 [Fresh 文档][Deploy to production],了解如何将 Fresh 项目部署到 Deno Deploy。 ::: Deno Deploy 生成并推送到你的仓库的 GitHub Actions 工作流文件如下所示。 ```yml title=".github/workflows/deploy.yml" name: Deploy on: push: branches: main pull_request: branches: main jobs: deploy: name: Deploy runs-on: ubuntu-latest permissions: id-token: write # Deno Deploy 认证所需 contents: read # 克隆仓库所需 steps: - name: Clone repository uses: actions/checkout@v4 - name: Install Deno uses: denoland/setup-deno@v2 with: deno-version: v2.x - name: Build step run: "deno task build" - name: Upload to Deno Deploy uses: denoland/deployctl@v1 with: project: "" entrypoint: "main.ts" root: "." ``` 有关更多详细信息,请参见 [deployctl README](https://github.com/denoland/deployctl/blob/main/action/README.md)。 [fileserver]: https://jsr.io/@std/http#file-server [ghapp]: https://github.com/apps/deno-deploy [deploy-action]: https://github.com/denoland/deployctl/blob/main/action/README.md [ahead-of-time builds]: https://fresh.deno.dev/docs/concepts/ahead-of-time-builds [Deploy to production]: https://fresh.deno.dev/docs/getting-started/deploy-to-production --- # 调度 cron 任务 URL: https://docs.deno.com/deploy/classic/cron :::info 旧版文档 您正在查看 Deno Deploy Classic 的旧版文档。我们建议您迁移到新的 Deno Deploy 平台。 ::: [`Deno.cron`](https://docs.deno.com/api/deno/~/Deno.cron) 接口使您能够配置以可配置的时间表执行的 JavaScript 或 TypeScript 代码,使用 [cron 语法](https://en.wikipedia.org/wiki/Cron)。在下面的示例中,我们配置一段每分钟执行一次的 JavaScript 代码。 ```ts Deno.cron("记录消息", "* * * * *", () => { console.log("每分钟打印一次。"); }); ``` 也可以使用 JavaScript 对象来定义 cron 调度。在下面的示例中,我们配置一段每小时执行一次的 JavaScript 代码。 ```ts Deno.cron("记录消息", { hour: { every: 1 } }, () => { console.log("每小时打印一次。"); }); ``` `Deno.cron` 接受三个参数: - 一个可读的人类名称,描述 cron 任务 - 一个 cron 调度字符串或定义调度的 JavaScript 对象,指定 cron 任务的运行时间 - 一个在给定调度上执行的函数 如果您是 cron 语法的新手,有一些第三方模块可帮助您生成 cron 调度字符串 [像这个](https://www.npmjs.com/package/cron-time-generator)。 ## 重试失败的运行 失败的 cron 调用会自动按照默认重试策略进行重试。如果您想指定自定义重试策略,可以使用 `backoffSchedule` 属性来指定一个等待时间数组(以毫秒为单位),该数组用于在再次重试函数调用之前等待。在以下示例中,我们将尝试重试失败的回调三次——第一次等待一秒,第二次等待五秒,然后等待十秒。 ```ts Deno.cron("重试示例", "* * * * *", { backoffSchedule: [1000, 5000, 10000], }, () => { throw new Error("Deno.cron 将重试这三次,但没有成功!"); }); ``` ## 设计和限制 在使用 `Deno.cron` 时,请注意以下一些设计细节和限制。 ### 任务必须在顶层模块作用域中定义 [`Deno.cron`](https://docs.deno.com/api/deno/~/Deno.cron) 接口旨在支持基于预定义调度的静态 cron 任务定义。所有 `Deno.cron` 任务必须在模块的顶层定义。任何嵌套的 `Deno.cron` 定义(例如在 [`Deno.serve`](https://docs.deno.com/api/deno/~/Deno.serve) 处理程序内)将导致错误或被忽略。 如果您需要在 Deno 程序执行期间动态调度任务,您可以使用 [Deno Queues](/deploy/classic/queues/) API。 ### 时区 `Deno.cron` 调度使用 UTC 时区指定。这有助于避免因观察夏令时而造成的时区问题。 ### 重叠执行 下一个计划的 cron 任务调用可能会与先前的调用重叠。如果发生这种情况,`Deno.cron` 将跳过下一个计划调用,以避免重叠执行。 ### 星期几数字表示 `Deno.cron` 不使用基于 0 的星期几数字表示。相反,它使用 1-7(或 SUN-SAT)来表示从星期日到星期六。这可能与其他使用 0-6 表示法的 cron 引擎不同。 ## 在 Deno Deploy 上的使用 通过 [Deno Deploy](https://deno.com/deploy),您可以在云中的 V8 隔离环境中运行您的后台任务。在这样做时,有一些注意事项要考虑。 ### 与 Deno CLI 的不同 像其他 Deno 运行时内置功能(如队列和 Deno KV)一样,`Deno.cron` 的实现在线上 Deno Deploy 中稍有不同。 #### 默认情况下 cron 的工作原理 Deno 运行时中的 `Deno.cron` 实现将执行状态保存在内存中。如果您运行多个使用 `Deno.cron` 的 Deno 程序,每个程序将有自己独立的 cron 任务集。 #### Deno Deploy 上的cron工作原理 Deno Deploy 提供了一个无服务器实现的 `Deno.cron`,旨在实现高可用性和扩展性。Deno Deploy 会在部署时自动提取您的 `Deno.cron` 定义,并使用按需隔离进行任务调度。您最新的生产部署定义了安排执行的活动 cron 任务集。要添加、删除或修改 cron 任务,只需修改代码并创建一个新的生产部署。 Deno Deploy 保证您的 cron 任务在每个计划的时间间隔内至少执行一次。这通常意味着您的 cron 处理程序每次按照计划的时间调用一次。在某些故障场景下,处理程序可能会因同一调度时间被多次调用。 ### Cron 仪表板 当您进行包含 cron 任务的生产部署时,可以在您项目的 [Deploy 仪表板](https://dash.deno.com/projects) 中的 `Cron` 选项卡下查看所有 cron 任务的列表。 ![Deno 仪表板中 cron 任务的列表](./images/cron-tasks.png) ### 定价 `Deno.cron` 调用的收费与针对您的部署的入站 HTTP 请求相同。有关定价的更多信息,请访问 [这里](https://deno.com/deploy/pricing)。 ### 特定于部署的限制 - `Deno.cron` 仅适用于生产部署(不适用于预览部署) - 您的 `Deno.cron` 处理程序的确切调用时间可能与计划时间相差最多一分钟 ## Cron 配置示例 以下是一些常见的 cron 配置,供您参考。 ```ts title="每分钟运行一次" Deno.cron("每分钟运行一次", "* * * * *", () => { console.log("你好,cron!"); }); ``` ```ts title="每十五分钟运行一次" Deno.cron("每十五分钟运行一次", "*/15 * * * *", () => { console.log("你好,cron!"); }); ``` ```ts title="每小时整点运行一次" Deno.cron("每小时整点运行一次", "0 * * * *", () => { console.log("你好,cron!"); }); ``` ```ts title="每三小时运行一次" Deno.cron("每三小时运行一次", "0 */3 * * *", () => { console.log("你好,cron!"); }); ``` ```ts title="每天凌晨 1 点运行一次" Deno.cron("每天凌晨 1 点运行一次", "0 1 * * *", () => { console.log("你好,cron!"); }); ``` ```ts title="每周三午夜运行一次" Deno.cron("每周三午夜运行一次", "0 0 * * WED", () => { console.log("你好,cron!"); }); ``` ```ts title="每月第一天午夜运行一次" Deno.cron("每月第一天午夜运行一次", "0 0 1 * *", () => { console.log("你好,cron!"); }); ``` --- # 自定义域名 URL: https://docs.deno.com/deploy/classic/custom-domains :::info 旧版文档 您正在查看 Deno Deploy Classic 的旧版文档。我们建议您迁移至新的 Deno Deploy 平台。 ::: 默认情况下,项目可以通过其预览 URL 访问,即 `$PROJECT_ID.deno.dev`,例如 `dead-clam-55.deno.dev`。您也可以通过以下说明添加自定义域名。 ## **步骤 1:** 在 Deno Deploy Classic 控制台添加自定义域名 1. 在项目页面点击“设置”按钮,然后从侧边栏选择“域名”。 2. 输入您希望添加到项目中的域名,并点击“添加”。请注意,您必须拥有想要添加到项目的域名。如果您尚未拥有域名,可以在 Google Domains、Namecheap 或 gandi.net 等域名注册商处注册。 3. 该域名会被添加到域名列表中,并带有“设置”徽章。 4. 点击“设置”徽章进入域名设置页面,页面将显示需要为您的域名创建/更新的 DNS 记录列表。 ## **步骤 2:** 更新您的自定义域名的 DNS 记录 前往您的域名注册商的 DNS 配置面板(或您用于管理 DNS 的服务),并按照域名设置页面上的说明输入记录。 ## **步骤 3:** 验证 DNS 记录是否已更新 返回 Deno Deploy Classic 控制台,点击域名设置页面上的 **验证** 按钮。它会检查 DNS 记录是否正确设置,如果正确,状态将更新为“已验证,等待证书配置”。 ## **步骤 4:** 为您的自定义域名配置证书 此时,您有两个选择。99% 的情况下,您应该选择第一个选项。 1. 让我们自动配置一个证书,使用 Let's Encrypt。 点击 **获取自动证书** 按钮。配置 TLS 证书可能需要一分钟左右。如果您的域名指定了阻止 [Let's Encrypt](https://letsencrypt.org/) 颁发证书的 CAA 记录,配置可能会失败。证书将在到期前约 30 天自动续期。当证书成功颁发后,您将看到一个绿色勾选标记。 2. 手动上传证书和私钥。 若要手动上传证书链和私钥,请点击 **上传您自己的证书** 按钮。系统会提示您上传完整有效的证书链和私钥,您的叶证书需要位于链的顶部。 --- # 在命令行中使用 deployctl URL: https://docs.deno.com/deploy/classic/deployctl :::info Legacy Documentation 你正在查看 Deno Deploy Classic 的遗留文档。我们建议迁移到新的 Deno Deploy 平台。 ::: `deployctl` 是一个命令行工具 (CLI),允许你在不离开终端的情况下操作 Deno Deploy 平台。使用它,你可以部署代码、创建和管理项目及其部署,并监控使用情况和日志。 ## 依赖项 `deployctl` 的唯一依赖是 Deno 运行时。你可以通过运行以下命令进行安装: ```sh curl -fsSL https://deno.land/install.sh | sh ``` 你无需在部署你的第一个项目之前先设置 Deno Deploy Classic 账户。账户将在你部署第一个项目的过程中自动创建。 ## 安装 `deployctl` 安装 Deno 运行时后,你可以使用以下命令安装 `deployctl` 工具: ```sh deno install -gArf jsr:@deno/deployctl ``` 在 deno install 命令中,`-A` 选项赋予已安装脚本所有权限。你可以选择不使用它,在这种情况下,当工具执行时,会提示你授予必要的权限。 ## 部署 要对你的代码进行新部署,请导航到项目的根目录并执行: ```shell deployctl deploy ``` ### 项目和入口点 如果这是项目的第一次部署,`deployctl` 将根据其所在的 Git 仓库或目录推断项目名称。同样,它会通过查找具有常见入口点名称的文件(如 `main.ts`、`src/main.ts` 等)来推断入口点。在首次部署之后,使用的设置将存储在配置文件中(默认是 deno.json)。 你可以分别使用 `--project` 和 `--entrypoint` 参数指定项目名称和/或入口点。如果项目不存在,将自动创建。默认情况下,它是在用户的个人组织中创建,但也可以通过指定 `--org` 参数在自定义组织中创建。如果组织尚不存在,也会自动创建。 ```shell deployctl deploy --project=helloworld --entrypoint=src/entrypoint.ts --org=my-team ``` ### 包含和排除文件 默认情况下,deployctl 会递归部署当前目录下的所有文件(除了 `node_modules` 目录)。你可以使用 `--include` 和 `--exclude` 参数自定义此行为(这些参数在配置文件中也支持)。这些参数接受特定文件、整个目录和通配符。以下是一些示例: - 仅包括源文件和静态文件: ```shell deployctl deploy --include=./src --include=./static ``` - 仅包括 Typescript 文件: ```shell deployctl deploy --include=**/*.ts ``` - 排除本地工具和工件 ```shell deployctl deploy --exclude=./tools --exclude=./benches ``` 一个常见的陷阱是未包括需要运行的源代码模块(入口点和依赖项)。以下示例将失败,因为未包括 `main.ts`: ```shell deployctl deploy --include=./static --entrypoint=./main.ts ``` 入口点也可以是远程脚本。一个常见的用例是使用 `std/http/file_server.ts` 部署一个静态网站(更多细节请参考 [静态网站教程](https://docs.deno.com/deploy/tutorials/static-site)): ```shell deployctl deploy --include=dist --entrypoint=jsr:@std/http/file-server ``` ### 环境变量 你可以使用 `--env` 设置环境变量(设置单个环境变量)或使用 `--env-file` 加载一个或多个环境文件。可以组合这些选项并多次使用: ```shell deployctl deploy --env-file --env-file=.other-env --env=DEPLOYMENT_TS=$(date +%s) ``` 部署将通过 `Deno.env.get()` 获取这些变量。请注意,使用 `--env` 和 `--env-file` 设置的环境变量特定于正在创建的部署,不会添加到 [为项目配置的环境变量列表](./environment-variables.md)。 ### 生产部署 你创建的每个部署都有一个唯一的 URL。此外,项目有一个“生产 URL”和指向其“生产”部署的自定义域名。部署可以随时晋升为生产,或者使用 `--prod` 标志直接创建为生产: ```shell deployctl deploy --prod ``` 在 [部署](./deployments) 文档中了解有关生产部署的更多信息。 ## 部署命令组 部署子命令将所有与部署相关的操作分组。 ### 列出 你可以列出一个项目的部署: ```shell deployctl deployments list ``` 输出: ``` ✔ 项目 'my-project' 的部署列表第 1 页已经准备好 ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ 部署 │ 日期 │ 状态 │ 数据库 │ 域 │ 入口点 │ 分支 │ 提交 │ ├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ kcbxc4xwe4mc │ 2024年12月3日 13:21:40 CET (2天) │ 预览 │ 预览 │ https://my-project-kcbxc4xwe4mc.deno.dev │ main.ts │ main │ 4b6c506 │ │ c0ph5xa9exb3 │ 2024年12月3日 13:21:25 CET (2天) │ 生产 │ 生产 │ https://my-project-c0ph5xa9exb3.deno.dev │ main.ts │ main │ 4b6c506 │ │ kwkbev9er4h2 │ 2024年12月3日 13:21:12 CET (2天) │ 预览 │ 预览 │ https://my-project-kwkbev9er4h2.deno.dev │ main.ts │ main │ 4b6c506 │ │ dxseq0jc8402 │ 2024年6月3日 23:16:51 CET (8天) │ 预览 │ 生产 │ https://my-project-dxseq0jc8402.deno.dev │ main.ts │ main │ 099359b │ │ 7xr5thz8yjbz │ 2024年6月3日 22:58:32 CET (8天) │ 预览 │ 预览 │ https://my-project-7xr5thz8yjbz.deno.dev │ main.ts │ another │ a4d2953 │ │ 4qr4h5ac3rfn │ 2024年6月3日 22:57:05 CET (8天) │ 失败 │ 预览 │ n/a │ main.ts │ another │ 56d2c88 │ │ 25wryhcqmb9q │ 2024年6月3日 22:56:41 CET (8天) │ 预览 │ 预览 │ https://my-project-25wryhcqmb9q.deno.dev │ main.ts │ another │ 4b6c506 │ │ 64tbrn8jre9n │ 2024年6月3日 8:21:33 CET (8天) │ 预览 │ 生产 │ https://my-project-64tbrn8jre9n.deno.dev │ main.ts │ main │ 4b6c506 │ │ hgqgccnmzg04 │ 2024年6月3日 8:17:40 CET (8天) │ 失败 │ 生产 │ n/a │ main.ts │ main │ 8071902 │ │ rxkh1w3g74e8 │ 2024年6月3日 8:17:28 CET (8天) │ 失败 │ 生产 │ n/a │ main.ts │ main │ b142a59 │ │ wx6cw9aya64c │ 2024年6月3日 8:02:29 CET (8天) │ 预览 │ 生产 │ https://my-project-wx6cw9aya64c.deno.dev │ main.ts │ main │ b803784 │ │ a1qh5fmew2yf │ 2024年5月3日 16:25:29 CET (9天) │ 预览 │ 生产 │ https://my-project-a1qh5fmew2yf.deno.dev │ main.ts │ main │ 4bb1f0f │ │ w6pf4r0rrdkb │ 2024年5月3日 16:07:35 CET (9天) │ 预览 │ 生产 │ https://my-project-w6pf4r0rrdkb.deno.dev │ main.ts │ main │ 6e487fc │ │ nn700gexgdzq │ 2024年5月3日 13:37:11 CET (9天) │ 预览 │ 生产 │ https://my-project-nn700gexgdzq.deno.dev │ main.ts │ main │ c5b1d1f │ │ 98crfqxa6vvf │ 2024年5月3日 13:33:52 CET (9天) │ 预览 │ 生产 │ https://my-project-98crfqxa6vvf.deno.dev │ main.ts │ main │ 090146e │ │ xcdcs014yc5p │ 2024年5月3日 13:30:58 CET (9天) │ 预览 │ 生产 │ https://my-project-xcdcs014yc5p.deno.dev │ main.ts │ main │ 5b78c0f │ │ btw43kx89ws1 │ 2024年5月3日 13:27:31 CET (9天) │ 预览 │ 生产 │ https://my-project-btw43kx89ws1.deno.dev │ main.ts │ main │ 663452a │ │ 62tg1ketkjx7 │ 2024年5月3日 13:27:03 CET (9天) │ 预览 │ 生产 │ https://my-project-62tg1ketkjx7.deno.dev │ main.ts │ main │ 24d1618 │ │ 07ag6pt6kjex │ 2024年5月3日 13:19:11 CET (9天) │ 预览 │ 生产 │ https://my-project-07ag6pt6kjex.deno.dev │ main.ts │ main │ 4944545 │ │ 4msyne1rvwj1 │ 2024年5月3日 13:17:16 CET (9天) │ 预览 │ 生产 │ https://my-project-4msyne1rvwj1.deno.dev │ main.ts │ main │ dda85e1 │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 按回车获取下一页 [Enter] ``` 该命令默认输出每页 20 个部署。你可以使用回车键迭代查看页面,并使用 `--page` 和 `--limit` 选项查询特定页面和页面大小。 与其余命令一样,如果你不在项目目录中或想列出其他项目的部署,可以使用 `--project` 选项指定项目。 ### 显示 使用以下命令获取特定部署的所有详细信息: ```shell deployctl deployments show ``` 输出: ``` ✔ 项目 'my-project' 的生产部署为 'c0ph5xa9exb3' ✔ 部署 'c0ph5xa9exb3' 的详细信息已准备好: c0ph5xa9exb3 ------------ 状态: 生产 日期: 2天前,12小时,29分钟,46秒 (2024年12月3日 13:21:25 CET) 项目: my-project (e54f23b5-828d-4b7f-af12-706d4591062b) 组织: my-team (d97822ac-ee20-4ce9-b942-5389330b57ee) 域名: https://my-project.deno.dev https://my-project-c0ph5xa9exb3.deno.dev 数据库: 生产 (0efa985f-3793-48bc-8c05-f740ffab4ca0) 入口点: main.ts 环境变量: HOME Git 引用: main [4b6c506] 消息: change name 作者: John Doe @johndoe [mailto:johndoe@deno.com] URL: https://github.com/arnauorriols/my-project/commit/4b6c50629ceeeb86601347732d01dc7ed63bf34f 定时任务: 另一个定时任务 [*/10 * * * *] 于 2024年3月15日 1:50:00 CET 成功,耗时 2 秒 (下一个在 2024年3月15日 2:00:00 CET) 最新定时任务 [*/10 * * * *] n/a 又一个定时任务 [*/10 * * * *] 于 2024年3月15日 1:40:00 CET 失败,耗时 2 秒 (下一个在 2024年3月15日 1:51:54 CET) ``` 如果未指定部署,命令将显示当前项目的生产部署的详细信息。要查看最后一次部署的详细信息,请使用 `--last`,要查看特定部署的详细信息,请使用 `--id`(或位置参数)。你还可以使用 `--next` 或 `--prev` 按时间顺序浏览部署。 例如,要查看倒数第二次部署的详细信息,你可以这样做: ```shell deployctl deployments show --last --prev ``` 要查看特定部署后 2 次部署的详细信息: ```shell deployctl deployments show 64tbrn8jre9n --next=2 ``` ### 重新部署 重新部署命令创建一个新的部署,重用现有部署的构建,目的是更改与之关联的资源。这包括生产域、环境变量和 KV 数据库。 :::info 选择要重新部署的部署的语义与 [show 子命令](#显示) 相同,包括 `--last`、`--id`、`--next` 和 `--prev`。 ::: #### 生产域 如果你想将项目的生产域路由到特定部署,可以使用 `--prod` 选项重新部署它: ```shell deployctl deployments redeploy --prod 64tbrn8jre9n ``` 这将创建一个新的部署,具有与指定部署相同的代码和环境变量,但项目的生产域名将指向它。对于具有预览/生产数据库的项目(即链接到 GitHub 的项目),这也将为新的部署设置生产数据库。 :::note 此功能类似于 Deno Deploy Web 应用程序中找到的“推广到生产”按钮,唯一的区别是“推广到生产”按钮不创建新的部署。相反,“推广到生产”按钮在原地更改域名路由,但仅限于已使用生产数据库的部署。 ::: #### KV 数据库 如果这是一个 GitHub 部署,它将拥有 2 个数据库,一个用于生产部署,一个用于预览部署。你可以通过使用 `--db` 选项重新部署来更改部署的数据库: ```shell deployctl deployments redeploy --db=prod --id=64tbrn8jre9n ``` :::note 将部署重新部署到生产时,默认情况下,它将自动配置为使用生产数据库。你可以同时使用 `--prod` 和 `--db` 选项以选择不采用此行为。例如,以下命令将重新部署当前的生产部署(由于缺少位置参数,`--id` 或 `--last`)。新的部署将成为新的生产部署,但将使用预览数据库而不是生产数据库: ```shell deployctl deployments redeploy --prod --db=preview ``` ::: 如果你的组织有自定义数据库,你也可以通过 UUID 设置它们: ```shell deployctl deployments redeploy --last --db=5261e096-f9aa-4b72-8440-1c2b5b553def ``` #### 环境变量 创建部署时,它会继承项目的环境变量。由于部署是不可变的,因此永远不能更改其环境变量。要在部署中设置新的环境变量,你需要使用 `--env`(设置单个变量)和 `--env-file`(加载一个或多个环境文件)重新部署它。 以下命令使用 `.env` 和 `.other-env` 文件中定义的环境变量重新部署当前的生产部署,并将 `DEPLOYMENT_TS` 变量设置为当前时间戳。生成的部署将是一个预览部署(即生产域将不路由流量到它,因为缺少 `--prod`)。 ```shell deployctl deployments redeploy --env-file --env-file=.other-env --env=DEPLOYMENT_TS=$(date +%s) ``` :::note 请注意,在更改环境变量时,仅在重新部署命令中设置的环境变量将被新部署使用。项目环境变量和正在重新部署的部署的环境变量将被忽略。如果这不符合你的需求,请在 https://github.com/denoland/deploy_feedback/issues/ 报告你的反馈。 ::: :::note 当你在 Deno Deploy Web 应用程序中更改项目环境变量时,当前的生产部署将使用新的环境变量重新部署,并且新部署将成为新的生产部署。 ::: ### 删除 你可以使用 `delete` 子命令删除一个部署: ```shell deployctl deployments delete 64tbrn8jre9n ``` 与 `show` 和 `redeploy` 一样,`delete` 也可以使用 `--last`、`--next` 和 `--prev` 选择要删除的部署。以下是删除项目中所有部署(除了最后一个)的示例命令(使用时请谨慎!): ```shell while deployctl deployments delete --project=my-project --last --prev; do :; done ``` ## 项目 `projects` 子命令将所有与项目整体相关的操作分组。这包括 `list`、`show`、`rename`、`create` 和 `delete`。 ### 列出 `deployctl projects list` 输出你用户可以访问的所有项目,按组织分组: ``` 个人组织: blog url-shortener 'my-team' 组织: admin-site main-site analytics ``` 你可以使用 `--org` 通过组织进行过滤: ```shell deployctl projects list --org=my-team ``` ### 显示 要查看特定项目的详细信息,请使用 `projects show`。如果你在项目内,它将从配置文件中获取项目 ID。你也可以使用 `--project` 或位置参数指定项目: ```shell deployctl projects show main-site ``` 输出: ``` main-site --------- 组织: my-team (5261e096-f9aa-4b72-8440-1c2b5b553def) 域名: https://my-team.com https://main-site.deno.dev 仪表盘 URL: https://dash.deno.com/projects/8422c515-f68f-49b2-89f3-157f4b144611 代码库: https://github.com/my-team/main-site 数据库: [main] dd28e63e-f495-416b-909a-183380e3a232 [*] e061c76e-4445-409a-bc36-a1a9040c83b3 定时任务: 另一个定时任务 [*/10 * * * *] 于 2024年3月12日 14:40:00 CET 成功,耗时 2 秒 (下一个在 2024年3月12日 14:50:00 CET) 最新定时任务 [*/10 * * * *] n/a 又一个定时任务 [*/10 * * * *] 于 2024年3月12日 14:40:00 CET 失败,耗时 2 秒 (下一个在 2024年3月12日 14:50:00 CET) 部署: kcbxc4xwe4mc c0ph5xa9exb3* kwkbev9er4h2 dxseq0jc8402 7xr5thz8yjbz 4qr4h5ac3rfn 25wryhcqmb9q 64tbrn8jre9n hgqgccnmzg04 rxkh1w3g74e8 wx6cw9aya64c a1qh5fmew2yf w6pf4r0rrdkb nn700gexgdzq 98crfqxa6vvf xcdcs014yc5p btw43kx89ws1 62tg1ketkjx7 07ag6pt6kjex 4msyne1rvwj1 ``` ### 重命名 项目可以通过 `rename` 子命令轻松重命名。与其他命令类似,如果你在项目的目录中运行命令,则无需指定项目的当前名称: ```shell deployctl projects rename my-personal-blog ``` 输出: ``` ℹ 使用配置文件 '/private/tmp/blog/deno.json' ✔ 找到项目 'blog' (8422c515-f68f-49b2-89f3-157f4b144611) ✔ 项目 'blog' 重命名为 'my-personal-blog' ``` :::note 请记住,项目名称是预览域名的一部分 (https://my-personal-blog-kcbxc4xwe4mc.deno.dev) 和默认生产域名 (https://my-personal-blog.deno.dev)。因此,在更改项目名称时,之前名称的 URL 将不再路由到项目的相应部署。 ::: ### 创建 你可以创建一个空项目: ```shell deployctl projects create my-new-project ``` ### 删除 你可以删除一个项目: ```shell deployctl projects delete my-new-project ``` ## 监控资源使用情况 `top` 子命令用于实时监控项目的资源使用情况: ```shell deployctl top ``` 输出: ``` ┌────────┬────────────────┬────────────────────────┬─────────┬───────┬─────────┬──────────┬─────────────┬────────────┬─────────┬─────────┬───────────┬───────────┐ │ (idx) │ 部署 │ 区域 │ 每分钟请求 │ CPU% │ 每请求 CPU │ RSS/5分钟 │ 每分钟流入 │ 每分钟流出 │ KV 读/分钟 │ KV 写/分钟 │ 队列入队/分钟 │ 队列出队/分钟 │ ├────────┼────────────────┼────────────────────────┼─────────┼───────┼─────────┼──────────┼─────────────┼────────────┼─────────┼─────────┼───────────┼───────────┤ │ 6b80e8 │ "kcbxc4xwe4mc" │ "亚洲-东北1" │ 80 │ 0.61 │ 4.56 │ 165.908 │ 11.657 │ 490.847 │ 0 │ 0 │ 0 │ 0 │ │ 08312f │ "kcbxc4xwe4mc" │ "亚洲-东北1" │ 76 │ 3.49 │ 27.58 │ 186.278 │ 19.041 │ 3195.288 │ 0 │ 0 │ 0 │ 0 │ │ 77c10b │ "kcbxc4xwe4mc" │ "亚洲-南部1" │ 28 │ 0.13 │ 2.86 │ 166.806 │ 7.354 │ 111.478 │ 0 │ 0 │ 0 │ 0 │ │ 15e356 │ "kcbxc4xwe4mc" │ "亚洲-南部1" │ 66 │ 0.97 │ 8.93 │ 162.288 │ 17.56 │ 4538.371 │ 0 │ 0 │ 0 │ 0 │ │ a06817 │ "kcbxc4xwe4mc" │ "亚洲-东南部1" │ 126 │ 0.44 │ 2.11 │ 140.087 │ 16.504 │ 968.794 │ 0 │ 0 │ 0 │ 0 │ │ d012b6 │ "kcbxc4xwe4mc" │ "亚洲-东南部1" │ 119 │ 2.32 │ 11.72 │ 193.704 │ 23.44 │ 8359.829 │ 0 │ 0 │ 0 │ 0 │ │ 7d9a3d │ "kcbxc4xwe4mc" │ "澳大利亚-东南部1" │ 8 │ 0.97 │ 75 │ 158.872 │ 10.538 │ 3.027 │ 0 │ 0 │ 0 │ 0 │ │ 3c21be │ "kcbxc4xwe4mc" │ "澳大利亚-东南部1" │ 1 │ 0.04 │ 90 │ 105.292 │ 0.08 │ 1.642 │ 0 │ 0 │ 0 │ 0 │ │ b75dc7 │ "kcbxc4xwe4mc" │ "欧洲-西部2" │ 461 │ 5.43 │ 7.08 │ 200.573 │ 63.842 │ 9832.936 │ 0 │ 0 │ 0 │ 0 │ │ 33607e │ "kcbxc4xwe4mc" │ "欧洲-西部2" │ 35 │ 0.21 │ 3.69 │ 141.98 │ 9.438 │ 275.788 │ 0 │ 0 │ 0 │ 0 │ │ 9be3d2 │ "kcbxc4xwe4mc" │ "欧洲-西部2" │ 132 │ 0.92 │ 4.19 │ 180.654 │ 15.959 │ 820.513 │ 0 │ 0 │ 0 │ 0 │ │ 33a859 │ "kcbxc4xwe4mc" │ "欧洲-西部3" │ 1335 │ 7.57 │ 3.4 │ 172.032 │ 178.064 │ 10967.918 │ 0 │ 0 │ 0 │ 0 │ │ 3f54ce │ "kcbxc4xwe4mc" │ "欧洲-西部4" │ 683 │ 4.76 │ 4.19 │ 187.802 │ 74.696 │ 7565.017 │ 0 │ 0 │ 0 │ 0 │ │ cf881c │ "kcbxc4xwe4mc" │ "欧洲-西部4" │ 743 │ 3.95 │ 3.19 │ 177.213 │ 86.974 │ 6087.454 │ 0 │ 0 │ 0 │ 0 │ │ b4565b │ "kcbxc4xwe4mc" │ "美洲-西部1" │ 3 │ 0.21 │ 55 │ 155.46 │ 2.181 │ 0.622 │ 0 │ 0 │ 0 │ 0 │ │ b97970 │ "kcbxc4xwe4mc" │ "南美-东部1" │ 3 │ 0.08 │ 25 │ 186.049 │ 1.938 │ 0.555 │ 0 │ 0 │ 0 │ 0 │ │ fd7a08 │ "kcbxc4xwe4mc" │ "美国-东部4" │ 3 │ 0.32 │ 80 │ 201.101 │ 0.975 │ 58.495 │ 0 │ 0 │ 0 │ 0 │ │ 95d68a │ "kcbxc4xwe4mc" │ "美国-东部4" │ 133 │ 1.05 │ 4.77 │ 166.052 │ 28.107 │ 651.737 │ 0 │ 0 │ 0 │ 0 │ │ c473e7 │ "kcbxc4xwe4mc" │ "美国-东部4" │ 0 │ 0 │ 0 │ 174.154 │ 0.021 │ 0 │ 0 │ 0 │ 0 │ 0 │ │ ebabfb │ "kcbxc4xwe4mc" │ "美国-东部4" │ 19 │ 0.15 │ 4.78 │ 115.732 │ 7.764 │ 67.054 │ 0 │ 0 │ 0 │ 0 │ │ eac700 │ "kcbxc4xwe4mc" │ "美国-南部1" │ 114 │ 2.37 │ 12.54 │ 183.001 │ 18.401 │ 22417.397 │ 0 │ 0 │ 0 │ 0 │ │ cd2194 │ "kcbxc4xwe4mc" │ "美国-南部1" │ 35 │ 0.33 │ 5.68 │ 145.871 │ 8.142 │ 91.236 │ 0 │ 0 │ 0 │ 0 │ │ 140fec │ "kcbxc4xwe4mc" │ "美国-西部2" │ 110 │ 1.43 │ 7.84 │ 115.298 │ 18.093 │ 977.993 │ 0 │ 0 │ 0 │ 0 │ │ 51689f │ "kcbxc4xwe4mc" │ "美国-西部2" │ 1105 │ 7.66 │ 4.16 │ 187.277 │ 154.876 │ 14648.383 │ 0 │ 0 │ 0 │ 0 │ │ c5806e │ "kcbxc4xwe4mc" │ "美国-西部2" │ 620 │ 4.38 │ 4.24 │ 192.291 │ 109.086 │ 9685.688 │ 0 │ 0 │ 0 │ 0 │ └────────┴────────────────┴────────────────────────┴─────────┴───────┴─────────┴──────────┴─────────────┴────────────┴─────────┴─────────┴───────────┴───────────┘ ⠼ 正在流媒体... ``` 列的定义如下: | 列 | 描述 | | ----------- | ---------------------------------------------------------------------------------------------- | | idx | 实例区分符。用于区分在同一区域运行的不同执行的不透明 ID。 | | 部署 | 正在执行的实例中运行的部署的 ID。 | | 每分钟请求 | 项目每分钟接收的请求数。 | | CPU% | 项目使用的 CPU 百分比。 | | 每请求 CPU | 每个请求的 CPU 时间,以毫秒为单位。 | | RSS/5分钟 | 项目在最近 5 分钟内使用的最大 RSS,单位为 MB。 | | 每分钟流入 | 项目每分钟接收的数据,单位为 KB。 | | 每分钟流出 | 项目每分钟输出的数据,单位为 KB。 | | KV 读/分钟 | 项目每分钟执行的 KV 读取。 | | KV 写/分钟 | 项目每分钟执行的 KV 写入。 | | 队列入队/分钟 | 项目每分钟执行的队列入队。 | | 队列出队/分钟 | 项目每分钟执行的队列出队。 | 你可以使用 `--region` 按区域过滤,该选项接受子字符串并可以多次使用: ```shell deployctl top --region=asia --region=southamerica ``` ## 日志 你可以使用 `deployctl logs` 获取你的部署的日志。它支持实时日志(日志生成时流式传输到控制台)和查询已保存的日志(获取过去生成的日志)。 要显示项目当前生产部署的实时日志: ```shell deployctl logs ``` :::note 与 Deno Deploy Web 应用程序不同,目前日志子命令在更改时不会自动切换到新的生产部署。 ::: 要显示特定部署的实时日志: ```shell deployctl logs --deployment=1234567890ab ``` 日志可以使用 `--levels`、`--regions` 和 `--grep` 选项按级别、区域和文本过滤: ```shell deployctl logs --levels=error,info --regions=region1,region2 --grep='unexpected' ``` 要显示已保存的日志,可以使用 `--since` 和/或 `--until` 选项: ```sh deployctl logs --since=$(date -Iseconds -v-2H) --until=$(date -Iseconds -v-30M) ``` ```sh deployctl logs --since=$(date -Iseconds --date='2 hours ago') --until=$(date -Iseconds --date='30 minutes ago') ``` ## API 如果你使用 [子托管 API](../../subhosting/manual/index.md),`deployctl api` 将帮助你与 API 交互,同时处理身份验证和头信息: ```shell deployctl api /projects/my-personal-blog/deployments ``` 使用 `--method` 和 `--body` 指定 HTTP 方法和请求体: ```shell deployctl api --method=POST --body='{"name": "main-site"}' organizations/5261e096-f9aa-4b72-8440-1c2b5b553def/projects ``` ## 本地开发 对于本地开发,你可以使用 `deno` CLI。要安装 `deno`,请遵循 [Deno 手册](https://deno.land/manual/getting_started/installation) 中的说明。 安装完成后,你可以在本地运行你的脚本: ```shell $ deno run --allow-net=:8000 ./main.ts Listening on http://localhost:8000 ``` 要监视文件更改,请添加 `--watch` 标志: ```shell $ deno run --allow-net=:8000 --watch ./main.ts Listening on http://localhost:8000 ``` 有关 Deno CLI 的更多信息,以及如何配置你的开发环境和 IDE,请访问 Deno 手册的 [入门][manual-gs] 部分。 [manual-gs]: https://deno.land/manual/getting_started ## JSON 输出 所有输出数据的命令都有一个 `--format=json` 选项,将数据以 JSON 对象的形式输出。当 stdout 不是 TTY 时,这种输出模式是默认的,尤其是当以管道输入到其他命令时。与 `jq` 一起使用时,这种模式使得对 `deployctl` 提供的所有数据进行编程使用: 获取当前生产部署的 ID: ```shell deployctl deployments show | jq .build.deploymentId ``` 获取每个区域每个隔离的 CPU 时间流的 csv: ```shell deployctl top | jq -r '[.id,.region,.cpuTimePerRequest] | @csv' ``` --- # 部署 URL: https://docs.deno.com/deploy/classic/deployments :::info Legacy Documentation 您正在查看 Deno Deploy Classic 的遗留文档。我们推荐迁移到新的 Deno Deploy 平台。 ::: 部署是运行应用程序所需的代码和环境变量的快照。可以通过 [使用 `deployctl`](./deployctl.md#deploy) 或者在配置后通过 Deploy 的 GitHub 集成自动创建新的部署。 部署在创建后是不可变的。要为应用程序部署新版本的代码,必须创建新的部署。一旦创建,部署将持续可访问。 所有可用的部署在您的项目页面的 `Deployments` 标签下列出,如下图所示。旧部署可以通过 [使用 `deployctl`](./deployctl.md#delete) 和 [通过 API](https://apidocs.deno.com/#delete-/deployments/-deploymentId-) 删除。 ![显示项目仪表板中的部署标签](./images/project_deployments.png) ## 自定义域名 还可以有其他 URL 指向一个部署,比如 [自定义域名](custom-domains)。 ## 分支域名 `.deno.dev` 也受到支持。 ## 生产部署与预览部署 所有部署都有一个预览 URL,可以用于查看该特定的部署。预览 URL 的格式为 `{project_name}-{deployment_id}.deno.dev`。 ![图像](../docs-images/preview_deployment.png) 部署可以是生产部署或预览部署。这些部署在运行时功能上没有任何区别。唯一的区别是项目的生产部署将接收来自项目 URL(例如 `myproject.deno.dev`)和自定义域名的流量,以及指向部署的预览 URL 的流量。 ## 通过 Deno Deploy UI 将预览部署提升为生产部署 可以通过 Deno Deploy UI 将预览部署“提升”为生产: 1. 导航到项目页面。 2. 点击 **Deployments** 标签。 3. 点击您想提升为生产的部署旁边的三个点,并选择 **Promote to Production** ![promote_to_production](../docs-images/promote_to_production.png) 将部署提升为生产是有限制的,仅限于已使用生产 KV 数据库的部署。这对于使用不同数据库进行预览和生产部署的 GitHub 部署尤其相关。部署(即使使用预览 KV 数据库的部署)始终可以使用 [the `deployctl deployments redeploy` 命令](./deployctl.md#production-domains) 重新部署到生产。 ## 通过 `deployctl` 创建生产部署 如果您使用 `deployctl` 部署 Deno 代码,可以使用 `--prod` 标志直接部署到生产: ```sh deployctl deploy --prod --project=helloworld main.ts ``` --- # 连接到 DynamoDB URL: https://docs.deno.com/deploy/classic/dynamodb :::info Legacy Documentation 您正在查看 Deno Deploy Classic 的旧文档。我们推荐迁移到新的 Deno Deploy 平台。 ::: 亚马逊 DynamoDB 是一个完全托管的 NoSQL 数据库。要将数据持久化到 DynamoDB,请按照以下步骤操作: 本教程假设您拥有 AWS 和 Deno Deploy Classic 帐户。 ## 从 DynamoDB 收集凭证 该过程的第一步是生成 AWS 凭证,以便以编程方式访问 DynamoDB。 生成凭证: 1. 访问 https://console.aws.amazon.com/iam/ 并进入“用户”部分。 2. 点击 **添加用户** 按钮,填写 **用户名** 字段(可以使用 `denamo`),并选择 **编程访问** 类型。 3. 点击 **下一步:权限**,然后点击 **直接附加现有策略**,搜索 `AmazonDynamoDBFullAccess` 并选择它。 4. 点击 **下一步:标签**,然后点击 **下一步:审核**,最后点击 **创建用户**。 5. 点击 **下载 .csv** 按钮以下载凭证。 ## 在 Deno Deploy 中创建项目 接下来,让我们在 Deno Deploy Classic 中创建一个项目并设置所需的环境变量: 1. 访问 [https://dash.deno.com/new](https://dash.deno.com/new)(如果尚未登录,请用 GitHub 登录),然后在 **从命令行部署** 下点击 **+ 空项目**。 2. 现在点击项目页面上的 **设置** 按钮。 3. 导航到 **环境变量** 部分并添加以下密钥。 - `AWS_ACCESS_KEY_ID` - 使用下载的 CSV 中 **访问密钥 ID** 列下的值。 - `AWS_SECRET_ACCESS_KEY` - 使用下载的 CSV 中 **秘密访问密钥** 列下的值。 ## 编写代码以连接到 DynamoDB AWS 提供了一个 [官方 SDK](https://www.npmjs.com/package/@aws-sdk/client-dynamodb),它可以在浏览器中使用。由于大多数 Deno Deploy 的 API 与浏览器类似,因此该 SDK 也可以在 Deno Deploy 中使用。要在 Deno 中使用该 SDK,可以从 CDN 导入,如下所示并创建一个客户端: ```js import { DynamoDBClient, GetItemCommand, PutItemCommand, } from "https://esm.sh/@aws-sdk/client-dynamodb?dts"; // 通过提供您的区域信息创建客户端实例。 // 凭证会从我们在 Deno Deploy 的项目创建步骤中设置的环境变量中自动获取,因此我们不需要在此手动传递它们。 const client = new ApiFactory().makeNew(DynamoDB); serve({ "/songs": handleRequest, }); async function handleRequest(request) { // async/await。 try { const data = await client.send(command); // 处理数据。 } catch (error) { // 错误处理。 } finally { // 最终处理。 } } ``` ## 将应用程序部署到 Deno Deploy 一旦您完成了应用程序的编写,就可以在 Deno Deploy Classic 上部署它。 为此,请返回到您的项目页面,地址为 `https://dash.deno.com/projects/`。 您应该会看到几个部署选项: - [Github 集成](ci_github) - [`deployctl`](./deployctl.md) ```sh deployctl deploy --project= ``` 除非您希望添加构建步骤,否则我们建议您选择 GitHub 集成。 有关在 Deno Deploy 上部署的不同方式和不同配置选项的更多详细信息,请阅读 [这里](how-to-deploy)。 --- # 边缘缓存 URL: https://docs.deno.com/deploy/classic/edge_cache [Web Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache) 在 Deno Deploy 上得到支持。此缓存旨在提供微秒级的读取延迟、多GB/s的写入吞吐量以及无限存储,尽管其在一致性和耐久性方面提供的是尽力而为的保障。 ```ts const cache = await caches.open("my-cache"); Deno.serve(async (req) => { const cached = await cache.match(req); if (cached) { return cached; } const res = new Response("cached at " + new Date().toISOString()); await cache.put(req, res.clone()); return res; }); ``` 缓存的数据存储在运行您代码的同一 Deno Deploy 区域。通常,您的隔离进程会在同一区域内观察到读取后写入(RAW)和写入后写入(WAW)的一致性;但是,在极少数情况下,最近的写入可能会丢失、顺序错乱或暂时不可见。 ## 过期 默认情况下,缓存的数据会无限期地保存。虽然我们定期扫描并删除不活跃的对象,但通常一个对象会在缓存中保持至少 30 天。 边缘缓存理解标准的 HTTP 响应头 `Expires` 和 `Cache-Control`。您可以使用它们为每个缓存对象指定过期时间,例如: ``` Expires: Thu, 22 Aug 2024 01:22:31 GMT ``` 或: ``` Cache-Control: max-age=86400 ``` ## 限制 - 如果响应不是从 `Uint8Array` 或 `string` 主体构建的,`Content-Length` 头需要手动设置。 - 目前不支持删除。 --- # 环境变量 URL: https://docs.deno.com/deploy/classic/environment-variables :::info 旧版文档 您正在查看的是 Deno Deploy Classic 的旧版文档。我们推荐 迁移到新的 Deno Deploy 平台。 ::: 环境变量用于存储诸如 web 服务访问令牌之类的值。每个部署在创建时都有一组环境变量,并且可以通过 `Deno.env` API 从代码中访问。定义部署环境变量有两种方式: ## 项目环境变量 您可以在项目级别定义环境变量。当您创建一个部署时,它将获得在 _特定时刻_ 项目已定义的环境变量集。 为了方便起见,当您更改项目的环境变量时,当前的生产部署会 _重新部署_,创建一个新的生产部署,并附带一组新的环境变量。 :::note 部署是不可更改的,包括它们的环境变量。更改项目的环境变量不会更改现有部署的环境变量。 ::: 要向您的项目添加环境变量,请点击项目页面上的 **设置** 按钮,然后从侧边栏中点击 **环境变量**。填写键/值字段,然后点击“添加”将环境变量添加到项目中。 ![environment_variable](../docs-images/fauna2.png) 更新现有环境变量的操作方法相同。点击“添加变量”按钮,输入您希望更新的环境变量的相同名称,并输入新值。点击“保存”按钮以完成更新。 ## 部署环境变量 在使用 `deployctl` 进行部署时,可以通过 [使用 `--env` 或 `--env-file` 标志](./deployctl.md#environment-variables) 来指定环境变量,补充已有的项目环境变量。您还可以传递多个 `--env-file` 参数(例如, `--env-file=.env.one --env-file=.env.two`)以包括来自多个文件的变量。 :::note 当在单个 `.env` 文件中存在相同环境变量的多个声明时,将应用第一个出现的值。但是,如果在多个 `.env` 文件中定义了相同的变量(使用多个 `--env-file` 参数),则最后指定的文件中的值将优先。即在最后列出的 `.env` 文件中的第一个出现的值将被应用。 ::: 这些环境变量将特定于正在创建的部署。 ### 默认环境变量 每个部署都有以下预设环境变量,您可以从代码中访问它们。 1. `DENO_REGION` 它包含部署运行区域的区域代码。您可以使用此变量提供区域特定的内容。 您可以在 [区域页面](regions) 中查找区域代码。 1. `DENO_DEPLOYMENT_ID` 它保存部署的 ID。 --- # 连接到 Firebase URL: https://docs.deno.com/deploy/classic/firebase :::info Legacy Documentation 您正在查看 Deno Deploy Classic 的旧文档。我们推荐您迁移到新的 Deno Deploy 平台。 ::: Firebase 是 Google 开发的一个平台,用于创建移动和网络应用程序。它的功能包括用于登录的身份验证原语和一个 NoSQL 数据存储库 Firestore,您可以将数据持久化到其中。 本教程介绍了如何从部署在 Deno Deploy 上的应用程序连接到 Firebase。 您可以在 [这里](../tutorials/tutorial-firebase) 找到一个更全面的教程,该教程基于 Firebase 构建了一个示例应用程序。 ## 从 Firebase 获取凭据 > 本教程假设您已经在 Firebase 中创建了一个项目并将网页应用程序添加到您的项目中。 1. 在 Firebase 中导航到您的项目,然后单击 **项目设置** 2. 向下滚动,直到您看到一张包含您的应用名称的卡片,以及一个包含 `firebaseConfig` 对象的代码示例。它应该看起来像下面的内容。请将其保留备用。我们稍后会用到它: ```js var firebaseConfig = { apiKey: "APIKEY", authDomain: "example-12345.firebaseapp.com", projectId: "example-12345", storageBucket: "example-12345.appspot.com", messagingSenderId: "1234567890", appId: "APPID", }; ``` ## 在 Deno Deploy 中创建项目 1. 访问 [https://dash.deno.com/new](https://dash.deno.com/new) (如果您尚未登录,请使用 GitHub 登录),然后在 **从命令行部署** 下单击 **+ 空项目**。 2. 现在单击项目页面上可用的 **设置** 按钮。 3. 导航至 **环境变量** 部分并添加以下内容:
FIREBASE_USERNAME
上述添加的 Firebase 用户(电子邮件地址)。
FIREBASE_PASSWORD
上述添加的 Firebase 用户密码。
FIREBASE_CONFIG
Firebase 应用程序的配置,格式为 JSON 字符串。
配置需要是有效的 JSON 字符串,以便应用程序可以读取。如果在设置时给出的代码片段如下所示: ```js var firebaseConfig = { apiKey: "APIKEY", authDomain: "example-12345.firebaseapp.com", projectId: "example-12345", storageBucket: "example-12345.appspot.com", messagingSenderId: "1234567890", appId: "APPID", }; ``` 您需要将字符串的值设置为如下(注意空格和换行不是必需的): ```json { "apiKey": "APIKEY", "authDomain": "example-12345.firebaseapp.com", "projectId": "example-12345", "storageBucket": "example-12345.appspot.com", "messagingSenderId": "1234567890", "appId": "APPID" } ``` ## 编写连接到 Firebase 的代码 我们要做的第一件事是导入 Firebase 在 Deploy 下运行所需的 `XMLHttpRequest` polyfill 以及一个用于 `localStorage` 的 polyfill,以允许 Firebase 身份验证保持登录用户: ```js import "https://deno.land/x/xhr@0.1.1/mod.ts"; import { installGlobals } from "https://deno.land/x/virtualstorage@0.1.0/mod.ts"; installGlobals(); ``` > ℹ️ 我们在撰写本教程时使用的是当前版本的包。它们可能不是最新的,您可能想要仔细检查当前版本。 由于 Deploy 具有许多网络标准 API,因此最好在 Deploy 下使用 Firebase 的 Web 库。目前 Firebase 的 v9 仍在测试阶段,所以我们将使用 v8: ```js import firebase from "https://esm.sh/firebase@9.17.0/app"; import "https://esm.sh/firebase@9.17.0/auth"; import "https://esm.sh/firebase@9.17.0/firestore"; ``` 现在我们需要设置我们的 Firebase 应用程序。我们将从我们之前设置的环境变量中获取配置,并获取我们将要使用的 Firebase 部分的引用: ```js const firebaseConfig = JSON.parse(Deno.env.get("FIREBASE_CONFIG")); const firebaseApp = firebase.initializeApp(firebaseConfig, "example"); const auth = firebase.auth(firebaseApp); const db = firebase.firestore(firebaseApp); ``` 好的,我们快完成了。我们只需创建我们的中间件应用程序并添加我们导入的 `localStorage` 中间件: ```js const app = new Application(); app.use(virtualStorage()); ``` 然后我们需要添加中间件来验证用户。在本教程中,我们只是从将要设置的环境变量中获取用户名和密码,但这很容易适应于将用户重定向到登录页面,如果他们没有登录: ```js app.use(async (ctx, next) => { const signedInUid = ctx.cookies.get("LOGGED_IN_UID"); const signedInUser = signedInUid != null ? users.get(signedInUid) : undefined; if (!signedInUid || !signedInUser || !auth.currentUser) { const creds = await auth.signInWithEmailAndPassword( Deno.env.get("FIREBASE_USERNAME"), Deno.env.get("FIREBASE_PASSWORD"), ); const { user } = creds; if (user) { users.set(user.uid, user); ctx.cookies.set("LOGGED_IN_UID", user.uid); } else if (signedInUser && signedInUid.uid !== auth.currentUser?.uid) { await auth.updateCurrentUser(signedInUser); } } return next(); }); ``` ## 将应用程序部署到 Deno Deploy 完成编写应用程序后,您可以将其部署到 Deno Deploy。 为此,请返回到您的项目页面,网址为 `https://dash.deno.com/projects/`。 您应该会看到几种部署选项: - [Github 集成](ci_github) - [`deployctl`](./deployctl.md) ```sh deployctl deploy --project= ``` 除非您想添加构建步骤,否则我们建议您选择 Github 集成。 有关在 Deno Deploy 上部署的不同方式以及不同配置选项的更多详细信息,请阅读 [这里](how-to-deploy)。 --- # 使用 GitHub 集成进行部署 URL: https://docs.deno.com/deploy/classic/how-to-deploy :::info 旧文档 您正在查看 Deno Deploy Classic 的旧文档。我们推荐迁移到新的 Deno Deploy 平台。 ::: 部署更复杂项目的最简单方法是通过我们的 GitHub 集成。 这允许您将 Deno Deploy Classic 项目链接到一个 GitHub 仓库。 每当您推送到该仓库时,您的更改将会自动部署。 通过 GitHub 集成,您可以添加一个 GitHub Action,该动作在您的部署过程中定义一个构建步骤。 有关更多详细信息,请参见 [GitHub 集成页面](ci_github)。 ### 使用 [`deployctl`](./deployctl.md) 从命令行进行部署 `deployctl` 是一个命令行工具,用于将您的代码部署到 Deno Deploy。使用 `deployctl` 可以控制比上述自动 GitHub 集成更多的部署细节。 有关更多详细信息,请参见 [deployctl 页面](./deployctl.md)。 ### 使用 playground 进行部署 部署一些代码最简单的方法是通过 Deno Deploy Classic playground。 有关更多详细信息,请参见 [playground 页面](playgrounds)。 --- # 部署 Classic URL: https://docs.deno.com/deploy/classic/ :::info 旧版文档 您正在查看 Deno Deploy Classic 的旧文档。我们建议您迁移至新的 Deno Deploy 平台。 ::: 本节记录的是传统的 Deno Deploy Classic 平台(dash.deno.com)。 我们不再为 Deploy Classic 新增用户或组织。 如果您已有 Deploy Classic 项目,可以继续使用,但我们强烈建议您开始迁移至 新的 Deno Deploy 平台,网址为 [console.deno.com](https://console.deno.com)。 主要区别: - Deploy Classic 已进入维护模式(无新功能,有限更新)。 - 新功能(增强的 Node/NPM 支持、集成构建、指标、追踪、框架预设、静态资源、改进基础设施)已在 Deno Deploy 提供。 您可以从这里开始使用新平台: [关于 Deno Deploy 早期访问](/deploy/)。 迁移指南即将推出。与此同时,您可以设置新的 Deno Deploy 组织并重新部署应用。 如需帮助,请联系 support。 ## 什么是 Deno Deploy Classic? Deno Deploy Classic 是一个全球分布的无服务器 JavaScript 应用平台。您的 JavaScript、TypeScript 和 WebAssembly 代码运行于地理上靠近用户的托管服务器上,实现低延迟和更快响应。Deploy Classic 应用运行于快速、轻量的 [V8 隔离环境](https://deno.com/blog/anatomy-isolate-cloud),而非虚拟机,基于 [Deno 运行时](/runtime/manual)。 让我们来部署您的第一个应用程序 - 这只需几分钟。 ## 安装 Deno 和 `deployctl` 如果您还没有安装,可以使用以下命令之一来 [安装 Deno 运行时](/runtime/getting_started/installation): ```sh curl -fsSL https://deno.land/install.sh | sh ``` ```powershell irm https://deno.land/install.ps1 | iex ``` ```sh curl -fsSL https://deno.land/install.sh | sh ``` 安装 Deno 后,请安装 [`deployctl`](./deployctl.md) 工具: ```sh deno install -A jsr:@deno/deployctl --global ``` 您可以通过运行以下命令来确认 `deployctl` 已正确安装: ```console deployctl --help ``` 现在,您准备好从命令行部署 Deno 脚本了! ## 编写和测试 Deno 程序 首先,创建一个项目目录,并在其中创建一个名为 `main.ts` 的文件,内容如下所示的 "Hello World" 网络服务器: ```ts title="main.ts" Deno.serve(() => new Response("Hello, world!")); ``` 您可以通过运行以下命令来测试它是否有效: ```sh deno run --allow-net main.ts ``` 您的服务器应该可以在 [localhost:8000](http://localhost:8000) 上查看。现在让我们在边缘使用 Deno Deploy 运行这段代码! ## 部署您的项目 在您刚创建的 `main.ts` 文件的目录中,运行以下命令: ```sh deployctl deploy ``` 系统会提示您授权 Deno Deploy 在 GitHub 上注册 Deno Deploy 并/或为 `deployctl` 配置访问令牌。片刻之后,您的 Hello World 服务器将部署在 Deno Deploy Classic 基础设施中,遍布全球,准备处理您期望的所有流量。 ## 下一步 现在您已创建了第一个部署,您可以 [了解您可以在 Deno Deploy 上运行哪些类型的应用程序](./use-cases.md),查看 [使用 deployctl 还可以做什么](./deployctl.md),或者继续阅读以了解将代码部署到 Deno Deploy 的其他选项。我们非常期待看到您通过 Deno Deploy 交付的内容! ### 部署您现有的项目 导入一个项目并在边缘使用 Deno Deploy 运行它。 1. [从 Deno Deploy Classic 仪表板](https://dash.deno.com) 点击 "新建项目" 按钮。 2. 连接到您的 GitHub 帐户并选择要部署的存储库。 3. 按照屏幕上的说明部署您的现有应用程序。 如果您的项目需要构建步骤,请使用项目配置表单创建一个 GitHub 操作以部署您的项目。为您的项目命名并从可选框架预设中选择。如果您不使用框架,可以使用表单设置您的构建选项。 4. 确认您的构建选项正确,然后点击 "部署项目" 按钮以启动新的 GitHub 操作并部署您的项目。 片刻之后,您的项目将部署到全球约 12 个数据中心,准备处理大量流量。 一旦部署成功,您可以访问成功页面上提供的 URL 来查看您新部署的项目,或者在您的仪表板中管理它。 ### 从游乐场开始 [游乐场](./playgrounds.md) 是一个基于浏览器的编辑器,使您能够立即编写和运行 JavaScript 或 TypeScript 代码。这是一个很好的选择,让您开始体验 Deno 和 Deno Deploy! 从 [Deno Deploy Classic 仪表板](https://dash.deno.com) 点击 "新建游乐场" 按钮以创建一个游乐场。我们还提供了多种现成的教程,供您尝试 Deno Deploy Classic,可以点击 "学习游乐场" 或访问:\ [简单 HTTP 服务器游乐场](https://dash.deno.com/tutorial/tutorial-http)\ [使用 Deno KV 数据库游乐场](https://dash.deno.com/tutorial/tutorial-http-kv)\ [RESTful API 服务器游乐场](https://dash.deno.com/tutorial/tutorial-restful)\ [使用 WebSockets 的实时应用游乐场](https://dash.deno.com/tutorial/tutorial-websocket)\ [使用 Deno.cron 的定期任务游乐场](https://dash.deno.com/tutorial/tutorial-cron) --- # Deno Deploy 上的 KV URL: https://docs.deno.com/deploy/classic/kv_on_deploy :::info 旧版文档 You are viewing legacy documentation for Deno Deploy Classic. We recommend migrating to the new Deno Deploy platform. ::: Deno Deploy Classic 提供了一个内置的无服务器键值数据库,称为 Deno KV。 此外,Deno KV 也可以在 Deno 自身内部使用,利用 SQLite 作为其后端。自 Deno v1.32 起,这个功能在使用 `--unstable` 标志时可以访问。要了解更多关于 [Deno KV](/deploy/kv/manual) 的信息。 ## 一致性 默认情况下,Deno KV 是一个强一致性的数据库。它提供了最严格的强一致性形式,称为 _外部一致性_,这意味着: - **可序列化性**:这是事务的最高隔离级别。它确保多个事务的并发执行结果与这些事务按顺序逐个执行的系统状态相同。换句话说, 可序列化事务的最终结果相当于这些事务的某种顺序执行。 - **线性一致性**:这种一致性模型保证了操作(如读取和写入)看起来是瞬时的,并且实时发生。一旦写入操作完成,所有后续的读取操作将立即返回更新的值。线性一致性确保对操作的强实时排序,使得系统更可预测且更易于推理。 同时,您可以通过在单个读取操作上设置 `consistency: "eventual"` 选项来放宽一致性约束。此选项允许系统从全局副本和缓存中服务读取,以获取最小的延迟。 以下是我们主要区域的延迟数据: | 区域 | 延迟(最终一致性) | 延迟(强一致性) | | ------------------------- | ------------------ | ------------------------ | | 北弗吉尼亚 (us-east4) | 7ms | 7ms | | 法兰克福 (europe-west3) | 7ms | 94ms | | 荷兰 (europe-west4) | 13ms | 95ms | | 加利福尼亚 (us-west2) | 72ms | 72ms | | 香港 (asia-east2) | 42ms | 194ms | ## 分布式队列 无服务器的分布式队列在 Deno Deploy 上可用。有关更多详细信息,请参阅 [Deno Deploy 上的队列](/deploy/kv/manual/queue_overview#queues-on-deno-deploy)。 ## 从 Deno Deploy 以外连接到托管数据库 您可以从 Deno Deploy 以外的 Deno 应用程序连接到您的 Deno Deploy KV 数据库。要打开一个托管数据库,请将 `DENO_KV_ACCESS_TOKEN` 环境变量设置为 Deno Deploy 个人访问令牌,并将数据库的 URL 提供给 `Deno.openKv`: ```ts const kv = await Deno.openKv( "https://api.deno.com/databases//connect", ); ``` 请查看 [文档](https://github.com/denoland/deno/tree/main/ext/kv#kv-connect) 了解远程 KV 数据库连接协议的规范。 ## 数据分布 Deno KV 数据库在主要区域内的至少 3 个数据中心间进行复制。一旦写操作提交,其变更会持久存储在主要区域内的数据中心仲裁多数节点中。如果启用了跨区域复制,异步复制通常会在 5 秒内将变更传输到目标区域。 系统设计能够容忍大多数数据中心级别的故障,而不会出现停机或数据丢失。恢复点目标 (RPO) 与恢复时间目标 (RTO) 用于量化系统在不同故障模式下的弹性。RPO 表示可接受的数据最大丢失时间,RTO 表示在故障后恢复系统正常运行的最长时间。 - 主要区域内丢失一个数据中心:RPO=0(无数据丢失),RTO<5秒(系统在 5 秒内恢复) - 副本区域内丢失任意数量数据中心:RPO=0,RTO<5秒 - 主要区域内丢失两个或更多数据中心:RPO<60秒(数据丢失在 60 秒以内) --- # 应用程序日志 URL: https://docs.deno.com/deploy/classic/logs :::info 旧版文档 您正在查看 Deno Deploy Classic 的旧版文档。我们建议您迁移到新的 Deno Deploy 平台。 ::: 应用程序可以在运行时使用控制台 API 生成日志,方法包括 `console.log`、`console.error` 等。这些日志可以通过以下方式实时查看: - 导航到项目或部署的 `日志` 面板。 - 使用 [deployctl](/deploy/classic/deployctl) 中的 `logs` 子命令。 日志会直接从应用程序流式传输到日志面板,或显示在 `deployctl logs` 中。 除了实时日志,日志还会保留一段时间,具体取决于您所订阅的计划。要查看持久化日志,您可以: - 如果您在浏览器中使用日志面板,请在搜索框旁的下拉菜单中,将其从 `实时` 切换到 `最近` 或 `自定义`。 - 如果您更喜欢命令行,可以在 `deployctl logs` 命令中添加 `--since=` 和/或 `--until=`。有关更多详细信息,请查阅 `deployctl logs --help`。 超出保留期限的日志会自动从系统中删除。 ## 限制 日志消息的大小和在特定时间内生成的日志量都有所限制。 日志消息的最大大小为 2KB。超过此限制的消息会被裁剪为 2KB。 一个部署每秒最多允许生成 1000 条日志。如果超过此限度,我们可能会终止该部署。 --- # 反向代理中间件 URL: https://docs.deno.com/deploy/classic/middleware :::info 旧版文档 您正在查看 Deno Deploy Classic 的旧版文档。我们建议迁移到新的 Deno Deploy 平台。 ::: 本快速入门将介绍如何部署一个小型的中间件,它反向代理另一个服务器(在这个例子中是 example.com)。有关常见中间件功能的其他示例,请参见 [示例画廊](../tutorials/index.md)。 ## **步骤 1:** 在 Deno Deploy 上创建一个新的 Playground 项目 导航到 https://dash.deno.com/projects 并点击 "新建 Playground" 按钮。 ## **步骤 2:** 通过 Playground 部署中间件代码 在下一个页面,将以下代码复制并粘贴到编辑器中。这是一个将所有请求代理到 https://example.com 的 HTTP 服务器。 ```ts async function reqHandler(req: Request) { const reqPath = new URL(req.url).pathname; return await fetch("https://example.com" + reqPath, { headers: req.headers }); } Deno.serve(reqHandler); ``` 点击 **保存并部署**。 您应该看到类似以下内容的界面: ![image](../docs-images/proxy_to_example.png) --- # 连接到 Neon Postgres URL: https://docs.deno.com/deploy/classic/neon-postgres :::info Legacy Documentation 您正在查看 Deno Deploy Classic 的旧文档。我们建议您迁移到新的 Deno Deploy 平台。 ::: 本教程涵盖了如何从部署在 Deno Deploy 上的应用程序连接到 Neon Postgres 数据库。 ## 设置 Postgres 要开始,我们需要创建一个新的 Postgres 实例供我们连接。为了本教程,我们将使用 [Neon Postgres](https://neon.tech/),因为他们提供免费的托管 Postgres 实例。如果你希望将数据库托管在其他地方,也可以这么做。 1. 访问 https://neon.tech/ 并点击 **注册**,通过电子邮件、Github、Google 或合作伙伴帐户进行注册。注册后,系统将引导你进入 Neon 控制台以创建你的第一个项目。 2. 输入项目的名称,选择 Postgres 版本,提供数据库名称,并选择区域。通常,你会选择离你的应用程序最近的区域。当你完成后,点击 **创建项目**。 3. 系统会向你展示新项目的连接字符串,你可以用来连接到数据库。保存连接字符串,通常看起来像这样: ```sh postgres://alex:AbC123dEf@ep-cool-darkness-123456.us-east-2.aws.neon.tech/dbname?sslmode=require ``` 你需要在下一步中使用连接字符串。 ## 在 Deno Deploy 中创建项目 接下来,让我们在 Deno Deploy Classic 中创建一个项目,并设置所需的环境变量: 1. 访问 [https://dash.deno.com/new](https://dash.deno.com/new)(如果你还没有登录,使用 GitHub 登录)并点击 **创建一个空项目**,在 **部署你自己的代码** 下。 2. 现在点击项目页面上的 **设置** 按钮。 3. 导航到 **环境变量** 部分并添加以下秘密。 - `DATABASE_URL` - 值应设置为你在上一步中保存的连接字符串。 ![postgres_env_variable](../docs-images/neon_postgres_env_variable.png) ## 编写连接到 Postgres 的代码 要使用 [Neon 无服务器驱动程序](https://deno.com/blog/neon-on-jsr) 来读写 Postgres,首先使用 `deno add` 命令安装它: ```sh deno add jsr:@neon/serverless ``` 这将创建或更新你的 `deno.json` 文件,并添加依赖项: ```json { "imports": { "@neon/serverless": "jsr:@neon/serverless@^0.10.1" } } ``` 现在你可以在代码中使用该驱动程序: ```ts import { neon } from "@neon/serverless"; // 从环境变量 "DATABASE_URL" 获取连接字符串 const databaseUrl = Deno.env.get("DATABASE_URL")!; // 创建 SQL 查询执行器 const sql = neon(databaseUrl); try { // 创建表 await sql` CREATE TABLE IF NOT EXISTS todos ( id SERIAL PRIMARY KEY, title TEXT NOT NULL ) `; } catch (error) { console.error(error); } ``` ## 将应用程序部署到 Deno Deploy Classic 一旦你完成了应用程序的编写,就可以在 Deno Deploy Classic 上部署它。 为此,回到你的项目页面 `https://dash.deno.com/projects/`。 你应该会看到几个部署选项: - [Github 集成](ci_github) - [`deployctl`](./deployctl.md) ```sh deployctl deploy --project= ``` 除非你想添加构建步骤,否则我们建议你选择 GitHub 集成。 有关在 Deno Deploy Classic 上以不同方式部署和不同配置选项的详细信息,请阅读 [这里](how-to-deploy)。 --- # 组织 URL: https://docs.deno.com/deploy/classic/organizations :::info 旧文档 您正在查看 Deno Deploy Classic 的旧文档。我们建议迁移到全新的 Deno Deploy 平台。 ::: **组织** 允许您与其他用户协作。在一个组织中创建的项目对该组织的所有成员都是可访问的。用户应首先注册 Deno Deploy Classic,然后才能被添加到组织中。 目前,所有组织成员都拥有对组织的完全访问权限。他们可以添加/移除成员,并创建/删除/修改该组织中的所有项目。 ### 创建组织 <<<<<<< HEAD:deploy/manual/organizations.md 1. 在您的 Deploy 控制台上,点击屏幕左上角导航栏中的组织下拉菜单。 ======= 1. 在您的 Classic 控制台上,点击屏幕左上角导航栏中的组织下拉菜单。 ![organizations](../docs-images/organizations.png) 2. 选择 **组织 +**。 3. 输入您的组织名称,然后点击 **创建**。 ### 添加成员 <<<<<<< HEAD:deploy/manual/organizations.md 1. 在屏幕左上角导航栏中的组织下拉菜单中选择所需的组织。 2. 点击 **成员** 图标按钮。 3. 在 **成员** 面板下,点击 **+ 邀请成员**。 > **注意:** 用户应首先使用 > [这个链接](https://dash.deno.com/signin) 注册 Deno Deploy,然后您才能邀请他们。 4. 输入用户的 GitHub 用户名,然后点击 **邀请**。 Deploy 会向用户发送邀请电子邮件。然后他们可以选择接受或拒绝您的邀请。一旦他们接受邀请,他们将被添加到您的组织并显示在成员面板中。 ======= 1. 在屏幕左上角导航栏中的组织下拉菜单中选择所需的组织。 2. 点击 **成员** 图标按钮。 3. 在 **成员** 面板下,点击 **+ 邀请成员**。 > **注意:** 用户应首先使用 > [此链接](https://dash.deno.com/signin) 在 Deno Deploy Classic 注册,之后您才能邀请他们。 4. 输入用户的 GitHub 用户名,然后点击 **邀请**。 Deno Deploy Classic 会向用户发送邀请邮件。用户可以选择接受或拒绝邀请。一旦接受,用户将被添加到您的组织中并在成员面板中显示。 >>>>>>> origin/upstream:deploy/classic/organizations.md 待处理的邀请将在 **邀请** 面板中显示。您可以通过点击待处理邀请旁边的删除图标来撤销待处理的邀请。 ### 移除成员 1. 在屏幕左上角导航栏中的组织下拉菜单中选择所需的组织。 2. 点击 **成员** 图标按钮。 3. 在 **成员** 面板中,点击要移除的用户旁边的删除按钮。 --- # 游乐场 URL: https://docs.deno.com/deploy/classic/playgrounds :::info Legacy Documentation 您正在查看 Deno Deploy Classic 的传统文档。我们推荐迁移到新的Deno Deploy平台。 ::: **游乐场** 是一个简单的方式来玩转 Deno Deploy,并创建小项目。使用游乐场,您可以编写代码、运行代码,并在浏览器中完全看到输出。 游乐场拥有 Deno Deploy 的全部功能:它们支持与普通项目相同的所有功能,包括环境变量、自定义域名和日志。 游乐场的性能与 Deno Deploy 上的所有其他项目同样优秀:它们充分利用我们的全球网络,以尽可能接近用户的方式运行您的代码。 - [创建一个游乐场](#创建一个游乐场) - [使用游乐场编辑器](#使用游乐场编辑器) - [将游乐场设为公开](#将游乐场设为公开) - [将游乐场导出到 GitHub](#将游乐场导出到-github) ## 创建一个游乐场 要创建一个新的游乐场,请点击 [项目概览页面](https://dash.deno.com/projects) 右上角的 **新建游乐场** 按钮。 这将创建一个随机生成名称的新游乐场。您可以稍后在项目设置中更改此名称。 ## 使用游乐场编辑器 创建新游乐场时,游乐场编辑器会自动打开。您也可以通过导航到项目概览页面并点击 **编辑** 按钮来打开它。 编辑器主要分为两个区域:左侧是编辑器,右侧是预览面板。编辑器是您编写代码的地方,预览面板是您可以通过浏览器窗口查看代码输出的地方。 左侧的编辑器面板下方还有一个日志面板。此面板显示您的代码控制台输出,对调试代码很有帮助。 在编辑代码后,您需要保存并部署它,以便右侧的预览更新。您可以通过点击右上角的 **保存并部署** 按钮,按下 Ctrl + S,或按 F1 打开命令面板并选择 **部署:保存并部署** 来完成此操作。 在编辑器右上角的工具栏中,您可以在保存时查看项目的当前部署状态。 每次保存并部署代码时,右侧的预览面板会自动刷新。 编辑器右上角的语言下拉菜单允许您在 JavaScript、JSX、TypeScript 和 TSX 之间切换。默认选中的语言是 TSX,这在大多数情况下都能正常工作。 ## 将游乐场设为公开 游乐场可以通过设为公开与其他用户共享。这意味着任何人都可以查看游乐场及其预览。公共游乐场不能被其他人编辑:它们只能由您编辑。日志也仅对您可见。用户可以选择分叉一个公共游乐场,以创建一个可以编辑的私有副本。 要将游乐场设为公开,请在编辑器顶部工具栏中点击 **共享** 按钮。您游乐场的 URL 将自动复制到剪贴板。 您还可以通过 Deno Deploy 仪表板中的游乐场设置页面更改游乐场的可见性。这可以用于将游乐场的可见性从公开更改为私有。 ## 将游乐场导出到 GitHub 游乐场可以导出到 GitHub。这在您的项目开始超出游乐场编辑器的单文件限制时非常有用。 这样做将创建一个包含游乐场代码的新 GitHub 仓库。该项目将自动转换为一个与这个新 GitHub 仓库关联的 git 项目。环境变量和域名将被保留。 新创建的 GitHub 仓库将存储在您的个人账户中,并设置为私有。您可以在 GitHub 仓库设置中稍后更改这些设置。 导出游乐场后,您将无法再使用 Deno Deploy 游乐场编辑器来处理该项目。这是一个单向操作。 要导出游乐场,请访问 Deno Deploy 仪表板中的游乐场设置页面,或在命令面板中选择 **部署:导出到 GitHub**(在编辑器中按 F1)。 在这里,您可以输入新 GitHub 仓库的名称。该名称将用于在 GitHub 上创建仓库。仓库不能已经存在。 按下 **导出** 将游乐场导出到 GitHub。 --- # 连接到 Postgres URL: https://docs.deno.com/deploy/classic/postgres :::info Legacy Documentation 您正在查看 Deno Deploy Classic 的旧文档。我们建议迁移到新的 Deno Deploy 平台。 ::: 本教程介绍如何从部署在 Deno Deploy 上的应用程序连接到 Postgres 数据库。 ## 设置 Postgres > 本教程将完全集中于无加密连接到 Postgres。如果您希望使用自定义 CA 证书进行加密,请使用 [这里](https://deno-postgres.com/#/?id=ssltls-connection) 的文档。 要开始,我们需要为我们要连接的 Postgres 实例创建一个新的实例。对于本教程,我们将使用 [Supabase](https://supabase.com),因为它们提供免费的托管 Postgres 实例。如果您希望将数据库托管在其他地方,您也可以这样做。 1. 访问 https://app.supabase.io/ 并单击 **新项目**。 2. 为您的数据库选择一个名称、密码和地区。确保保存密码,因为稍后您将需要它。 3. 单击 **创建新项目**。创建项目可能需要一些时间,请耐心等待。 ## 从 Postgres 获取凭据 设置完 Postgres 数据库后,从您的 Postgres 实例中收集连接信息。 ### Supabase 对于上述 Supabase 实例,要获取连接信息: 1. 导航到左侧的 **数据库** 选项卡。 2. 转到 **项目设置** >> **数据库**,并从 **连接字符串** >> **URI** 字段中复制连接字符串。这是您将用来连接数据库的连接字符串。将您之前保存的密码插入该字符串中,然后将其保存到某处 - 您稍后会需要它。 ### psql 如果您使用 psql,通常可以通过运行以下命令找到连接信息: ```psql test=# \conninfo ``` 您的 Postgres 连接字符串将采用以下形式: ```sh postgres://user:password@127.0.0.1:5432/deploy?sslmode=disable ``` ## 在 Deno Deploy 中创建项目 接下来,让我们在 Deno Deploy Classic 中创建一个项目,并设置所需的环境变量: 1. 转到 [https://dash.deno.com/new](https://dash.deno.com/new)(如果尚未登录,请用 GitHub 登录),然后在 **从命令行部署** 下点击 **+ 空项目**。 2. 现在点击项目页面上的 **设置** 按钮。 3. 导航到 **环境变量** 部分,并添加以下密钥。 - `DATABASE_URL` - 值应为您在上一步中获取的连接字符串。 ![postgres_env_variable](../docs-images/postgres_env_variable.png) ## 编写连接到 Postgres 的代码 要读取/写入Postgres,请导入合适的Postgres模块,例如 [这个来自 JSR 的模块](https://jsr.io/@bartlomieju/postgres),从环境变量中读取连接字符串,并创建一个连接池。 ```ts import { Pool } from "jsr:@bartlomieju/postgres"; // 从环境变量 "DATABASE_URL" 获取连接字符串 const databaseUrl = Deno.env.get("DATABASE_URL")!; // 创建一个带有三个懒惰建立的连接的数据库连接池 const pool = new Pool(databaseUrl, 3, true); // 连接到数据库 const connection = await pool.connect(); try { // 创建表 await connection.queryObject` CREATE TABLE IF NOT EXISTS todos ( id SERIAL PRIMARY KEY, title TEXT NOT NULL ) `; } finally { // 将连接释放回连接池 connection.release(); } ``` ## 部署应用程序到 Deno Deploy Classic 完成应用程序的编写后,您可以将其部署到 Deno Deploy Classic。 为此,请返回到您的项目页面,网址为 `https://dash.deno.com/projects/`。 您应该会看到几个部署选项: - [Github 集成](ci_github) - [`deployctl`](./deployctl.md) ```sh deployctl deploy --project= ``` 除非您希望添加构建步骤,否则我们建议您选择 GitHub 集成。 有关在 Deno Deploy Classic 上以不同方式部署和不同配置选项的更多详细信息,请阅读 [这里](how-to-deploy)。 --- # Connect to Prisma Postgres URL: https://docs.deno.com/deploy/classic/prisma-postgres :::info Legacy Documentation You are viewing legacy documentation for Deno Deploy Classic. We recommend migrating to the new Deno Deploy platform. ::: This tutorial covers how to connect to a Prisma Postgres database from an application deployed on Deno Deploy. ## Setup Postgres There are several ways to set up a Prisma Postgre database for your Prisma project. This guide covers the most common approaches. ### Method 1: Using Prisma CLI Run the following command to initialize a new Prisma project with a database: ```bash npx prisma init --db ``` This will prompt you to select your preferred region and database name. Once completed, you'll find the `DATABASE_URL` connection string in your `.env` file. ### Method 2: Using `npx create-db` Alternatively, you can use the dedicated database creation tool: ```bash npx create-db@latest ``` This command will provide you with two connection strings tied to the same database: **Prisma ORM optimized connection string:** ```txt prisma+postgres://accelerate.prisma-data.net/?api_key= ``` **Standard Prisma Postgres connection string:** ```txt postgresql://:@db.prisma.io:5432/postgres ``` In order to keep the database created with `npx create-db`, you must follow through with the claim process. That can be done via the claim link provided in the terminal. The Prisma ORM optimized connection string (`prisma+postgres://`) only works with the Prisma ORM, while the standard Prisma Postgre connection string can be used with other database tools and libraries. ## Create a project in Deno Deploy Next, let's create a project in Deno Deploy Classic and set it up with the requisite environment variables: 1. Go to [https://dash.deno.com/new](https://dash.deno.com/new) (Sign in with GitHub if you didn't already) and click on **Create an empty project** under **Deploy your own code**. 2. Now click on the **Settings** button available on the project page. 3. Navigate to **Environment Variables** Section and add the following secret. - `DATABASE_URL` - The value should be set to the connection string you saved in the last step. ![postgres_env_variable](../docs-images/prisma_postgres_env_variable.png) ## Write code that connects to Postgres Now that you have your database set up, let's create a simple application that connects to the Prisma Postgres database using Prisma ORM. ### 1. Install dependencies First, install the required dependencies: ```bash deno install npm:@prisma/client deno install npm:@prisma/extension-accelerate deno install npm:dotenv-cli ``` :::note The `dotenv-cli` package is needed because Prisma Client doesn't read `.env` files by default on Deno. ::: ### 2. Create the database schema With your database connection configured, you can now apply the data model to your database: ```bash deno run -A npm:prisma migrate dev --name init ``` This command creates a new SQL migration file and runs it against your database. ### 3. Update your Prisma schema Edit your `prisma/schema.prisma` file to define a `Log` model and configure it for Deno: ```ts generator client { provider = "prisma-client" output = "../generated/prisma" runtime = "deno" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model Log { id Int @id @default(autoincrement()) level Level message String meta Json } enum Level { Info Warn Error } ``` ### 4. Create your application Create `index.ts` in your project root with the following content: ```typescript import { serve } from "https://deno.land/std@0.140.0/http/server.ts"; import { withAccelerate } from "npm:@prisma/extension-accelerate"; import { PrismaClient } from "./generated/prisma/client.ts"; const prisma = new PrismaClient().$extends(withAccelerate()); async function handler(request: Request) { // Ignore /favicon.ico requests: const url = new URL(request.url); if (url.pathname === "/favicon.ico") { return new Response(null, { status: 204 }); } const log = await prisma.log.create({ data: { level: "Info", message: `${request.method} ${request.url}`, meta: { headers: JSON.stringify(request.headers), }, }, }); const body = JSON.stringify(log, null, 2); return new Response(body, { headers: { "content-type": "application/json; charset=utf-8" }, }); } serve(handler); ``` ### 4. Test your application locally Start your application locally to test the database connection: ```bash npx dotenv -- deno run -A ./index.ts ``` Visit `http://localhost:8000` in your browser. Each request will create a new log entry in your database and return the log data as JSON. ## Deploy application to Deno Deploy Classic Once you have finished writing your application, you can deploy it on Deno Deploy Classic. To do this, go back to your project page at `https://dash.deno.com/projects/`. You should see a couple of options to deploy: - [Github integration](ci_github) - [`deployctl`](./deployctl.md) ```sh deployctl deploy --project= ``` Unless you want to add a build step, we recommend that you select the GitHub integration. For more details on the different ways to deploy on Deno Deploy Classic and the different configuration options, read [here](how-to-deploy). --- # 使用队列 URL: https://docs.deno.com/deploy/classic/queues :::info 旧文档 您正在查看 Deno Deploy Classic 的旧文档。我们建议迁移到新的 Deno Deploy 平台。 ::: Deno 运行时包含一个队列 API,支持异步处理大型工作负载,并确保队列消息的至少一次投递。队列可以用于在 web 应用程序中卸载任务,或安排未来某个时间的工作单元。 您将使用的主要 API 在 `Deno.Kv` 命名空间中,分别是 [ `enqueue`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.enqueue) 和 [ `listenQueue`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.listenQueue)。 ## 入队消息 要将消息入队以进行处理,请在一个 [`Deno.Kv`](https://docs.deno.com/api/deno/~/Deno.Kv) 实例上使用 `enqueue` 方法。在下面的示例中,我们展示了如何入队一个通知以便发送。 ```ts title="queue_example.ts" // 描述您的消息对象的形状(可选) interface Notification { forUser: string; body: string; } // 获取 KV 实例的引用 const kv = await Deno.openKv(); // 创建一个通知对象 const message: Notification = { forUser: "alovelace", body: "您有邮件!", }; // 将消息入队以进行立即投递 await kv.enqueue(message); ``` 您可以通过指定一个延迟选项(以毫秒为单位)来将消息入队以便稍后投递。 ```ts // 将消息安排在 3 天后投递 const delay = 1000 * 60 * 60 * 24 * 3; await kv.enqueue(message, { delay }); ``` 如果由于某种原因您的消息未被投递,您还可以指定一个 Deno KV 键,其中将存储您的消息值。 ```ts // 配置一个键以便发送未投递消息 const backupKey = ["failed_notifications", "alovelace", Date.now()]; await kv.enqueue(message, { keysIfUndelivered: [backupKey] }); // ... 灾难降临 ... // 获取未发送的消息 const r = await kv.get(backupKey); // 这是未发送的消息: console.log("找到未发送的通知给:", r.value?.forUser); ``` ## 监听消息 您可以配置一个 JavaScript 函数,通过在一个 [`Deno.Kv`](https://docs.deno.com/api/deno/~/Deno.Kv) 实例上使用 `listenQueue` 方法来处理添加到队列中的项目。 ```ts title="listen_example.ts" // 定义我们期望的队列消息的对象形状 interface Notification { forUser: string; body: string; } // 创建一个类型保护来检查传入消息的类型 function isNotification(o: unknown): o is Notification { return ( ((o as Notification)?.forUser !== undefined && typeof (o as Notification).forUser === "string") && ((o as Notification)?.body !== undefined && typeof (o as Notification).body === "string") ); } // 获取 KV 数据库的引用 const kv = await Deno.openKv(); // 注册一个处理函数以监听值 - 此示例展示 // 您如何发送通知 kv.listenQueue((msg: unknown) => { // 使用类型保护 - 然后 TypeScript 编译器知道 msg 是 Notification if (isNotification(msg)) { console.log("向用户发送通知:", msg.forUser); // ... 做一些实际发送通知的事情! } else { // 如果消息是未知类型,可能是一个错误 console.error("收到未知消息:", msg); } }); ``` ## 带有 KV 原子事务的队列 API 您可以将队列 API 与 [KV 原子事务](./transactions) 结合,以在同一事务中原子地入队消息和修改键。 ```ts title="kv_transaction_example.ts" const kv = await Deno.openKv(); kv.listenQueue(async (msg: unknown) => { const nonce = await kv.get(["nonces", msg.nonce]); if (nonce.value === null) { // 这条消息已经被处理 return; } const change = msg.change; const bob = await kv.get(["balance", "bob"]); const liz = await kv.get(["balance", "liz"]); const success = await kv.atomic() // 确保这条消息尚未被处理 .check({ key: nonce.key, versionstamp: nonce.versionstamp }) .delete(nonce.key) .sum(["processed_count"], 1n) .check(bob, liz) // 余额没有变化 .set(["balance", "bob"], bob.value - change) .set(["balance", "liz"], liz.value + change) .commit(); }); // 在同一 KV 事务中修改键并入队消息! const nonce = crypto.randomUUID(); await kv .atomic() .check({ key: ["nonces", nonce], versionstamp: null }) .enqueue({ nonce: nonce, change: 10 }) .set(["nonces", nonce], true) .sum(["enqueued_count"], 1n) .commit(); ``` ## 队列行为 ### 消息投递保证 运行时保证至少一次投递。这意味着对于大多数入队消息,[ `listenQueue`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.listenQueue) 处理程序将针对每条消息调用一次。在某些故障场景中,处理程序可能会多次调用同一消息以确保投递。重要的是要设计您的应用程序,以便能够正确处理重复消息。 您可以将队列与 [KV 原子事务](https://docs.deno.com/deploy/kv/manual/transactions) 原语结合使用,以确保您的队列处理程序 KV 更新仅针对每个消息执行一次。请参阅 [带有 KV 原子事务的队列 API](#queue-api-with-kv-atomic-transactions)。 ### 自动重试 当队列消息准备投递时,`[listenQueue](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.listenQueue)` 处理程序被调用以处理您的入队消息。如果您的处理程序抛出异常,运行时将自动重试调用处理程序,直到成功或达到最大重试次数。消息被认为已成功处理,一旦 [ `listenQueue`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.listenQueue) 处理程序调用成功完成。如果处理程序在重试时持续失败,消息将被丢弃。 ### 消息投递顺序 运行时尽量按照入队顺序投递消息。但是,没有严格的顺序保证。偶尔,为了确保最大吞吐量,消息可能会被无序投递。 ## Deno Deploy 中的队列 Deno Deploy 提供了全球范围内、无服务器、分布式的队列 API 实现,旨在提供高可用性和高吞吐量。您可以使用它构建能够处理大工作负载的应用程序。 ### 按需快速启动孤立环境 在使用 Deno Deploy 的队列时,将根据需要自动快速启动孤立环境,以在消息可用于处理时调用您的 [ `listenQueue`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.listenQueue) 处理程序。定义 [ `listenQueue`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.listenQueue) 处理程序是启用队列处理的唯一要求,无需其他配置。 ### 队列大小限制 未投递队列消息的最大数量限制为 100,000。当队列已满时, [ `enqueue`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.enqueue) 方法将失败并返回错误。 ### 定价细节和限制 - [ `enqueue`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.enqueue) 与其他 [ `Deno.Kv`](https://docs.deno.com/api/deno/~/Deno.Kv) 写入操作一样对待。 入队的消息会占用 KV 存储和写入单位。 - 通过 [ `listenQueue`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.listenQueue) 发送的消息会消耗请求和 KV 写入单位。 - 有关更多信息,请参见 [定价细节](https://deno.com/deploy/pricing)。 ## 用例 队列在许多不同场景中都很有用,但在构建 web 应用程序时您可能会看到的一些用例有: ### 卸载异步进程 有时,客户发起的任务(例如发送通知或 API 请求)可能会花费较长时间,因此您不希望让客户在返回响应之前等待该任务完成。其他时候,客户其实根本不需要响应,例如,当客户向您的应用程序发送 [webhook 请求](https://en.wikipedia.org/wiki/Webhook) 时,因此没有必要在返回响应之前等待底层任务完成。 在这些情况下,您可以将工作卸载到队列中,以保持您的 web 应用程序响应并向客户发送即时反馈。要查看此用例的实际示例,请查看我们的 [webhook 处理示例](../tutorials/webhook_processor.md)。 ### 为未来调度工作 队列的另一个有用应用(以及类似这种的队列 API)是安排在未来适当时间进行的工作。也许您想在新客户下单后的一天发送通知,以便向他们发送满意度调查。您可以安排一个队列消息在 24 小时后投递,并设置一个监听器在那个时候发送出通知。 要查看安排未来发送通知的示例,请查看我们的 [通知示例](../tutorials/schedule_notification.md)。 --- # 区域 URL: https://docs.deno.com/deploy/classic/regions :::info 旧版文档 您正在查看 Deno Deploy Classic 的旧版文档。我们建议您迁移至新的 Deno Deploy 平台。 ::: Deno Deploy Classic 将您的代码部署到全球各地。每个新请求都从离用户最近的区域提供服务。Deno Deploy Classic 目前位于以下区域: - 新加坡(`asia-southeast1`) - 伦敦(`europe-west2`) - 法兰克福(`europe-west3`) - 圣保罗(`southamerica-east1`) - 北弗吉尼亚(`us-east4`) - 加利福尼亚(`us-west2`) 该列表将保持更新,以反映我们区域的最新概况。 代码被部署到所有区域,并从离最终用户最近的区域提供服务,以最小化延迟。目前无法限制您的代码部署的区域。 --- # 本地开发 URL: https://docs.deno.com/deploy/classic/running-scripts-locally :::info 旧版文档 您正在查看 Deno Deploy Classic 的旧版文档。我们建议 迁移至新的 Deno Deploy 平台。 ::: 要进行本地开发,您可以使用 `deno` CLI。要安装 `deno`,请按照 [Deno 手册](https://deno.land/manual/getting_started/installation)中的说明进行操作。 安装后,您可以在本地运行脚本: ```shell $ deno run --allow-net=:8000 https://deno.com/examples/hello.js 正在监听 http://localhost:8000 ``` 要监视文件更改,请添加 `--watch` 标志: ```shell $ deno run --allow-net=:8000 --watch ./main.js 正在监听 http://localhost:8000 ``` 有关 Deno CLI 的更多信息,以及如何配置您的开发环境和 IDE,请访问 Deno 手册的[开始使用][manual-gs]部分。 [manual-gs]: https://deno.land/manual/getting_started --- # 兑现政策 > 我们关于 Deno Deploy 的退款和取消政策。 URL: https://docs.deno.com/deploy/fulfillment_policy ## 退款政策 在 Deno Deploy,我们努力提供卓越的服务。如果您对我们的服务不满意,您可以在以下条件下请求退款: 退款必须在初次购买或升级任何订阅计划后的 14 天内请求。如果服务未能正常运行,并且如果我们的支持团队在合理的时间内无法解决问题,则可以考虑退款。对于违反我们的服务条款所使用的服务或明显归因于用户错误或外部平台变化的问题,将不予退款。定期订阅可以取消,但仅在 14 天内请求时才有资格获得初始计费周期的退款。 ## 取消政策 您可以在以下条款下随时取消您的 Deno Deploy 或 Deno Deploy Classic 订阅: 订阅取消将立即生效,服务将继续运行至当前计费周期结束。要取消您的订阅,请导航到 Deno Deploy 仪表板上的帐户设置,并选择“取消订阅”。一旦订阅被取消,将不再产生进一步费用,但您仍需对取消生效日期之前的所有费用负责。如需了解我们履行政策的更多信息,或如果您需要帮助,请通过 [deploy@deno.com](mailto:deploy@deno.com) 联系我们的支持团队。 --- # 入门指南 > 逐步指南,帮助您创建和配置您的第一个 Deno Deploy 早期访问应用程序,包括组织设置、构建配置、环境变量和部署监控。 URL: https://docs.deno.com/deploy/getting_started :::info 您正在查看 Deno DeployEA 的文档。寻找 Deploy Classic 文档?[点击这里查看](/deploy/)。 ::: ## 创建组织 Deno DeployEA 终将取代 Deno Deploy Classic。在此之前,两个系统将同时可用。通过为 DeployEA 创建组织,您可以在不干扰已经使用 Deploy Classic 的项目的情况下,探索 DeployEA。 开始使用 Deno DeployEA: 1. 访问 [console.deno.com](http://console.deno.com) 2. 创建组织: ![Deno DeployEA 组织创建屏幕。](./images/create_org.png) 注意,您不能使用与 Deploy Classic 中已存在项目相同的 slug 来创建组织。组织名称和 slug 创建后不可更改。 ## 创建应用 创建组织后,系统会跳转至组织应用页面,展示您所有的应用,并可访问组织设置及自定义域名。 :::info 您也可以通过命令行使用 `deno deploy create` 创建和配置应用。详情请参见[应用参考](/deploy/reference/apps/#using-the-cli)。 ::: ## 选择仓库 ![部署应用创建屏幕截图](./images/create_app.png) 应用是一个单一部署的 Web 服务,拥有构建配置、构建历史、环境变量、附加自定义域名、关联的 GitHub 仓库等。 1. 选择您的应用对应的 GitHub 仓库: ![部署组织选择屏幕截图](./images/select_org.png) 如果您的仓库未显示,点击 `Add another GitHub account` 或 `Configure GitHub App permissions` 按钮,授权 Deno Deploy GitHub 应用访问您的仓库。 > ⏳ 目前尚不支持 Mono-repos(应用位于仓库子目录中的多仓库结构)。 ## 配置您的应用 Deno DeployEA 会自动尝试检测您的应用类型并配置相应的构建设置。您可以在 `App Config` 框中查看检测到的配置: ![部署应用配置屏幕截图](./images/app_config.png) 要修改配置,请点击 `Edit build config`。 ![部署构建配置屏幕截图](./images/build_config.png) ## 配置构建 在构建配置抽屉中,您可以自定义: ### 框架预设 选择您的框架,或者如果使用自定义配置,则选择 `No Preset`。 ### 安装命令 安装依赖的命令(例如 `npm install`、`deno install`)。如果是没有 `package.json` 的 Deno 应用此项可留空。 ### 构建命令 用于编译/打包应用的命令(例如 `next build`、`deno task build`)。如果您的应用不需要构建,可留空。 ### 运行时配置 对于大多数框架,此处无需配置,Deno DeployEA 会根据框架预设自动确定最优的运行时配置。若未配置框架,您可以在此选择应用是需要针对每个请求执行服务端代码的 `Dynamic` 应用(如 API 服务、服务端渲染应用等),还是仅由一组静态文件组成的 `Static` 应用。 ### 动态入口文件 启动应用时应执行的 JavaScript 或 TypeScript 文件。这个路径应与您本地通过 `deno run` 或 `node` 启动应用时传入的路径相同,且相对于工作目录。 ### 动态参数 启动时传递给应用的额外命令行参数(入口文件后),这些参数传递给应用而非 Deno 本身。 ### 静态目录 工作目录中存放静态文件的目录,例如 `dist`、`_site` 或 `.output`。 ### 单页应用模式 应用是否为单页应用(SPA),当静态目录中不存在路径对应文件时,是否应返回根目录 `index.html` 而非 404 页面。 关闭抽屉即保存设置。 ### 环境变量 添加环境变量的方法: 1. 点击 `Add/Edit environment variables` 2. 点击抽屉中的 `+ Add variable` 3. 输入变量名称和数值 4. 选择其为普通变量还是密钥 5. 选择其可用的上下文环境: - **Production**:针对生产域名的请求 - **Development**:针对预览/分支域名的请求 6. 点击 `Save` 以应用更改 ![部署环境变量配置屏幕截图](./images/env_var.png) ## 构建并部署您的应用 1. 点击 `Create App` 创建应用并启动首次构建。 2. 通过实时日志查看构建进度: ![应用构建日志截图](./images/build_logs.png) 构建日志分为以下阶段: - **Prepare(准备)**:克隆仓库并恢复缓存 - **Install(安装)**:执行安装命令和框架特定设置 - **Build(构建)**:执行构建命令并生成部署产物 - **Warm up(预热)**:测试部署,通过请求确认应用正常启动 - **Route(部署)**:将构建版本部署至全球节点 您可以通过界面左上角按钮取消构建,失败时也可从此处重新启动构建。 构建完成后,右上角显示预览 URL,下方列出构建所部署的所有时间线。 ## 监控您的应用程序 部署完成后,您可以使用监控工具来观察您的应用: ### 日志 浏览应用日志,支持通过上下文、版本和文本内容筛选: ![日志页面截图](./images/logs.png) 使用搜索栏过滤日志(如 `context:production`、`revision:`)。时间选择器调整日志显示区间。 当日志关联到追踪时,您会看到 “查看跟踪” 按钮,点击即可查看追踪详情。 ### 跟踪 查看请求追踪及详细的时间信息: ![跟踪页面截图](./images/traces.png) 点击任意追踪打开带有瀑布流视图的追踪详情页,展示所有跨度信息: ![跟踪视图截图](./images/trace.png) 追踪视图包括: - 跨度时间线及持续时长 - 跨度详情及属性 - 在跨度期间产生日志的信息 要保存环境变量,请点击保存按钮。您也可以重新打开抽屉编辑或删除已有环境变量。 您还可以在此页面编辑应用名称,选择应用服务的区域。 ## 构建并部署您的应用(重复部分) 最后,您可以点击 `Create App` 按钮创建应用,系统将立即触发首次构建: ![应用构建日志截图](./images/build_logs.png) 构建页面实时显示多阶段的构建日志: - **Prepare(准备)**:克隆 GitHub 仓库与恢复缓存 - **Install(安装)**:执行安装命令及框架预安装步骤 - **Build(构建)**:执行构建命令及框架预设操作,为部署生成产物 - **Warm up(预热)**:向部署预览 URL 发送请求,保证应用正常启动。此处显示的日志为运行时日志,不是构建日志。 - **Route(部署)**:Deno Deploy 将新版本推送到全球所有区域 左上角有取消构建按钮,失败时也可重新启动。 完成后右上角显示预览 URL,及构建版本所部署所有时间线(如 `Production` 或 `Git Branch`)。 您可查看构建触发方式: - `manual action` 表示手动触发 - `GitHub repo` 表示通过 GitHub 集成触发 可通过预览 URL 或时间线列表中的其他 URL 访问您的应用。 ## 监控您的应用程序(重复部分) 访问应用后,您可查看遥测信息,包括日志和追踪数据,通过左侧栏访问对应页面。 ### 日志 ![日志页面截图](./images/logs.png) 日志页面显示项目中的最近所有日志。默认显示生产和开发环境日志,可通过顶部筛选或搜索栏限制显示内容,例如输入 `context:production` 过滤生产环境日志,`revision:` 过滤特定版本。 支持全文搜索,仅匹配日志中写入文本(不区分大小写)。 默认显示最近一小时日志,通过右上角时间选择器调整时间范围,时间戳显示按选择器时区。 日志行右侧出现“查看跟踪”按钮,点击后可查看该日志关联的追踪详情。 ### 跟踪 ![跟踪页面截图](./images/traces.png) 跟踪页面显示项目中的最新追踪,默认展示所有环境的追踪,通过顶部筛选和搜索限制显示范围,例如 `context:production` 仅显示生产环境追踪,`revision:` 筛选特定版本。 列表包括所有传入 HTTP 请求追踪,显示请求路径和耗时(毫秒)。点击追踪项打开详情视图,显示包含的全部跨度和日志。 ![跟踪视图截图](./images/trace.png) 每个跨度包括持续时间、名称、开始和结束时间,以及记录的属性。点击时间线上跨度即可在底部摘要面板查看详细信息。 对应跨度的日志内容显示在底部“日志”标签页,切换跨度时日志内容也会随之更新。 --- # About Deno Deploy > Guide to Deno Deploy features, comparison with Deploy Classic, and getting started instructions for deployment. URL: https://docs.deno.com/deploy/ Go to the Deno Deploy dashboard Deno Deploy comes with an easy to use dashboard at [console.deno.com](https://console.deno.com). In this dashboard, you can create new Deno Deploy organizations that contain Deno Deploy apps. Within a single organization, you cannot mix Deno Deploy apps with Deploy Classic projects. You can switch between different organizations using the organization picker in the top left of the dashboard. ## What is Deno Deploy? Deno Deploy is a serverless platform for running JavaScript and TypeScript applications in the cloud (or self-hosted on your own infrastructure). It provides a management plane for deploying and running applications with the built-in CI or through integrations such as GitHub actions. ## Comparison to Deploy Classic Deno Deploy is a complete rework of Deploy Classic. It has a new dashboard, and a new execution environment that uses Deno 2.0 and is much more powerful than Deploy Classic. The below table compares the two versions of Deno Deploy. | Feature | Deno Deploy | Deploy Classic | | ------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | Web interface | console.deno.com | dash.deno.com | | Dark mode | ✅ Supported | ❌ Not supported | | Builds | ✅ Fully integrated | 🟠 Runs in GitHub Actions, no live streamed logs in the dashboard, caching requires manual setup, changing config requires editing YAML | | Can run Deno apps | ✅ Full support | 🟠 Limited (no FFI, subprocesses, write permission) | | Can run Node apps | ✅ Full support | 🟠 Limited (no FFI, native addons, subprocesses, write permission, and degraded NPM compatibility) | | Can run Next.js/Astro/SvelteKit | ✅ First-class support | 🟠 Framework dependent, requires manual setup | | First class static sites | ✅ Supported | ❌ Not supported | | Environment Variables | ✅ Different dev/prod env vars | 🟠 One set of env vars for all deployments | | CDN caching | ✅ Supported | ❌ Not supported | | Web Cache API | ✅ Supported | ✅ Supported | | Databases | ✅ Supported | 🟠 Deno KV | | Queues | ❌ Not supported | ✅ Supported | | Cron | ❌ Not supported | ✅ Supported | | Deploy from GitHub | ✅ Supported | ✅ Supported | | Deploy from CLI | ✅ Supported | ✅ Supported | | Instant Rollback | ✅ Supported | ✅ Supported | | Logs | ✅ Supported | ✅ Supported | | Tracing | ✅ Supported | ❌ Not supported | | Metrics | ✅ Supported | ❌ Not supported | | OpenTelemetry export | ⏳ Work in progress | ❌ Not supported | | Regions | 2 | 6 | | Self hostable regions | ✅ Supported | ❌ Not supported | ## How to access Deno Deploy To begin using Deno Deploy: 1. Visit [console.deno.com](https://console.deno.com) to access the new dashboard 2. Create a new Deno Deploy organization 3. Create your first application within this organization 4. Deploy from your GitHub repository or directly from the dashboard For detailed configuration instructions and framework-specific guides, please refer to our reference documentation. --- # 备份 URL: https://docs.deno.com/deploy/kv/backup 在 Deno Deploy 上托管的 KV 数据库可以持续备份到您自己的 S3 兼容存储桶。这是我们为所有存储在托管 Deno KV 数据库中的数据内部执行的复制和备份的补充,以确保高可用性和数据持久性。 此备份将持续进行,几乎没有延迟,支持 _[时间点恢复](https://en.wikipedia.org/wiki/Point-in-time_recovery)_ 和实时复制。为 KV 数据库启用备份可以解锁各种有趣的使用案例: - 在过去的任何时间点检索数据的一致快照 - 独立于 Deno Deploy 运行只读数据副本 - 通过将变更管道到流媒体平台和分析数据库(如 Kafka、BigQuery 和 ClickHouse)来推送数据到您最喜欢的数据管道 ## 配置备份到 Amazon S3 首先,您必须在 AWS 上创建一个存储桶: 1. 访问 [AWS S3 控制台](https://s3.console.aws.amazon.com/s3/home) 2. 点击 “创建存储桶” 3. 输入存储桶名称并选择 AWS 区域,然后向下滚动并点击 “下一步” 1. 安装 [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) 2. 运行 `aws s3api create-bucket --bucket --region --create-bucket-configuration LocationConstraint=` (将 `` 和 `` 替换为您的值) 接下来,创建一个对存储桶具有 `PutObject` 访问权限的 IAM 策略,将其附加到 IAM 用户,并为该用户创建访问密钥: 1. 访问 [AWS IAM 控制台](https://console.aws.amazon.com/iam/home) 2. 在左侧边栏点击 “策略” 3. 点击 “创建策略” 4. 选择 “JSON” 策略编辑器并粘贴以下策略: ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "KVBackup", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::/*" } ] } ``` 将 `` 替换为您之前创建的存储桶名称。 5. 点击 “审核策略” 6. 输入策略名称并点击 “创建策略” 7. 在左侧边栏点击 “用户” 8. 点击 “添加用户” 9. 输入用户名称并点击 “下一步” 10. 点击 “直接附加策略” 11. 搜索您之前创建的策略,并点击其旁边的复选框 12. 点击 “下一步” 13. 点击 “创建用户” 14. 点击您刚刚创建的用户 15. 点击 “安全凭证”,然后点击 “创建访问密钥” 16. 选择 “其他”,然后点击 “下一步” 17. 输入访问密钥描述并点击 “创建访问密钥” 18. 复制访问密钥 ID 和秘密访问密钥,并将其保存在安全的地方。 您稍后会需要它们,并且将无法再检索它们。 1. 复制以下命令到终端,将 `` 替换为您之前创建的存储桶名称,然后运行: ``` aws iam create-policy --policy-name --policy-document '{"Version":"2012-10-17","Statement":[{"Sid":"KVBackup","Effect":"Allow","Action":"s3:PutObject","Resource":"arn:aws:s3:::/*"}]}' ``` 2. 复制以下命令到终端,将 `` 替换为您正在创建的用户的名称,然后运行: ``` aws iam create-user --user-name ``` 3. 复制以下命令到终端,将 `` 替换为您在步骤 1 中创建的策略的 ARN,并将 `` 替换为您在上一步中创建的用户的名称,然后运行: ``` aws iam attach-user-policy --policy-arn --user-name ``` 4. 复制以下命令到终端,将 `` 替换为您在步骤 2 中创建的用户的名称,然后运行: ``` aws iam create-access-key --user-name ``` 5. 复制访问密钥 ID 和秘密访问密钥,并将其保存在安全的地方。 您稍后会需要它们,并且将无法再检索它们。 现在访问 [Deno Deploy 控制台](https://dash.deno.com),并在您的项目中点击 “KV” 选项卡。滚动到 “备份” 部分,点击 “AWS S3”。输入您之前创建的存储桶名称、访问密钥 ID 和秘密访问密钥,以及存储桶所在的区域。然后点击 “保存”。 将备份添加到仪表板 备份将立即开始。一旦数据备份完成,并且连续备份处于活动状态,您将看到状态变为 “活跃”。 ## 配置备份到 Google Cloud Storage Google Cloud Storage (GCS) 兼容 S3 协议,也可以用作备份目标。 首先,您必须在 GCP 上创建一个存储桶: 1. 访问 [GCP Cloud Storage 控制台](https://console.cloud.google.com/storage/browser) 2. 点击顶部栏的 “创建” 3. 输入存储桶名称,选择位置,然后点击 “创建” 1. 安装 [gcloud CLI](https://cloud.google.com/sdk/docs/install) 2. 运行 `gcloud storage buckets create --location ` (将 `` 和 `` 替换为您的值) 接下来,创建一个对存储桶具有 `Storage Object Admin` 访问权限的服务账号,并为该服务账号创建 HMAC 访问密钥: 1. 访问 [GCP IAM 控制台](https://console.cloud.google.com/iam-admin/iam) 2. 在左侧边栏点击 “服务账户” 3. 点击 “创建服务账户” 4. 输入服务账户名称并点击 “完成” 5. 复制您刚刚创建的服务账户的电子邮件。您稍后会需要它。 6. 访问 [GCP Cloud Storage 控制台](https://console.cloud.google.com/storage/browser) 7. 点击您之前创建的存储桶 8. 点击工具栏上的 “权限” 9. 点击 “授予访问权限” 10. 将您之前复制的服务账户的电子邮件粘贴到 “新主体” 字段中 11. 从 “选择角色” 下拉菜单中选择 “Storage Object Admin” 12. 点击 “保存” 13. 在左侧边栏点击 “设置”(仍然在 Cloud Storage 控制台中) 14. 点击 “互操作性” 标签 15. 点击 “为服务帐号创建密钥” 16. 选择您之前创建的服务帐号 17. 点击 “创建密钥” 18. 复制访问密钥和秘密访问密钥,并将其保存在安全的地方。您稍后会需要它们,并且将无法再检索它们。 1. 运行以下命令,将 `` 替换为您正在创建的服务帐号的名称: ``` gcloud iam service-accounts create ``` 2. 运行以下命令,将 `` 替换为您之前创建的存储桶名称,并将 `` 替换为您在上一步创建的服务帐户的电子邮件: ``` gsutil iam ch serviceAccount::objectAdmin gs:// ``` 3. 运行以下命令,将 `` 替换为您在上一步创建的服务帐户的电子邮件: ``` gcloud storage hmac create ``` 4. 复制 `accessId` 和 `secret` 并将其保存在安全的地方。您稍后会需要它们,并且将无法再检索它们。 现在访问 [Deno Deploy 控制台](https://dash.deno.com),并在您的项目中点击 “KV” 选项卡。滚动到 “备份” 部分,点击 “Google Cloud Storage”。输入您之前创建的存储桶名称、访问密钥 ID 和秘密访问密钥,以及存储桶所在的区域。然后点击 “保存”。 备份将立即开始。一旦数据备份完成,并且连续备份处于活动状态,您将看到状态变为 “活跃”。 ## 使用备份 S3 备份可以与 `denokv` 工具一起使用。有关更多详细信息,请参阅 [文档](https://github.com/denoland/denokv)。 --- # 在 TypeScript 中的数据建模 URL: https://docs.deno.com/deploy/kv/data_modeling_typescript 在 TypeScript 应用程序中,通常希望创建强类型、良好文档化的对象,以包含应用程序操作的数据。使用 [接口](https://www.typescriptlang.org/docs/handbook/2/objects.html) 或 [类](https://www.typescriptlang.org/docs/handbook/2/classes.html),您可以描述程序中对象的形状和行为。 然而,如果您使用的是 Deno KV,则需要进行一些额外的工作来持久化和检索强类型对象。在本指南中,我们将讨论在 Deno KV 中处理强类型对象的策略。 ## 使用接口和类型断言 在 Deno KV 中存储和检索应用数据时,您可能想要首先使用 TypeScript 接口描述数据的形状。以下是一个对象模型,它描述了博客系统的一些关键组件: ```ts title="model.ts" export interface Author { username: string; fullName: string; } export interface Post { slug: string; title: string; body: string; author: Author; createdAt: Date; updatedAt: Date; } ``` 这个对象模型描述了一篇博客帖子及其相关的作者。 使用 Deno KV,您可以将这些 TypeScript 接口用作 [数据传输对象 (DTO)](https://martinfowler.com/bliki/LocalDTO.html) ——对您可能发送到 Deno KV 或从中接收的非类型对象的强类型封装。 无需任何额外工作,您可以愉快地将这些 DTO 的内容存储在 Deno KV 中。 ```ts import { Author } from "./model.ts"; const kv = await Deno.openKv(); const a: Author = { username: "acdoyle", fullName: "Arthur Conan Doyle", }; await kv.set(["authors", a.username], a); ``` 然而,从 Deno KV 检索这个对象时,它默认不会具有与之关联的类型信息。如果您知道存储了哪个键的对象形状,您可以使用 [类型断言](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) 通知 TypeScript 编译器对象的形状。 ```ts import { Author } from "./model.ts"; const kv = await Deno.openKv(); const r = await kv.get(["authors", "acdoyle"]); const ac = r.value as Author; console.log(ac.fullName); ``` 您还可以为 `get` 指定一个可选的 [类型参数](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.get): ```ts import { Author } from "./model.ts"; const kv = await Deno.openKv(); const r = await kv.get(["authors", "acdoyle"]); console.log(r.value.fullName); ``` 对于更简单的数据结构,这个技术可能足够了。但通常,您希望或需要在创建或访问您的领域对象时应用一些业务逻辑。当这种需要出现时,您可以开发一组纯函数来操作您的 DTO。 ## 使用服务层封装业务逻辑 当您应用程序的持久化需求变得更加复杂时——例如,当您需要创建 [二级索引](./secondary_indexes) 以通过不同的键查询数据,或维护对象之间的关系——您将希望创建一组函数来位于 DTO 之上,以确保传递的数据是有效的(而不仅仅是正确类型的)。 从我们上述的业务对象来看,`Post` 对象足够复杂,因此可能需要一小层代码来保存和检索对象的实例。以下是两个包裹底层 Deno KV API 的函数示例,同时返回强类型的 `Post` 接口实例。 值得注意的是,我们需要存储一个 `Author` 对象的标识符,以便稍后从 KV 检索作者信息。 ```ts import { Author, Post } from "./model.ts"; const kv = await Deno.openKv(); interface RawPost extends Post { authorUsername: string; } export async function savePost(p: Post): Promise { const postData: RawPost = Object.assign({}, p, { authorUsername: p.author.username, }); await kv.set(["posts", p.slug], postData); return p; } export async function getPost(slug: string): Promise { const postResponse = await kv.get(["posts", slug]); const rawPost = postResponse.value as RawPost; const authorResponse = await kv.get(["authors", rawPost.authorUsername]); const author = authorResponse.value as Author; const post = Object.assign({}, postResponse.value, { author, }) as Post; return post; } ``` 这个薄层使用 `RawPost` 接口,扩展了实际的 `Post` 接口,以包括一些用于引用另一索引(相关的 `Author` 对象)的附加数据。 `savePost` 和 `getPost` 函数取代了直接的 Deno KV `get` 或 `set` 操作,从而可以正确地序列化和“注入”带有适当类型和关联的模型对象。 --- # Deno KV 快速入门 URL: https://docs.deno.com/deploy/kv/ **Deno KV** 是一个 [键值数据库](https://en.wikipedia.org/wiki/Key%E2%80%93value_database) ,直接构建在 Deno 运行时中,提供在 [`Deno.Kv` 命名空间](https://docs.deno.com/api/deno/~/Deno.Kv) 中可用。它可以用于许多类型的数据存储用例,但在存储简单数据结构时表现出色,这些数据结构受益于非常快速的读取和写入。Deno KV 可在 Deno CLI 和 [Deno Deploy](./on_deploy) 上使用。 :::caution Deno KV 仍在开发中,可能会发生变化。要使用它,必须向 Deno 传递 `--unstable-kv` 标志。 ::: 让我们来了解 Deno KV 的关键特性。 ## 打开和关闭数据库 在你的 Deno 程序中,可以使用 [`Deno.openKv()`](https://docs.deno.com/api/deno/~/Deno.openKv) 获取对 KV 数据库的引用。你可以传入一个可选的文件系统路径,以确定你希望存储数据库的位置,否则将根据你的脚本的当前工作目录为你创建一个。 要关闭数据库连接,请使用 [`.close()`](https://docs.deno.com/api/deno/~/Deno.Kv#method_close_0) 方法。 ```ts const kv = await Deno.openKv(); // 执行一些查询 ... await kv.close(); ``` ## 创建、更新和读取键值对 Deno KV 中的数据以键值对的形式存储,类似于 JavaScript 对象字面量的属性或 [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)。 [键](./key_space) 由 JavaScript 类型的数组表示,如 `string`、`number`、`bigint` 或 `boolean`。值可以是任意 JavaScript 对象。在这个例子中,我们创建了一个表示用户 UI 偏好的键值对,并使用 [`kv.set()`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.set) 保存它。 ```ts const kv = await Deno.openKv(); const prefs = { username: "ada", theme: "dark", language: "en-US", }; const result = await kv.set(["preferences", "ada"], prefs); ``` 一旦设置了键值对,你可以使用 [`kv.get()`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.get) 从数据库中读取它: ```ts const entry = await kv.get(["preferences", "ada"]); console.log(entry.key); console.log(entry.value); console.log(entry.versionstamp); ``` `get` 和 `list` [操作](./operations) 都返回一个 [KvEntry](https://docs.deno.com/api/deno/~/Deno.KvEntry) 对象,具有以下属性: - `key` - 用于设置值的数组键 - `value` - 为此键设置的 JavaScript 对象 - `versionstamp` - 用于确定键是否已更新的生成值。 `set` 操作也用于更新已存在的对象。当键的值被更新时,它的 `versionstamp` 将更改为一个新的生成值。 ## 列出多个键值对 为了获取有限数量的键的值,您可以使用 [`kv.getMany()`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.getMany)。传入多个键作为参数,您将收到一个每个键的值数组。请注意,**如果给定的键没有值,值和版本戳可以是 `null`**。 ```ts const kv = await Deno.openKv(); const result = await kv.getMany([ ["preferences", "ada"], ["preferences", "grace"], ]); result[0].key; // ["preferences", "ada"] result[0].value; // { ... } result[0].versionstamp; // "00000000000000010000" result[1].key; // ["preferences", "grace"] result[1].value; // null result[1].versionstamp; // null ``` 通常,从所有具有给定前缀的键中检索键值对列表是很有用的。这种操作可以使用 [`kv.list()`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.list) 完成。在这个例子中,我们获取了共享 `"preferences"` 前缀的键值对列表。 ```ts const kv = await Deno.openKv(); const entries = kv.list({ prefix: ["preferences"] }); for await (const entry of entries) { console.log(entry.key); // ["preferences", "ada"] console.log(entry.value); // { ... } console.log(entry.versionstamp); // "00000000000000010000" } ``` 返回的键按键前缀后下一个组件的字典顺序排序。因此,具有以下键的 KV 对将按此顺序通过 `kv.list()` 返回: - `["preferences", "ada"]` - `["preferences", "bob"]` - `["preferences", "cassie"]` 读取操作可以在 [**强一致性或最终一致性模式**](./operations) 下执行。强一致性模式保证读取操作将返回最近写入的值。最终一致性模式可能返回过时的值,但速度较快。相比之下,写入始终在强一致性模式下执行。 ## 删除键值对 你可以使用 [`kv.delete()`](https://docs.deno.com/api/deno/~/Deno.Kv.prototype.delete) 从数据库中删除一个键。如果未找到给定键的值,则不执行任何操作。 ```ts const kv = await Deno.openKv(); await kv.delete(["preferences", "alan"]); ``` ## 原子事务 Deno KV 能够执行 [原子事务](./transactions),这使你能够有条件地同时执行一个或多个数据操作。在下面的示例中,我们只有在尚未创建喜好对象的情况下才会创建一个新的喜好对象。 ```ts const kv = await Deno.openKv(); const key = ["preferences", "alan"]; const value = { username: "alan", theme: "light", language: "en-GB", }; const res = await kv.atomic() .check({ key, versionstamp: null }) // `null` 版本戳表示 '没有值' .set(key, value) .commit(); if (res.ok) { console.log("喜好尚不存在。已插入!"); } else { console.error("喜好已存在。"); } ``` 在 Deno KV 中了解更多关于事务的信息 [这里](./transactions)。 ## 通过二级索引改进查询 [二级索引](./secondary_indexes) 通过多个键存储相同的数据,允许更简单地查询所需的数据。假设我们需要能够通过用户名和电子邮件访问用户偏好。为此,你可以提供一个函数,该函数包装保存偏好的逻辑以创建两个索引。 ```ts const kv = await Deno.openKv(); async function savePreferences(prefs) { const key = ["preferences", prefs.username]; // 设置主键 const r = await kv.set(key, prefs); // 将二级键的值设置为主键 await kv.set(["preferencesByEmail", prefs.email], key); return r; } async function getByUsername(username) { // 和之前一样使用... const r = await kv.get(["preferences", username]); return r; } async function getByEmail(email) { // 先通过电子邮件查找键,再进行第二次查找以获取实际数据 const r1 = await kv.get(["preferencesByEmail", email]); const r2 = await kv.get(r1.value); return r2; } ``` 了解更多关于 [二级索引的手册信息](./secondary_indexes)。 ## 监听 Deno KV 的更新 你也可以使用 `kv.watch()` 监听 Deno KV 的更新,它将会发出你提供的键的新的值或多个值。在下面的聊天示例中,我们监听键 `["last_message_id", roomId]` 的更新。我们检索 `messageId`,然后使用 `kv.list()` 从 `seen` 和 `messageId` 中获取所有的新消息。 ```ts let seen = ""; for await (const [messageId] of kv.watch([["last_message_id", roomId]])) { const newMessages = await Array.fromAsync(kv.list({ start: ["messages", roomId, seen, ""], end: ["messages", roomId, messageId, ""], })); await websocket.write(JSON.stringify(newMessages)); seen = messageId; } ``` 了解更多关于 [使用 Deno KV 监听的内容](./operations#watch)。 ## 生产使用 Deno KV 可在 [Deno Deploy](./on_deploy) 上用于实时应用程序。在生产环境中,Deno KV 由 [FoundationDB](https://www.foundationdb.org/) 支持,后者是苹果创建的开源键值存储。 **运行使用 KV 的 Deno 程序在 Deploy 上无须额外配置** - 当你的代码需要时,会为你提供新的 Deploy 数据库。了解更多关于 Deno KV 在 Deno Deploy 上的信息 [这里](./on_deploy)。 ## 测试 默认情况下,[`Deno.openKv()`](https://docs.deno.com/api/deno/~/Deno.openKv) 根据运行调用它的脚本的路径创建或打开一个持久存储。这通常不适合测试,因为测试需要在多次连续运行时产生相同的行为。 要测试使用 Deno KV 的代码,可以使用特殊参数 `":memory:"` 创建一个短暂的 Deno KV 数据存储。 ```ts async function setDisplayName( kv: Deno.Kv, username: string, displayname: string, ) { await kv.set(["preferences", username, "displayname"], displayname); } async function getDisplayName( kv: Deno.Kv, username: string, ): Promise { return (await kv.get(["preferences", username, "displayname"])) .value as string; } Deno.test("Preferences", async (t) => { const kv = await Deno.openKv(":memory:"); await t.step("可以设置 displayname", async () => { const displayName = await getDisplayName(kv, "example"); assertEquals(displayName, null); await setDisplayName(kv, "example", "Exemplary User"); const displayName = await getDisplayName(kv, "example"); assertEquals(displayName, "Exemplary User"); }); }); ``` 这可行是因为 Deno KV 在本地开发时由 SQLite 支持。就像内存 SQLite 数据库一样,多个短暂的 Deno KV 存储可以同时存在而互不干扰。有关特殊数据库寻址模式的更多信息,请参见 [SQLite 文档中的相关主题](https://www.sqlite.org/inmemorydb.html)。 ## 下一步 到这里,你刚刚开始接触 Deno KV。确保查看我们关于 [Deno KV 键空间](./key_space) 的指南,以及 [这里](../tutorials/index.md) 的一系列 [教程和示例应用程序]。 --- # 键过期(键的 TTL) URL: https://docs.deno.com/deploy/kv/key_expiration 自版本 1.36.2 起,Deno KV 支持键过期,允许开发者控制 KV 数据库中键的生存时间(TTL)。这允许与一个键关联一个过期时间戳,在该时间戳之后,键将自动从数据库中删除: ```ts const kv = await Deno.openKv(); // `expireIn` 是键过期的毫秒数。 function addSession(session: Session, expireIn: number) { await kv.set(["sessions", session.id], session, { expireIn }); } ``` 键过期在 Deno CLI 和 Deno Deploy 上均得到支持。 ## 原子性过期多个键 如果在同一原子操作中设置多个键并具有相同的 `expireIn` 值,则这些键的过期将具有原子性。例如: ```ts const kv = await Deno.openKv(); function addUnverifiedUser( user: User, verificationToken: string, expireIn: number, ) { await kv.atomic() .set(["users", user.id], user, { expireIn }) .set(["verificationTokens", verificationToken], user.id, { expireIn }) .commit(); } ``` ## 注意事项 过期时间戳指定了 _最早_ 可以从数据库中删除的时间。实现允许在指定时间戳之后的任何时间删除键,但不得在之前。如果您需要严格执行过期时间(例如出于安全目的),请将其作为值的一个字段添加,并在从数据库中检索到值后进行检查。 --- # 关键空间 URL: https://docs.deno.com/deploy/kv/key_space Deno KV 是一个键值存储。关键空间是一个扁平的命名空间,由键+值+版本戳对组成。键是键部分的序列,允许对分层数据进行建模。值是任意的 JavaScript 对象。版本戳表示值插入/修改的时间。 ## 键 Deno KV 中的键是键部分的序列,这些部分可以是 `string`、`number`、`boolean`、`Uint8Array` 或 `bigint`。 使用一系列部分,而不是一个单一的字符串可以消除分隔符注入攻击的可能性,因为没有可见的分隔符。 > 键注入攻击发生在攻击者通过将键编码方案中使用的分隔符注入到用户控制的变量中,操纵键值存储的结构,导致意外行为或未经授权的访问。例如,考虑到一个使用斜杠 (/) 作为分隔符的键值存储,键如 "users/alice/settings" 和 "users/bob/settings"。攻击者可以创建一个名为 "alice/settings/hacked" 的新用户,从而形成键 "users/alice/settings/hacked/settings",注入分隔符并操纵键结构。在 Deno KV 中,这种注入将导致键 `["users", "alice/settings/hacked", "settings"]`,这并不有害。 在键部分之间,使用不可见的分隔符来分隔这些部分。这些分隔符永远是不可见的,但确保一个部分不会与另一个部分混淆。例如,键部分 `["abc", "def"]`、`["ab", "cdef"]`、`["abc", "", "def"]` 都是不同的键。 键是区分大小写的,并按其部分以字典序排列。第一部分是最重要的,最后一部分是最不重要的。部分的顺序由部分的类型和数值共同决定。 ### 键部分排序 键部分按类型的字典序排序,在给定类型内,按其值排序。类型的排序如下: 1. `Uint8Array` 1. `string` 1. `number` 1. `bigint` 1. `boolean` 在给定类型内,排序为: - `Uint8Array`: 数组的字节排序 - `string`: 字符串的 UTF-8 编码的字节排序 - `number`: -Infinity < -1.0 < -0.5 < -0.0 < 0.0 < 0.5 < 1.0 < Infinity < NaN - `bigint`: 数学排序,最大负数在前,最大正数在后 - `boolean`: false < true 这意味着部分 `1.0`(一个数字)的排序在部分 `2.0`(也是一个数字)之前,但大于部分 `0n`(一个 bigint),因为 `1.0` 是一个数字,而 `0n` 是一个 bigint,类型排序优先于在类型内的值排序。 ### 键示例 ```js ["users", 42, "profile"]; // ID 为 42 的用户的个人资料 ["posts", "2023-04-23", "comments"]; // 2023-04-23 所有帖子评论 ["products", "electronics", "smartphones", "apple"]; // 电子类别中的苹果智能手机 ["orders", 1001, "shipping", "tracking"]; // 订单 ID 1001 的追踪信息 ["files", new Uint8Array([1, 2, 3]), "metadata"]; // 带有 Uint8Array 标识符的文件元数据 ["projects", "openai", "tasks", 5]; // OpenAI 项目中 ID 为 5 的任务 ["events", "2023-03-31", "location", "san_francisco"]; // 2023-03-31 在旧金山的事件 ["invoices", 2023, "Q1", "summary"]; // 2023 年 Q1 发票的摘要 ["teams", "engineering", "members", 1n]; // 工程团队中 ID 为 1n 的成员 ``` ### 通用唯一字典序可排序标识符 (ULID) 键部分排序允许时间戳和 ID 部分组成的键按时间顺序列出。通常,你可以使用以下方式生成一个键: [`Date.now()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now) 和 [`crypto.randomUUID()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID): ```js async function setUser(user) { await kv.set(["users", Date.now(), crypto.randomUUID()], user); } ``` 连续运行多次,这将生成以下键: ```js ["users", 1691377037923, "8c72fa25-40ad-42ce-80b0-44f79bc7a09e"]; // 第一个用户 ["users", 1691377037924, "8063f20c-8c2e-425e-a5ab-d61e7a717765"]; // 第二个用户 ["users", 1691377037925, "35310cea-58ba-4101-b09a-86232bf230b2"]; // 第三个用户 ``` 然而,在某些情况下,将时间戳和 ID 表示在一个键部分中可能更直接。你可以使用一个 [通用唯一字典序可排序标识符 (ULID)](https://github.com/ulid/spec) 来做到这一点。这种标识符编码了 UTC 时间戳,字典序可排序,并且默认情况下是加密随机的: ```js import { ulid } from "jsr:@std/ulid"; const kv = await Deno.openKv(); async function setUser(user) { await kv.set(["users", ulid()], user); } ``` ```js ["users", "01H76YTWK3YBV020S6MP69TBEQ"]; // 第一个用户 ["users", "01H76YTWK4V82VFET9YTYDQ0NY"]; // 第二个用户 ["users", "01H76YTWK5DM1G9TFR0Y5SCZQV"]; // 第三个用户 ``` 此外,你还可以使用 `monotonicUlid` 函数生成单调递增的 ULID: ```js import { monotonicUlid } from "jsr:@std/ulid"; async function setUser(user) { await kv.set(["users", monotonicUlid()], user); } ``` ```js // 对于同一时间戳进行严格排序,通过将最低有效随机位递增 1 ["users", "01H76YTWK3YBV020S6MP69TBEQ"]; // 第一个用户 ["users", "01H76YTWK3YBV020S6MP69TBER"]; // 第二个用户 ["users", "01H76YTWK3YBV020S6MP69TBES"]; // 第三个用户 ``` ## 值 Deno KV 中的值可以是与 [结构化克隆算法][structured clone algorithm] 兼容的任意 JavaScript 值。这包括: - `undefined` - `null` - `boolean` - `number` - `string` - `bigint` - `Uint8Array` - `Array` - `Object` - `Map` - `Set` - `Date` - `RegExp` 对象和数组可以包含上述任何类型,包括其他对象和数组。`Map` 和 `Set` 可以包含上述任何类型,包括其他 `Map` 和 `Set`。 值中的循环引用是支持的。 不支持具有非原始原型的对象(例如类实例或 Web API 对象)。函数和符号也不能被序列化。 ### `Deno.KvU64` 类型 除了结构化可序列化值外,特殊值 `Deno.KvU64` 也被支持。这个对象表示一个 64 位无符号整数,以 bigint 形式表示。它可以与 `sum`、`min` 和 `max` KV 操作一起使用。它不能存储在对象或数组中。它必须作为顶级值存储。 可以通过 `Deno.KvU64` 构造函数创建: ```js const u64 = new Deno.KvU64(42n); ``` ### 值示例 ```js,ignore undefined; null; true; false; 42; -42.5; 42n; "hello"; new Uint8Array([1, 2, 3]); [1, 2, 3]; { a: 1, b: 2, c: 3 }; new Map([["a", 1], ["b", 2], ["c", 3]]); new Set([1, 2, 3]); new Date("2023-04-23"); /abc/; // 循环引用是支持的 const a = {}; const b = { a }; a.b = b; // Deno.KvU64 是支持的 new Deno.KvU64(42n); ``` ## 版本戳 Deno KV 键空间中的所有数据都是有版本的。每次插入或修改一个值时,会为其分配一个版本戳。版本戳是单调递增的、非顺序的、12 字节的值,表示值被修改的时间。版本戳并不表示实际时间,而是表示值被修改的顺序。 由于版本戳是单调递增的,因此可以用来判断某个值是否比另一个值更新。这可以通过比较两个值的版本戳来完成。如果版本戳 A 大于版本戳 B,则值 A 的修改时间比值 B 更新。 ```js versionstampA > versionstampB; "000002fa526aaccb0000" > "000002fa526aacc90000"; // true ``` 由单个事务修改的所有数据都被分配相同的版本戳。这意味着如果在同一原子操作中执行两个 `set` 操作,则新值的版本戳将是相同的。 版本戳用于实现乐观并发控制。原子操作可以包含检查,确保它们操作的数据的版本戳与传递给操作的版本戳匹配。如果数据的版本戳与传递给操作的版本戳不同,则事务将失败,操作将不被应用。 [结构化克隆算法]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm --- # 在 Node.js 中使用 KV URL: https://docs.deno.com/deploy/kv/node 在 Node.js 中连接 Deno KV 数据库可以通过我们的 [官方客户端库在 npm 上](https://www.npmjs.com/package/@deno/kv)进行支持。您可以在下面找到此选项的使用说明。 ## 安装和使用 使用您喜欢的 npm 客户端通过以下命令之一来安装 Node.js 的客户端库。 ```sh npm install @deno/kv ``` ```sh pnpm add @deno/kv ``` ```sh yarn add @deno/kv ``` 一旦您将包添加到 Node 项目中,就可以导入 `openKv` 函数(支持 ESM `import` 和 CJS `require` 基于的用法): ```js import { openKv } from "@deno/kv"; // 连接到一个 KV 实例 const kv = await openKv(""); // 写入一些数据 await kv.set(["users", "alice"], { name: "Alice" }); // 读取数据 const result = await kv.get(["users", "alice"]); console.log(result.value); // { name: "Alice" } ``` 默认情况下,用于身份验证的访问令牌来自 `DENO_KV_ACCESS_TOKEN` 环境变量。您也可以明确传递它: ```js import { openKv } from "@deno/kv"; const kv = await openKv("", { accessToken: myToken }); ``` 一旦您的 Deno KV 客户端初始化,Deno 中可用的相同 API 也可以在 Node 中使用。 ## KV 连接 URL 在 Deno 之外连接到 KV 数据库需要一个 [KV Connect](https://github.com/denoland/denokv/blob/main/proto/kv-connect.md) URL。一个在 Deno Deploy 中托管的数据库的 KV 连接 URL 将是这种格式:`https://api.deno.com/databases//connect`。 您项目的 `database-id` 可以在 [Deno Deploy 仪表板](https://dash.deno.com/projects)中找到,在项目的 "KV" 标签下。 ![Deploy 中连接字符串的位置](./images/kv-connect.png) ## 更多信息 有关如何在 Node 中使用 Deno KV 模块的更多信息可以在项目的 [README 页面](https://www.npmjs.com/package/@deno/kv)上找到。 --- # 操作 URL: https://docs.deno.com/deploy/kv/operations Deno KV API 提供了一组可以在键空间上执行的操作。 有两个操作用于从存储中读取数据,还有五个操作用于将数据写入存储。 读取操作可以在强一致性模式或最终一致性模式下执行。强一致性模式保证读取操作会返回最近写入的值。最终一致性模式可能返回过期值,但速度更快。 写入操作始终在强一致性模式下执行。 ## `get` `get` 操作返回与给定键关联的值和版本戳。如果值不存在,get 将返回 `null` 值和版本戳。 可以使用两个 API 执行 `get` 操作。 [`Deno.Kv.prototype.get(key, options?)`][get] API,可以用来读取单个键,以及 [`Deno.Kv.prototype.getMany(keys, options?)`][getMany] API,可以用来一次读取多个键。 获取操作在所有一致性模式下都作为“快照读取”执行。这意味着在一次检索多个键时,返回的值将彼此一致。 ```ts const res = await kv.get(["config"]); console.log(res); // { key: ["config"], value: "value", versionstamp: "000002fa526aaccb0000" } const res = await kv.get(["config"], { consistency: "eventual" }); console.log(res); // { key: ["config"], value: "value", versionstamp: "000002fa526aaccb0000" } const [res1, res2, res3] = await kv.getMany<[string, string, string]>([ ["users", "sam"], ["users", "taylor"], ["users", "alex"], ]); console.log(res1); // { key: ["users", "sam"], value: "sam", versionstamp: "00e0a2a0f0178b270000" } console.log(res2); // { key: ["users", "taylor"], value: "taylor", versionstamp: "0059e9035e5e7c5e0000" } console.log(res3); // { key: ["users", "alex"], value: "alex", versionstamp: "00a44a3c3e53b9750000" } ``` ## `list` `list` 操作返回与给定选择器匹配的键的列表。这些键关联的值和版本戳也会被返回。可以使用 2 个不同的选择器来过滤匹配的键。 `prefix` 选择器匹配所有以给定前缀键部分开头的键,但不包括精确匹配该键的情况。前缀选择器可以选择性地给定一个 `start` 或 `end` 键来限制返回的键的范围。`start` 键是包含的,`end` 键是不包含的。 `range` 选择器匹配所有在给定的 `start` 和 `end` 键之间的键。`start` 键是包含的,`end` 键是不包含的。 > 注意:在前缀选择器的情况下,`prefix` 键必须仅由完整(而非部分)键部分组成。例如,如果存储中存在键 `["foo", "bar"]`,则前缀选择器 `["foo"]` 将匹配它,但前缀选择器 `["f"]` 将不会。 list 操作可以选择性地给定一个 `limit` 来限制返回的键数量。 可以使用 [`Deno.Kv.prototype.list(selector, options?)`][list] 方法执行列表操作。该方法返回一个 `Deno.KvListIterator`,可以用来遍历返回的键。这是一个异步迭代器,可以与 `for await` 循环一起使用。 ```ts // 返回所有用户 const iter = kv.list({ prefix: ["users"] }); const users = []; for await (const res of iter) users.push(res); console.log(users[0]); // { key: ["users", "alex"], value: "alex", versionstamp: "00a44a3c3e53b9750000" } console.log(users[1]); // { key: ["users", "sam"], value: "sam", versionstamp: "00e0a2a0f0178b270000" } console.log(users[2]); // { key: ["users", "taylor"], value: "taylor", versionstamp: "0059e9035e5e7c5e0000" } // 返回前 2 个用户 const iter = kv.list({ prefix: ["users"] }, { limit: 2 }); const users = []; for await (const res of iter) users.push(res); console.log(users[0]); // { key: ["users", "alex"], value: "alex", versionstamp: "00a44a3c3e53b9750000" } console.log(users[1]); // { key: ["users", "sam"], value: "sam", versionstamp: "00e0a2a0f0178b270000" } // 返回在 "taylor" 之后的所有用户 const iter = kv.list({ prefix: ["users"], start: ["users", "taylor"] }); const users = []; for await (const res of iter) users.push(res); console.log(users[0]); // { key: ["users", "taylor"], value: "taylor", versionstamp: "0059e9035e5e7c5e0000" } // 返回在 "taylor" 之前的所有用户 const iter = kv.list({ prefix: ["users"], end: ["users", "taylor"] }); const users = []; for await (const res of iter) users.push(res); console.log(users[0]); // { key: ["users", "alex"], value: "alex", versionstamp: "00a44a3c3e53b9750000" } console.log(users[1]); // { key: ["users", "sam"], value: "sam", versionstamp: "00e0a2a0f0178b270000" } // 返回 以 "a" 和 "n" 之间的字符开头的所有用户 const iter = kv.list({ start: ["users", "a"], end: ["users", "n"] }); const users = []; for await (const res of iter) users.push(res); console.log(users[0]); // { key: ["users", "alex"], value: "alex", versionstamp: "00a44a3c3e53b9750000" } ``` list 操作从存储中批量读取数据。可以使用 `batchSize` 选项控制每批的大小。默认批大小是 500 个键。批内的数据在单个快照读取中读取,因此值彼此一致。一致性模式适用于每批读取的数据。在批次之间,数据是一致的。批次之间的边界从 API 中是不可见的,因为迭代器返回单个键。 list 操作可以通过将 `reverse` 选项设置为 `true` 来反向执行。这将返回以字母顺序降序排列的键。`start` 和 `end` 键仍然分别是包含和不包含的,并且仍然被解释为字母顺序升序。 ```ts // 以反向顺序返回所有用户,截止到 "sam" const iter = kv.list({ prefix: ["users"], start: ["users", "sam"] }, { reverse: true, }); const users = []; for await (const res of iter) users.push(res); console.log(users[0]); // { key: ["users", "taylor"], value: "taylor", versionstamp: "0059e9035e5e7c5e0000" } console.log(users[1]); // { key: ["users", "sam"], value: "sam", versionstamp: "00e0a2a0f0178b270000" } ``` > 注意:在上述示例中,我们将 `start` 键设置为 `["users", "sam"]`,即使返回的第一个键是 `["users", "taylor"]`。这是因为 `start` 和 `end` 键始终以字母顺序升序评估,即使在以反向顺序执行列表操作时(返回的键按字母顺序降序)。 ## `set` `set` 操作在存储中设置键的值。如果键不存在,则创建该键。如果键已经存在,则其值将被覆盖。 可以使用 [`Deno.Kv.prototype.set(key, value)`][set] 方法执行 `set` 操作。该方法返回一个 `Promise`,解析为一个 `Deno.KvCommitResult` 对象,其中包含提交的 `versionstamp`。 set 操作始终在强一致性模式下执行。 ```ts const res = await kv.set(["users", "alex"], "alex"); console.log(res.versionstamp); // "00a44a3c3e53b9750000" ``` ## `delete` `delete` 操作从存储中删除一个键。如果该键不存在,则操作为无效操作。 可以使用 [`Deno.Kv.prototype.delete(key)`][delete] 方法执行 `delete` 操作。 删除操作始终在强一致性模式下执行。 ```ts await kv.delete(["users", "alex"]); ``` ## `sum` `sum` 操作原子地将一个值添加到存储中的一个键。如果该键不存在,则创建该键并设置为该值得和。如果该键已经存在,则其值将被添加到和中。 `sum` 操作只能作为原子操作的一部分执行。可以使用 [`Deno.AtomicOperation.prototype.mutate({ type: "sum", value })`][mutate] 方法将和变换添加到原子操作中。 sum 操作只能在类型为 `Deno.KvU64` 的值上执行。操作数和存储中的值必须都是类型为 `Deno.KvU64`。 如果键的新值大于 `2^64 - 1` 或小于 `0`,sum 操作将回绕。例如,如果存储中的值是 `2^64 - 1` 而操作数是 `1`,那么新值将为 `0`。 sum 操作始终在强一致性模式下执行。 ```ts await kv.atomic() .mutate({ type: "sum", key: ["accounts", "alex"], value: new Deno.KvU64(100n), }) .commit(); ``` ## `min` `min` 操作原子地将键设置为其当前值和给定值中的最小值。如果该键不存在,则用给定值创建该键。如果该键已经存在,则其值将被设置为其当前值和给定值中的最小值。 `min` 操作只能作为原子操作的一部分执行。可以使用 [`Deno.AtomicOperation.prototype.mutate({ type: "min", value })`][mutate] 方法将最小值变换添加到原子操作中。 min 操作只能在类型为 `Deno.KvU64` 的值上执行。操作数和存储中的值必须都是类型为 `Deno.KvU64`。 min 操作始终在强一致性模式下执行。 ```ts await kv.atomic() .mutate({ type: "min", key: ["accounts", "alex"], value: new Deno.KvU64(100n), }) .commit(); ``` ## `max` `max` 操作原子地将键设置为其当前值和给定值中的最大值。如果该键不存在,则用给定值创建该键。如果该键已经存在,则其值将被设置为其当前值和给定值中的最大值。 `max` 操作只能作为原子操作的一部分执行。可以使用 [`Deno.AtomicOperation.prototype.mutate({ type: "max", value })`][mutate] 方法将最大值变换添加到原子操作中。 max 操作只能在类型为 `Deno.KvU64` 的值上执行。操作数和存储中的值必须都是类型为 `Deno.KvU64`。 max 操作始终在强一致性模式下执行。 ```ts await kv.atomic() .mutate({ type: "max", key: ["accounts", "alex"], value: new Deno.KvU64(100n), }) .commit(); ``` ## `watch` `watch` 操作接受一个键的数组,并返回一个 [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream),当任何被观察的键更改其 `versionstamp` 时,会发出一个新值。发出的值是一个 [Deno.KvEntryMaybe](https://docs.deno.com/api/deno/~/Deno.KvEntryMaybe) 对象的数组。 请注意,返回的流不会返回被观察的键的每个中间状态,而是让您与键的最新状态保持同步。这意味着如果一个键被快速多次修改,您可能不会收到每次变化的通知,而是会收到该键的最新状态。 ```ts const db = await Deno.openKv(); const stream = db.watch([["foo"], ["bar"]]); for await (const entries of stream) { entries[0].key; // ["foo"] entries[0].value; // "bar" entries[0].versionstamp; // "00000000000000010000" entries[1].key; // ["bar"] entries[1].value; // null entries[1].versionstamp; // null } ``` [get]: https://docs.deno.com/api/deno/~/Deno.Kv.prototype.get [getMany]: https://docs.deno.com/api/deno/~/Deno.Kv.prototype.getMany [list]: https://docs.deno.com/api/deno/~/Deno.Kv.prototype.list [set]: https://docs.deno.com/api/deno/~/Deno.Kv.prototype.set [delete]: https://docs.deno.com/api/deno/~/Deno.Kv.prototype.delete [mutate]: https://docs.deno.com/api/deno/~/Deno.AtomicOperation.prototype.mutate --- # 二级索引 URL: https://docs.deno.com/deploy/kv/secondary_indexes 像 Deno KV 这样的键值存储将数据组织为键值对的集合,其中每个唯一的键都与单个值关联。这样的结构使得可以根据键轻松检索值,但不允许根据值本身进行查询。为了克服这一限制,您可以创建二级索引,它在包含(部分)该值的附加键下存储相同的值。 :::tip 推荐的指针索引方法 倾向于在二级索引中将主键(或对主键的紧凑引用)作为值存储。这可以减少存储使用,并避免维护多个相同数据副本的同步。代价是在通过索引查询时需要双重读取(索引 → 主键)。 优点 - 降低存储和写入放大 - 在非索引字段更改时减少更新次数 - 事务更新更清晰:同时更新主键和索引 缺点 - 需要第二次读取以解析主值 - 必须以原子方式维护引用完整性(在单个事务中创建/更新/删除) ::: 在使用二级索引时,保持主键与二级键之间的一致性至关重要。如果在主键处更新了一个值,但没有在二级键处更新,则通过针对二级键的查询返回的数据将是不正确的。为了确保主键和二级键始终表示相同的数据,在插入、更新或删除数据时使用原子操作。这种方法确保一组变更操作作为一个单元执行,或者全部成功或者全部失败,从而防止不一致。 ## 唯一索引(一对一) 唯一索引将索引中的每个键与确切的一个主键关联。例如,当存储用户数据并通过唯一的 ID 和电子邮件地址查找用户时,可以将用户数据存储在两个单独的键下:一个用于主键(用户 ID),另一个用于二级索引(电子邮件 → 用户 ID)。这种设置允许根据用户的 ID 或电子邮件查询用户。二级索引还可以对存储中的值施加唯一性约束。在用户数据的例子中,使用该索引确保每个电子邮件地址只与一个用户关联——换句话说,确保电子邮件是唯一的。 要实现这个例子的唯一二级索引,请按照以下步骤操作: 1. 创建一个表示数据的 `User` 接口: ```ts interface User { id: string; name: string; email: string; } ``` 2. 定义一个 `insertUser` 函数,在主键和二级键上存储用户数据: ```ts async function insertUser(user: User) { const primaryKey = ["users", user.id] as const; const byEmailKey = ["users_by_email", user.email.toLowerCase()] as const; const res = await kv.atomic() .check({ key: primaryKey, versionstamp: null }) .check({ key: byEmailKey, versionstamp: null }) .set(primaryKey, user) // 存储指针,而非完整用户数据 .set(byEmailKey, user.id) .commit(); if (!res.ok) { throw new TypeError("ID 或电子邮件已存在的用户"); } } ``` > 该函数使用原子操作进行插入,检查没有具有相同 ID 或电子邮件的用户存在。如果违反其中任何一项约束,插入将失败且不会修改任何数据。 3. 定义一个 `getUser` 函数,根据用户 ID 检索用户: ```ts async function getUser(id: string): Promise { const res = await kv.get(["users", id]); return res.value; } ``` 4. 定义一个 `getUserByEmail` 函数,通过电子邮件地址检索用户: ```ts async function getUserByEmail(email: string): Promise { const idRes = await kv.get([ "users_by_email", email.toLowerCase(), ]); if (!idRes.value) return null; const res = await kv.get(["users", idRes.value]); return res.value; } ``` 该函数使用二级键进行存储查询(`["users_by_email", email]`)。 5. 定义一个 `deleteUser` 函数,通过用户 ID 删除用户: ```ts async function deleteUser(id: string) { let res = { ok: false } as { ok: boolean }; while (!res.ok) { const cur = await kv.get(["users", id]); if (cur.value === null) return; res = await kv.atomic() .check(cur) .delete(["users", id]) .delete(["users_by_email", cur.value.email.toLowerCase()]) .commit(); } } ``` > 该函数首先通过用户 ID 检索用户,以获取用户的电子邮件地址。这是获取用户地址的二级索引键所需的。然后执行原子操作,检查数据库中的用户未更改,并删除指向用户值的主键和二级键。如果此操作失败(用户在查询和删除之间被修改),则原子操作将中止。整个过程将重试直到删除成功。检查是必需的,以防止在检索和删除之间值被修改的竞争条件。如果更新更改了用户的电子邮件,则会发生这种竞争,因为在这种情况下二级索引发生变化。然后二级索引的删除将失败,因为删除的目标是旧的二级索引键。 ## 非唯一索引(一对多) 非唯一索引是二级索引,其中单个键可以与多个主键关联,使您能够根据共享属性查询多个项目。例如,当根据用户的最爱颜色查询用户时,可以使用非唯一二级索引来实现。最爱颜色是一个非唯一属性,因为多个用户可以拥有相同的最爱颜色。 要为这个例子实现一个非唯一二级索引,请按照以下步骤操作: 1. 定义 `User` 接口: ```ts interface User { id: string; name: string; favoriteColor: string; } ``` 2. 定义 `insertUser` 函数: ```ts async function insertUser(user: User) { const primaryKey = ["users", user.id] as const; const byColorKey = [ "users_by_favorite_color", user.favoriteColor, user.id, ] as const; await kv.atomic() .check({ key: primaryKey, versionstamp: null }) .set(primaryKey, user) // 存储指针,而非完整用户数据 .set(byColorKey, user.id) .commit(); } ``` 3. 定义一个函数,根据用户的最爱颜色检索用户: ```ts async function getUsersByFavoriteColor(color: string): Promise { const iter = kv.list({ prefix: ["users_by_favorite_color", color], }); const ids: string[] = []; for await (const { value: id } of iter) { ids.push(id); } if (ids.length === 0) return []; const results = await kv.getMany( ids.map((id) => ["users", id] as const), ); return results.map((r) => r.value!).filter(Boolean); } ``` 这个例子演示了非唯一二级索引的使用,`users_by_favorite_color`,该索引允许根据用户的最爱颜色进行查询。主键仍然是用户的 `id`。 唯一索引和非唯一索引的实现主要区别在于二级键的结构和组织。在唯一索引中,每个二级键与确切的一个主键关联,确保索引属性在所有记录中是唯一的。在非唯一索引的情况下,单个二级键可以与多个主键关联,因为索引属性可能在多个记录中共享。为了实现这一点,非唯一二级键通常以附加的唯一标识符(例如主键)作为键的一部分来构建,从而允许多个具有相同属性的记录共存,而不会发生冲突。 ### 在何种情况下复制值可能是可接受的 虽然推荐使用指针索引,但在以下情况下,在二级索引中复制完整值是可以接受的: - 值较小且读取几乎完全通过二级索引发生 - 希望避免第二次读取,可以容忍额外的存储开销 - 可以通过原子事务可靠地保持主键和二级索引同步 如果复制值,确保插入/更新/删除操作在同一个原子事务中同时修改两个键。 ### 从复制值索引迁移 迁移已存在的复制值索引到指针索引: 1. 回填:扫描主键并将二级索引值设置为主键(例如用户 ID)。 2. 切换:更新写入路径以维护指针索引;临时保留旧索引以支持读取。 3. 清理:切换读取器使用指针索引,然后删除重复的索引条目。 --- # 事务 URL: https://docs.deno.com/deploy/kv/transactions Deno KV 存储利用 _乐观并发控制事务_,而不是像 PostgreSQL 或 MySQL 等许多 SQL 系统那样使用 _交互式事务_。这种方法使用版本戳来表示给定键的值的当前版本,通过不使用锁的方式来管理对共享资源的并发访问。当发生读取操作时,系统除了返回相关键的值外,还会返回一个版本戳。 要执行事务,可以执行一个原子操作,该操作可以包含多个变更操作(例如设置或删除)。与这些操作一起,提供键+版本戳对作为事务成功的条件。乐观并发控制事务只有在指定的版本戳与数据库中对应键的值的当前版本匹配时才会提交。这种事务模型在允许 Deno KV 存储中的并发交互的同时,确保了数据的一致性和完整性。 由于 OCC 事务是乐观的,它们在提交时可能会失败,因为原子操作中指定的版本约束被违反。这发生在代理在读取和提交之间更新了事务中使用的键。当这种情况发生时,执行事务的代理必须重试事务。 为了说明如何在 Deno KV 中使用 OCC 事务,下面的例子展示了如何实现一个 `transferFunds(from: string, to: string, amount: number)` 函数用于账户分类账。账户分类账在键值存储中存储每个账户的余额。键前缀为 `"account"`,后跟账户标识符:`["account", "alice"]`。为每个键存储的值是一个表示账户余额的数字。 以下是实现 `transferFunds` 函数的逐步示例: ```ts async function transferFunds(sender: string, receiver: string, amount: number) { if (amount <= 0) throw new Error("金额必须为正数"); // 构造发送者和接收者账户的 KV 键。 const senderKey = ["account", sender]; const receiverKey = ["account", receiver]; // 重试事务直到成功。 let res = { ok: false }; while (!res.ok) { // 读取两个账户的当前余额。 const [senderRes, receiverRes] = await kv.getMany([senderKey, receiverKey]); if (senderRes.value === null) { throw new Error(`未找到账户 ${sender}`); } if (receiverRes.value === null) { throw new Error(`未找到账户 ${receiver}`); } const senderBalance = senderRes.value; const receiverBalance = receiverRes.value; // 确保发送者有足够的余额来完成转账。 if (senderBalance < amount) { throw new Error( `账户 ${sender} 的余额不足以转账 ${amount}`, ); } // 执行转账。 const newSenderBalance = senderBalance - amount; const newReceiverBalance = receiverBalance + amount; // 尝试提交事务。如果事务由于检查失败而无法提交,`res` 返回一个对象,包含 `ok: false` // (即键的版本戳已更改) res = await kv.atomic() .check(senderRes) // 确保发送者的余额没有改变。 .check(receiverRes) // 确保接收者的余额没有改变。 .set(senderKey, newSenderBalance) // 更新发送者的余额。 .set(receiverKey, newReceiverBalance) // 更新接收者的余额。 .commit(); } } ``` 在这个例子中,`transferFunds` 函数读取两个账户的余额和版本戳,计算转账后的新余额,并检查账户 A 是否有足够的资金。然后,它执行一个原子操作,使用版本戳约束设置新的余额。如果事务成功,循环退出。如果版本约束被违反,事务失败,循环重试事务直到成功。 ## 限制 除了最大键大小为 2 KiB 和最大值大小为 64 KiB 外,Deno KV 事务 API 还有一些特定的限制: - **每个 `kv.getMany()` 的最大键数**:10 - **每个 `kv.list()` 的最大批处理大小**:1000 - **原子操作中的最大检查数**:100 - **原子操作中的最大变更数**:1000 - **原子操作的最大总大小**:800 KiB。包括所有键和值的检查和变更,以及编码开销也算入此限制。 - **键的最大总大小**:90 KiB。包括所有键的检查和变更,以及编码开销也算入此限制。 - **每个 `kv.watch()` 的最大监视键数**:10 --- # 定价和限制 > Deno Deploy的重要限制、服务等级预期及使用条款。 URL: https://docs.deno.com/deploy/pricing_and_limits 请参阅 [我们的定价页面](https://deno.com/deploy/pricing),了解所有套餐中可用功能的概览。如果您的使用场景超出了这些限制,[请联系我们](mailto:deploy@deno.com)。 在 Deno Deploy 的初始公开测试阶段不提供正常运行时间保证。对服务的访问将受到 [我们的可接受使用政策](/deploy/acceptable_use_policy)的控制。任何被我们认为违反此政策的用户,都可能面临账号被终止的风险。 ## 部署的最大大小 上传资产到部署时,所有文件(源文件和静态文件)的总大小 **不应超过1GB**。 ## 内存分配 应用程序的最大内存分配为 512MB。 ## 上传请求限制 只要您的应用程序符合 [我们的可接受使用政策](/deploy/acceptable_use_policy), 我们不限制您的应用程序可以处理的上传请求数量。 ## TLS 代理 对于端口 443(HTTPS使用端口)的外发连接,需进行 TLS 终止。禁止使用 [Deno.connect](https://docs.deno.com/api/deno/~/Deno.connect) 连接这些端口。如果需要建立到 443 端口的 TLS 连接,请改用 [Deno.connectTls](https://docs.deno.com/api/deno/~/Deno.connectTls)。`fetch` 不受此限制。 此限制的原因是,未终止 TLS 直接连接到 443 端口的情况通常用于 TLS-over-TLS 代理,而根据 [我们的可接受使用政策](/deploy/acceptable_use_policy),在 Deno Deploy 中禁止使用此类代理形式。 --- # Privacy Policy > Deno's Privacy Policy URL: https://docs.deno.com/deploy/privacy_policy **DENO PRIVACY POLICY** 09 September 2024 Deno Land Inc. (“Deno,” “we,” “us,” or “our”) collects and uses personal information in order to provide its products and services to you. This Privacy Policy (the “Policy”) describes the personal information we collect, the purposes for which we use it, the parties with whom we may share it, and your choices with respect to such information. For purposes of this Privacy Policy, “personal information” means any information that relates to you as an individual and could reasonably be used to identify you. This Privacy Policy applies to our collection and use of personal information through (i) our website at [https://deno.com](https://deno.com) (the “Site”); (ii) any websites, applications or other digital properties that link to this Privacy Policy; and (iii) the products and services (the “Deno Offerings”) we offer to you on our proprietary platform (the “Platform”) via the following websites: - Deno Deploy ([https://deno.com/deploy](https://deno.com/deploy)) - Deno Deploy Classic ([https://deno.com/deploy/classic](https://deno.com/deploy/classic)) - Deno Subhosting ([https://deno.com/subhosting](https://deno.com/subhosting)) By accessing or using the Site or any other digital property that links to this Privacy Policy, you may learn about Deno and our technology platform, and registered customers may also access the Deno Offerings (collectively, the “Services”). To the extent permitted by applicable law, your use of Deno’ products and services constitutes your acknowledgment and/or consent to the practices described in this Policy. This Privacy Policy incorporates [Deno’s Terms and Conditions](https://docs.deno.com/deploy/terms_and_conditions/) (the “Terms”). Capitalized terms that are not defined in the Privacy Policy have the meaning given to them in the Terms. **I. The Information We Collect, And How We Collect It** We collect the following categories of information, which may include personal information (collectively, the “**Information**”). **1\. Information You Provide To Us** We collect information from and about you directly when you provide it to us. This information may be collected when you contact us, fill out a form, create an account, subscribe to our blog, access or participate on our Sites, respond to surveys, or otherwise interact with us. This information may include: _Contact Information._ We collect your contact information when you voluntarily provide it to us. For example, you may disclose contact information to us via the “Contact” link on our Sites, submit information by mail, telephone, in person or electronically, when signing up for our newsletters and other marketing communications, or when you register to attend an event or program. Contact Information typically includes first name, last name, e-mail address, postal address, organization, telephone number and other information that identifies you or can be used to identify or contact you. _Account Credentials_. When you register to create an account with us, we will collect certain additional personal information, including your name, email address, and potentially other information such as your GitHub user name and public GitHub profile. In addition to Contact Information and Account Credentials, we may collect other kinds of information, such as: - Comments, questions, and requests you may make; - Information about your preferences, such as your preferred methods of communication and the types of information in which you are interested; - Event and service-related information (such as information required for registration, access to premises or online resources, dietary restrictions, and areas of interest); - Audio and visual information, such as photographs, video and voice recordings (e.g., from events you attended with us), or security camera recordings if you visit our premises; - Details of downloads from our Sites; - Records and copies of your correspondence (including email addresses and phone numbers), if you contact us; and - Any other information you voluntarily provide. **2\. Information Obtained From Third Parties** We may receive certain information about you from other sources, including publicly available sources (such as public records and social media platforms), as well as our service providers and marketing partners. When we collect personal information from users and visitors of other sites on which you have interacted with us, we will do so in accordance with the terms of use and privacy policies of those sites and applicable law. We may also receive personal information when you comment on our social media advertisements, post comments about us, or tag us in a public-facing social media post. Personal information may also be collected by the third-party social media sites that host our social media pages. These sites may provide aggregate information and analysis to us about visitors’ use of our social media pages. This allows us to better understand and analyze our user growth, general demographic information about the users of these pages, and interaction with the content that we post. Overall, this information may be used to help us understand the types of visitors and users of our social media pages and use of the content. This Privacy Policy does not cover personal information collected by such third-party social media sites. For more information on their privacy and security practices please review the privacy policies and terms of use on their respective websites. **3\. Information Collected Automatically** We and our service providers may automatically obtain certain information about you, your electronic device, and your interactions with us, including the following: - _Device data_. We may collect data such as the type of device and its operating system and settings, browser type, mobile device carrier, country, IP address, and unique identifiers. - _Internet and other or electronic activity data_. This includes information about your interaction with our Sites, emails, and other online content. - _Tracking Data_. We may collect tracking data using first and third-party cookies, pixels, web server logs, web beacons, and similar data collection and tracking technologies on the Sites, third party websites, apps and online services, and across your devices (such as IP address, browser, type, ISP, platform type, device type). Third parties such as advertising networks and analytics providers may also collect information about your online activities over time and across different websites and devices when you access or use the Sites. **II. How We Use And Share Your Information** Deno uses the Information for the purpose for which it was collected and in a manner that is consistent with this Privacy Policy. These functions include operation, maintenance and improvements to the Sites, providing our products and services, solicitation of your feedback, gaining a better understanding of our customers and visitors of our Sites, responding to your requests and questions, hosting events, and informing you about our organization, products, services, events, and other areas of interest. _Analytics Services_. We may use third-party web analytics services, such as Google Analytics, to help us understand and analyze how Site visitors use our services. For more information on how Google Analytics uses data collected through our Sites, visit [www.google.com/policies/privacy/partners](http://www.google.com/policies/privacy/partners). _Aggregated Data_. We may analyze your personal information in aggregate form which does not identify you personally (“**Aggregated Data**”). The Aggregated Data may be used to operate, maintain, manage, and improve the Sites, shared with our affiliates, agents, and business partners, and otherwise used and disclosed for lawful business purposes. We do not re-identify de-identified or aggregated information. _Service Providers/Vendors_. Like many businesses, we hire other companies to perform certain business-related services. We may disclose personal information to certain types of third party companies but only to the extent needed to enable them to provide such services, for example web hosting, disaster recovery, client survey and marketing, and data storage. _Reorganization_. If, in the future, Deno undergoes a corporate, partnership, or business reorganization, we may transfer the Information, including personal information, to the new or surviving entity.  _Protection of Rights and Compliance_. We may use your Information to protect the rights, privacy or safety of you, us or others; to ensure our compliance with legal and contractual requirements; and to prevent and investigate illegal, unethical, or unauthorized activities (including cyberattacks and identity theft). If Deno intends on using or disclosing your personal information in any manner that is not consistent with this Privacy Policy, you will be informed of such anticipated use prior to or at the time at which the personal information is collected. **III. How We Protect Your Information** We take commercially reasonable steps to protect your personal information from loss, misuse, and unauthorized access, disclosure, alteration, or destruction. Please understand, however, that no security system is impenetrable. We cannot guarantee the security of our databases, nor can we guarantee that the personal information that you supply will not be intercepted while being transmitted to and from us over the Internet. **IV. Data Retention** Deno determines the retention period for all Information based on the purposes for which we collect and/or receive the Information and/or tax, legal and regulatory requirements. In addition to this, we may consider other factors, such as the nature and sensitivity of the data, and whether we can achieve the purpose for which we collected the data through other means. **V. Your Privacy Choices** **1\. Your Information** You may request access to, correction of, or deletion of the personal information we maintain about you, and we will endeavor to respond promptly to your request. In order to make such a request, please contact us as indicated below. **2\. Marketing Communications** You may opt-out of marketing-related emails by clicking on the “unsubscribe” link located on the bottom of any marketing email or emailing us at [support@deno.com](mailto:support@deno.com). We will use commercially reasonable efforts to process such requests in a timely manner. Please note that even if you opt-out of marketing-related emails, you will continue to receive service-related and other non-marketing emails. **3\. Tracking Technology** You can choose not to permit tracking technologies, such as cookies and web beacons, when you use our services, but blocking some types of these tracking technologies may interfere with your experience. _Browser-Based Opt-Outs_. You may be able to disable tracking technologies using your web browser settings. Please review your browser’s instructions or visit [All About Cookies](https://allaboutcookies.org/) for general information. Note that your web browser may have settings that allow you to transmit a “Do Not Track” signal when you use online services. Like many websites, our Sites are not currently designed to respond to “Do Not Track” signals received from browsers. _Self-Regulatory Program Opt-Outs_. Two self-regulatory programs are available to help you control the use of tracking technologies on your browsers — the [Digital Advertising Alliance](https://digitaladvertisingalliance.org/) and the [Network Advertising Initiative](https://thenai.org/). Both programs help to regulate vendors in the digital advertising space. One function of their self-regulatory programs is to give you the ability to opt out of targeted (or interest-based) advertising, including the use of tracking technologies, from their member companies. You can visit the Digital Advertising Alliance’s Your Ad Choices website to opt out of targeted advertising for participating vendors. The Network Advertising Initiative similarly assists with opt outs through their Opt Out of Interest-Based Advertising webpage. _Google Analytics Opt-Out._ To opt out of Google Analytics cookies, visit Google’s [My Ad Center](https://myadcenter.google.com/personalizationoff) and/or download the Google Analytics Opt-Out Browser Add-On. **VI. Children** We do not knowingly collect personal information from children under the age of 18 through the Sites. If you are under 18, please do not give us any personal information. We encourage parents and legal guardians to monitor their children’s Internet usage and to help enforce our Privacy Policy by instructing their children never to provide personal information through the Sites without their permission. If you have reason to believe that a child under the age of 18 has provided personal information to us, please contact us, at [support@deno.com](mailto:support@deno.com) and we will endeavor to delete that information from our databases. **VII. External Websites** The Sites may contain links to third-party websites. These third-party sites may collect information about you if you click on a link. We have no control over the privacy practices or the content of these websites. As such, we are not responsible for the content or the privacy policies of those third-party websites. You should check the applicable third-party privacy policy and terms of use when visiting any other websites. **VIII. Important Notice To Non-U.S. Residents** The Sites are hosted in and provided from the United States and other countries. If you are located outside of the United States, please be aware that any information you provide to us may be transferred to the United States or other countries where the privacy laws may not be as protective as those in your country of origin. If you are located outside the United States and choose to use the Sites, you consent to any transfer and processing of your personal information in accordance with this Privacy Policy, and you do so at your own risk. **IX. Notice To California Residents** Pursuant to Section 1798.83 of the California Civil Code, residents of California have the right to obtain certain information about the types of personal information that companies with whom they have an established business relationship (and that are not otherwise exempt) have shared with third parties for direct marketing purposes during the preceding calendar year, including the names and addresses of those third parties, and examples of the types of services or products marketed by those third parties. In order to submit such a request, please contact us using the contact information provided at the end of this document. Please note, however, that we do not share, nor have we shared in the past, personal information with third parties for direct marketing purposes. **X. Notice To Nevada Residents** If you are a resident of Nevada, you have the right to opt-out of the sale of personal information to third parties. You can exercise this right by contacting us at [support@deno.com](mailto:support@deno.com) with the subject line “Nevada Do Not Sell Request” and providing us with your name and the email address. Please note, however, that we do not sell any personal information to third parties. **XI. Changes To This Privacy Policy** This Privacy Policy is effective as of the date stated at the top of this Privacy Policy. We may change this Privacy Policy from time to time. Any such changes will be posted on the Sites. By accessing the Sites after we make any such changes to this Privacy Policy, you are deemed to have accepted such changes. Please be aware that, to the extent permitted by applicable law, our use of the Information is governed by the Privacy Policy in effect at the time we collect the Information. Please refer back to this Privacy Policy on a regular basis. **XII. How To Contact Us** Please reach out to [support@deno.com](mailto:support@deno.com) for any questions, complaints, or requests regarding this Privacy Policy, and include in the subject line “Privacy Policy", or contact us by mail at: Deno Land Inc.\ 1111 6th Ave Ste 550\ PMB 702973\ San Diego CA, 92101\ USA **© 2024 Deno Land Inc. All rights reserved.** --- # 账户 > 有关用户账户、通过 GitHub 进行身份验证以及在 Deno Deploy 中管理个人资料的信息。 URL: https://docs.deno.com/deploy/reference/accounts Deno Deploy 支持使用 GitHub 登录。 您的主要联系邮箱地址和名称会从 GitHub 同步。您的用户名和邮箱地址会在每次登录时更新。在 GitHub 更改邮箱、登录名或名称后,请重新登录以便在 Deno Deploy 仪表板中看到这些更改。 Deno 也支持使用 Google 账户登录。 使用 Google 账户通过 Deno Deploy 进行身份验证的用户,在创建新应用时还需要提供 GitHub 凭据,以便访问用于部署的 GitHub 仓库。 --- # 应用程序 > Deno Deploy 早期访问中管理应用程序的指南,包括应用创建、配置、GitHub 集成和部署选项。 URL: https://docs.deno.com/deploy/reference/apps :::info 您正在查看 Deno DeployEA 的文档。想查看 Deploy Classic 文档?[请点击这里](/deploy/)。 ::: 应用程序是组织内提供流量服务的网络服务。每个应用程序包含一系列修订历史(之前的版本),通常对应于使用 GitHub 集成时的 Git 提交。 应用程序通过一个 slug 来标识,该 slug 必须在组织内唯一,并用于默认域名。 ## 创建应用程序 创建应用程序步骤: 1. 在组织页面点击“+ 创建应用程序”按钮 2. 选择要部署的 GitHub 仓库 3. 配置应用程序 slug(名称) 4. 设置构建配置 5. 添加所需的环境变量 > ⚠️ 目前,应用程序必须在创建时关联一个 GitHub 仓库。 构建配置决定了应用在部署过程中的构建方式。每次向关联的仓库推送代码时,或手动点击“部署默认分支”时,都会自动触发构建。有关详细的构建配置信息,请参阅[构建文档](/deploy/early-access/reference/builds/)。 您可以在创建应用时通过点击“编辑环境变量”添加环境变量。更多关于环境变量的详情,请参见[环境变量与上下文](/deploy/early-access/reference/env-vars-and-contexts/)文档。 ## 重命名应用程序 可以通过编辑应用设置页面上的应用 slug 来重命名应用程序。这将更新与应用关联的默认域名,因为它们基于应用 slug。新的 slug 必须在组织内唯一(即同一组织内的其他应用或游乐场中不能使用)。 :::warning 重命名后,之前指向该应用的所有 `deno.net` URL 将失效。 自定义域名将继续正常工作,因为它们不依赖于应用 slug。 ::: ## 删除应用程序 可以在应用设置页面删除应用程序。此操作将从组织中删除该应用及其所有修订版本。所有现有部署将立即停止服务,且所有自定义域名关联将被移除。 删除后,该应用及其修订将不再可访问,也不再服务任何流量。通过 Deno Deploy UI 无法恢复已删除的应用。 :::info 误删应用?请在 30 天内联系 Deno 支持以恢复。 ::: ## 限制 > ⚠️ 目前应用程序无法转移到其他组织。 ## GitHub 集成 GitHub 集成支持从 GitHub 仓库自动部署应用。每次向仓库推送时,都会触发应用的新构建。根据提交的分支,构建将部署到不同的[时间线](/deploy/early-access/reference/timelines/)。 应用程序在创建时与 GitHub 仓库关联。但创建后也可以解除关联,并可选择关联新的 GitHub 仓库。此操作可以在应用设置页面完成。 GitHub 仓库下拉菜单仅显示已经通过 Deno Deploy GitHub 应用授权的账户。您可以通过点击用户或组织下拉菜单中的“+ 添加另一个 GitHub 账户”按钮,或在仓库下拉菜单中点击“配置 GitHub 应用权限”按钮,授权新的组织或仓库。此操作会重定向您到 GitHub,以授权 Deno Deploy GitHub 应用访问所选 GitHub 账户或组织。授权完成后,您将被重定向回应用设置页面,此时可以选择新授权的 GitHub 仓库。 ### GitHub 事件集成 每当 Deno Deploy 从 GitHub 仓库构建应用时,会在构建开始和结束时向该仓库发送 [`repository_dispatch`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#repository_dispatch) 事件。这允许您根据构建状态触发 GitHub Actions 工作流。 Deno Deploy 会发送以下事件: | 事件名称 | 描述 | | -------------------------------- | --------------------------------------------------------- | | `deno_deploy.build.enqueued` | 触发构建排队时发送,即当向仓库推送时。 | | `deno_deploy.build.cancelled` | 构建被取消时发送,可能是手动取消或超时导致。 | | `deno_deploy.build.failed` | 构建失败时发送。 | | `deno_deploy.build.routed` | 构建成功完成且流量已路由至该构建时发送。 | 事件的负载结构遵循以下 TypeScript 类型定义: ```ts interface DenoDeployBuildEventPayload { app: { /** Deno Deploy 应用的 UUID。 */ id: string; /** Deno Deploy 应用的 slug(名称)。 */ slug: string; }; organization: { /** 包含该应用的 Deno Deploy 组织的 UUID。 */ id: string; /** 包含该应用的 Deno Deploy 组织的 slug(名称)。 */ slug: string; }; revision: { /** 正在构建的修订版本 ID。 */ id: string; /** 可在 Deno Deploy 仪表盘查看修订版本及构建状态的 URL。 */ html_url: string; /** 正在构建的 Git 提交 SHA。 */ git: { sha: string }; /** 如果构建成功,修订版本可访问的预览 URL。 */ preview_url: string | null; }; } ``` 您可以在 GitHub Actions 工作流中通过添加 `repository_dispatch` 触发器来接收这些事件。例如: ```yaml on: repository_dispatch: types: [deno_deploy.build.routed] # 监听成功构建事件 jobs: notify: runs-on: ubuntu-latest steps: - name: 测试 preview_url run: | echo "Deno Deploy 应用可通过 ${{ github.event.client_payload.revision.preview_url }} 访问" curl -I ${{ github.event.client_payload.revision.preview_url }} ``` --- # 构建 > Deno Deploy 中构建流程的详细说明,涵盖构建触发方式、阶段、配置选项、缓存以及构建环境。 URL: https://docs.deno.com/deploy/reference/builds 在 Deno Deploy 中,您应用程序代码的每个版本都被表示为一个修订(或构建)。当从 GitHub 部署时,修订通常与您仓库中的 git 提交一一对应。 ## 构建触发 构建可以通过三种方式触发: - **通过 UI 手动触发**:使用构建页面上的“部署默认分支”按钮,部署默认 git 分支(通常是 `main`)。下拉菜单允许您选择不同的分支。 - **通过 CLI 手动触发**:使用 `deno deploy` 命令。 - **通过 GitHub 自动触发**:当向与您的应用关联的 GitHub 仓库推送新的提交时。 ## 构建阶段 一个修订在变为可用状态前会经历以下阶段: 1. **排队**:修订等待分配给构建器。 2. **准备**:构建器下载源代码并恢复可用的构建缓存。 3. **安装**:执行安装命令(如果指定),通常用来下载依赖。 4. **构建**:执行构建命令(如果指定),生成构建产物并上传到运行时基础设施。 5. **部署**:修订准备部署到每个时间线。对于每个时间线,执行以下操作: 1. **创建数据库**:如果应用有附属数据库,确保该时间线存在数据库(必要时创建)。 2. **预部署命令**:执行应用配置的任何预部署命令,通常用于数据库迁移等任务。 3. **预热**:仅在“预览”时间线中,启动应用以确保其正常启动。 4. **路由**:将新修订推广到与该时间线关联的 URL。 如果任何步骤失败,构建将进入“失败”状态并且不会接收流量。 构建日志会在构建过程中实时推送到仪表盘,构建完成后仍可在构建页面查看。 构建缓存通过重用在构建间未更改的文件来加快构建速度。对于框架预设和 `DENO_DIR` 依赖缓存,此过程是自动进行的。 您可以使用构建页面右上角的“取消”按钮取消正在运行的构建。构建会在运行 5 分钟后自动取消。 ## 构建配置 App configuration defines how to convert source code into a deployable artifact. There are two places you can set app configuration: - **In source code**: Using a `deno.json` or `deno.jsonc` file in the application directory. - **In the Deno Deploy dashboard**: Using the app configuration settings. If you specify both options, settings in the source code take precedence over those in the dashboard. You will be unable to edit any of the app configuration values in the dashboard if the most recent successful build used configuration from source code. The application directory must be configured through the dashboard. This setting is not configurable from source code, as it determines where to find the source code itself. ### Editing app configuration in the dashboard You can modify app configuration in three places: - 创建应用时点击“编辑构建配置” - 在应用设置中点击构建配置部分的“编辑” - 在失败构建页面的重试抽屉中 在创建应用时,如果您使用已识别的框架或常见构建配置,构建配置可能会自动从仓库中检测。 #### Configuration options - **应用目录**:仓库中用作应用根目录的文件夹。适用于 Monorepo。默认是仓库根目录。 - **框架预设**:针对支持的框架(如 Next.js 或 Fresh)做了优化的配置。[了解更多框架集成](./frameworks/)。 - **安装命令**:安装依赖的 shell 命令,如 `npm install` 或 `deno install`。 - **构建命令**:构建项目的 shell 命令,通常是 `package.json` 或 `deno.json` 中的任务,如 `deno task build` 或 `npm run build`。 - **预部署命令**:构建完成但部署前运行的 shell 命令,通常用于数据库迁移等任务。 - **运行时配置**:决定应用如何提供流量: - **动态**:用于通过服务器响应请求的应用(API 服务器、服务器渲染网站等) - **入口文件**:要执行的 JavaScript 或 TypeScript 文件 - **参数**(可选):传递给应用的命令行参数 - **运行时工作目录**(可选):应用运行时的工作目录 - **运行时内存限制**(可选):应用运行时可使用的最大内存。默认 768 MB,Pro 计划下可增加至 4 GB。 - **静态**:用于提供预渲染静态内容的静态网站 - **目录**:包含静态资源的文件夹(如 `dist`、`.output`) - **单页应用模式**(可选):对不匹配静态文件的路径返回 `index.html`,而不是 404 错误 - **自动**:使用框架预设时,运行时配置会自动设置。 - **运行时内存限制**(可选):应用运行时可使用的最大内存。默认 768 MB,Pro 计划下可增加至 4 GB。 - **构建超时**:构建过程允许的最长时间。默认 5 分钟,Pro 计划下可增加至 15 分钟。 - **构建内存**:分配给构建过程的内存大小。默认 3 GB,Pro 计划下可增加至 4 GB。 ### Editing app configuration from source code To configure your application from source code, add a `deno.json` or `deno.jsonc` file to the root of your application directory with a `deploy` key. If any of the following app configuration options are specified under this key, the entire configuration will be sourced from the file instead of the dashboard (any configuration specified in the dashboard will be ignored). #### `deno.json` options - `deploy.framework` (required unless `deploy.runtime` is set): The framework preset to use, such as `nextjs` or `fresh`. Setting this option automatically configures defaults for the framework. Available presets are listed in the [framework integrations docs](./frameworks/). - `deploy.install` (optional): Shell command to install dependencies. - `deploy.build` (optional): Shell command to build the project. - `deploy.predeploy` (optional): Shell command to run after the build is complete but before deployment, typically for tasks like database migrations. - `deploy.runtime` (required unless `deploy.framework` is set): Configuration for how the app serves traffic. The app can either be static or dynamic, as defined below: - For dynamic apps: - `deploy.runtime.type`: Must be set to `"dynamic"`, or omitted (dynamic is the default). - `deploy.runtime.entrypoint`: The JavaScript or TypeScript file to execute. - `deploy.runtime.args` (optional): Command-line arguments to pass to the application. - `deploy.runtime.cwd` (optional): The working directory for the application at runtime. - `deploy.runtime.memory_limit` (optional): The maximum amount of memory the application can use at runtime. Defaults to 768 MB, can be increased to 4 GB on the Pro plan. - For static apps: - `deploy.runtime.type`: Must be set to `"static"`. - `deploy.runtime.cwd`: Folder containing static assets (e.g., `dist`, `.output`). - `deploy.runtime.spa` (optional): If `true`, serves `index.html` for paths that don't match static files instead of returning 404 errors. - For apps using a framework preset: - `deploy.runtime.memory_limit` (optional): The maximum amount of memory the application can use at runtime. Defaults to 768 MB, can be increased to 4 GB on the Pro plan. #### Examples **Example dynamic app configuration from `deno.json`:** ```jsonc { "deploy": { "install": "npm install", "build": "npm run build", "predeploy": "deno run --allow-net --allow-env migrate.ts", "runtime": { "type": "dynamic", "entrypoint": "./app/server.js", "args": ["--port", "8080"], "cwd": "./app" } } } ``` **Example static app configuration from `deno.jsonc`:** ```jsonc { "deploy": { "install": "npm install", "build": "npm run build", "runtime": { "type": "static", "cwd": "./public", "spa": true } } } ``` **Example framework preset configuration with Next.js from `deno.json`:** ```jsonc { "deploy": { "framework": "nextjs", "install": "npm install", "build": "npm run build" } } ``` ## 构建环境 构建环境在 Linux 上运行,支持 x64 或 ARM64 架构。可用工具包括: - `deno`(与运行时版本相同) - `node` - `npm` - `npx` - `yarn`(v1) - `pnpm` - `git` - `tar` - `gzip` :::info 构建器内部所有 JavaScript 代码均使用 Deno 执行。 `node` 命令实际上是一个 shim,负责将 Node.js 的调用转换为 `deno run`。类似地,`npm`、`npx`、`yarn` 和 `pnpm` 也都是通过 Deno 而非 Node.js 运行。 ::: 为“构建”上下文配置的环境变量在构建过程中可用,但来自“生产”或“开发”上下文的变量不可用。[了解更多关于环境变量](/deploy/reference/env_vars_and_contexts/)。 构建器在构建期间可用的存储空间为 8 GB。 --- # 部署按钮 > 帮助用户快速轻松地克隆代码并通过点击按钮将其部署到 Deno Deploy URL: https://docs.deno.com/deploy/reference/button :::info 您正在查看 Deno DeployEA 的文档。寻找 Deploy Classic 的文档? [点击此处查看](/deploy/)。 ::: 部署按钮为用户提供了一个快捷方式,基于托管在 Git 仓库中的现有代码,在 Deno Deploy 上创建并部署新应用程序。 它提供了一个直接进入 Deno Deploy 应用创建流程的链接,并根据提供的查询参数或指定源的 `deno.json` 文件中的值填充创建流程中的设置。 指定的仓库将被克隆到用户的 GitHub 账户,并设置为新项目的源。默认情况下,新仓库为公开,但如果需要也可以设置为私有。 ## 示例 下面的部署按钮演示了基于一个简单入门项目创建新应用程序的过程 [![Deploy on Deno](https://deno.com/button)](https://console.deno.com/new?clone=https://github.com/denoland/examples&path=hello-world) ## 创建并部署新应用程序 使用以下代码提供一个创建并部署新应用程序的按钮: **Markdown** ```bash [![Deploy on Deno](https://deno.com/button)](https://console.deno.com/new?clone=REPOSITORY_URL) ``` **HTML** ```bash Deploy on Deno ``` **URL** ```bash https://console.deno.com/new?clone=REPOSITORY_URL ``` ### 参数 以下查询参数可用于配置部署按钮: - `clone` — (必填)要克隆为新仓库并随后部署的源仓库 URL - `path` — (可选)在源仓库中需要克隆的路径。提供此参数将创建一个以该目录为根的新仓库 - `app_directory` — (可选)新仓库中用作应用程序根目录的路径。当仓库采用 monorepo 结构时此参数非常有用。 - `install` — (可选)构建前执行的命令,用于安装依赖 - `build` — (可选)构建应用程序时执行的命令 - `predeploy` — (可选)构建完成但部署之前执行的命令 --- # 缓存 > Deno Deploy 早期访问中的 CDN 缓存功能概览,涵盖缓存配置、指令及最佳实践。 URL: https://docs.deno.com/deploy/reference/caching :::info 您正在查看 Deno DeployEA 的文档。正在寻找 Deploy Classic 的文档?[请点击这里查看](/deploy/)。 ::: Deno DeployEA 内置了一个 CDN,可以缓存您应用程序的响应。这提升了以下内容的性能: - 静态资源(图片、CSS、JavaScript 文件) - 不频繁变化的 API 响应和服务器渲染页面 缓存默认对所有应用启用,但只有带有合适缓存头的响应才会被实际缓存。 Deno DeployEA 与 Next.js 等流行框架集成,自动优化像增量静态再生(ISR)这样的功能的缓存。 CDN 缓存与版本和上下文绑定。当您部署新版本时,缓存会自动失效,确保用户始终看到最新版本的应用。注意,如果 `Cache-Control` 头允许,浏览器缓存仍可能提供较旧内容。 ## 缓存资源 要缓存资源,请在响应中设置 `Cache-Control` 头。此标准 HTTP 头指示浏览器和 CDN 如何缓存您的内容。 ### 支持的缓存指令 Deno DeployEA 支持以下缓存指令: | 指令 | 描述 | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `max-age` | 响应被 CDN 和浏览器认为是新鲜的最大时间(秒)。过期后,响应被视为陈旧,需要与服务器重新验证。 | | `s-maxage` | 响应被共享缓存(仅限 CDN,不包括浏览器)认为是新鲜的最大时间(秒)。过期后,响应需要与服务器重新验证。 | | `stale-while-revalidate` | 在获取新鲜响应的后台过程中,可以提供陈旧响应的最大时间(秒)。 | | `stale-if-error` | 当服务器返回错误时,可以提供陈旧响应的最大时间(秒)。 | | `immutable` | 表示响应永远不会改变,允许无限期缓存。适用于内容哈希的静态资源。 | | `no-store` | 禁止缓存响应。适用于绝不应缓存的动态内容。 | | `no-cache` | 在从缓存提供响应前需重新向服务器进行验证。适用于频繁变化但可利用条件请求的内容。 | ### 其他缓存头 - `Vary`:指定哪些请求头应当包含在缓存键中,基于这些请求头创建独立缓存版本。 - `Expires`:为响应设置绝对过期时间(作为 `max-age` 的替代)。适用于不会变化的文件,如图片或 CSS 文件。 - `no-store`:响应不应被缓存。适用于不应缓存的动态响应,如 API 响应或服务器渲染的页面。 - `no-cache`:响应在从缓存提供前应向服务器重新验证。适用于可能经常变化的动态响应。 `Vary` 头可用来指定哪些请求头应作为请求的缓存键的一部分。 `Expires` 头可用来指定响应的绝对过期时间。这是 `max-age` 指令的另一种选择。 --- # 云连接 > 了解如何将 Deno Deploy 连接到 AWS 和 Google Cloud Platform 等云提供商,而无需管理凭据。 URL: https://docs.deno.com/deploy/reference/cloud_connections Deno Deploy 允许您连接到诸如 AWS 和 Google Cloud Platform(GCP)之类的云提供商,而无需手动管理静态凭据。这是通过使用 OpenID Connect (OIDC) 和身份联合来实现的。 ## 工作原理 Deno Deploy 是一个 OIDC 提供者。每个运行中的 Deno Deploy 应用都可以被签发由 Deno Deploy 签名的短期有效 JWT 令牌。这些令牌包含关于该应用的信息,例如组织和应用的 ID 和别名、应用执行的上下文,以及运行的修订版本 ID。了解更多关于 [Deno Deploy 中的 OIDC](/deploy/reference/oidc)。 通过将这些令牌发送到 AWS 或 GCP,可以将它们兑换为短期有效的 AWS 或 GCP 凭据,从而访问云资源,例如 AWS S3 存储桶或 Google Cloud Spanner 实例。向 AWS 或 GCP 发送令牌时,云提供商会验证该令牌,检查它是否由 Deno Deploy 签发,且是否有效且允许特定应用和上下文访问云资源。 为了使 AWS 或 GCP 能够将 OIDC 令牌兑换为凭据,云提供商需要配置为信任 Deno Deploy 作为 OIDC 身份提供者,并需要创建 AWS IAM 角色或 GCP 服务账号,使其允许特定 Deno Deploy 应用将令牌兑换为凭据的操作。 ## 设置 AWS 本指南包含三种设置 AWS 资源的指导。您可以使用任意一种方式完成 AWS 资源的设置。 - [在本地使用 `deno deploy setup-aws` 命令](#aws%e2%9a%a0-easy-setup-with-deno-deploy-setup-aws)(推荐) - [使用 `aws` CLI](#setup-aws-cli) - [使用 AWS 控制台](#setup-aws-console) - [使用 Terraform](#setup-aws-terraform) 要在您的 AWS 账户中设置与 Deno Deploy 的 AWS 集成,需要创建以下资源: - 一个 [AWS IAM OIDC 身份提供者](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html) 来信任 Deno Deploy 作为 OIDC 提供者。 - OIDC 提供者 URL 为 `https://oidc.deno.com`。 - 受众(客户端 ID)为 `sts.amazonaws.com`。 - 一个 [AWS IAM 角色](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html) ,可使用 Deno Deploy 的 OIDC 令牌进行“切换”(登录)。 - 角色的信任策略应允许 OIDC 提供者切换角色,例如: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam:::oidc-provider/oidc.deno.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.deno.com:aud": "sts.amazonaws.com", "oidc.deno.com:sub": "deployment://" } } } ] } ``` - 角色应拥有访问您想使用的 AWS 资源(如 S3 存储桶或 DynamoDB 表)的权限。 设置完 AWS 资源后,从应用设置中前往 AWS 云集成设置页面。在那里,您必须选择应可用云连接的上下文。 然后必须输入之前创建的 AWS IAM 角色的 ARN(亚马逊资源名称)。输入 ARN 后,可以点击“测试连接”按钮开始连接测试。连接测试会检查 AWS IAM 角色和 OIDC 提供者是否已正确配置,并且不会允许未授权的应用、组织或上下文访问。 测试连接成功后,即可保存云连接。 ### 使用方法 在建立 AWS 与 Deno Deploy 之间的云连接后,您可以直接从应用代码访问 AWS 资源(如 S3),无需配置任何凭据。 AWS SDK v3 会自动按云连接配置使用凭据。以下是一个示例,展示如何从配置了 AWS 账户的 Deno Deploy 应用访问 S3 存储桶。 ```ts import { ListBucketsCommand, S3Client } from "@aws-sdk/client-s3"; const s3 = new S3Client({ region: "us-west-2" }); Deno.serve(() => { const { Buckets } = await s3.send(new ListBucketsCommand({})); return Response.json(Buckets); }); ``` ## 设置 GCP 要在您的 GCP 账户中设置与 Deno Deploy 的集成,需要创建以下资源: - 一个 [工作负载身份池和工作负载身份提供者](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers) ,信任 Deno Deploy 作为 OIDC 提供者。 - OIDC 提供者 URL 为 `https://oidc.deno.com`。 - 受众应为默认值(以 `https://iam.googleapis.com` 开头)。 - 至少需设置以下属性映射: - `google.subject = assertion.sub` - `attribute.full_slug = assertion.org_slug + "/" + assertion.app_slug` - 一个 [服务账号](https://cloud.google.com/iam/docs/service-accounts-create) ,可用 OIDC 令牌“模拟”(登录)。 - 来自工作负载身份池的主体或主体集合应拥有使用工作负载身份用户角色 (`roles/iam.workloadIdentityUser`) 访问该服务账号的权限。示例: - 应用中的特定上下文: `principal://iam.googleapis.com/projects//locations/global/workloadIdentityPools/oidc-deno-com/subject/deployment://` - 应用中的所有上下文: `principalSet://iam.googleapis.com/projects//locations/global/workloadIdentityPools/oidc-deno-com/attribute.full_slug//` - 服务账号应拥有访问您想使用的 GCP 资源的权限,例如 Google Cloud Storage 存储桶。 本指南包含三种设置 GCP 资源的指导。您可以使用任意一种方式完成 GCP 资源的设置。 - [在本地使用 `deno deploy setup-gcp` 命令](#setup-gcp-easy)(推荐) - [使用 `gcloud` CLI](#setup-gcp-cli) - [使用 GCP 控制台](#setup-gcp-console) - [使用 Terraform](#setup-gcp-terraform) 设置完 GCP 资源后,前往应用设置中的 GCP 云集成设置页面。在那里,您必须选择应可用云连接的上下文。 然后您必须输入工作负载身份提供者 ID,格式为 `projects//locations/global/workloadIdentityPools/oidc-deno-com/providers/oidc-deno-com`, 以及之前创建的 GCP 服务账号邮箱。输入邮箱后,点击“测试连接”按钮开始连接测试。连接测试会检查 GCP 服务账号和 OIDC 提供者是否已正确配置,并且不会允许未授权的应用、组织或上下文访问。 测试成功后,即可保存云连接。 ### 使用方法 在建立 GCP 与 Deno Deploy 之间的云连接后,您可以直接从应用代码访问 GCP 资源(如 Cloud Storage),无需配置任何凭据。 Google Cloud SDK 会自动按云连接配置使用凭据。以下是一个示例,展示如何从配置了 GCP 账户的 Deno Deploy 应用访问 Cloud Storage 存储桶。 ```ts import { Storage } from "@google-cloud/storage"; const storage = new Storage(); Deno.serve(() => { const [buckets] = await storage.getBuckets(); return Response.json(buckets); }); ``` ## 删除云集成 您可以在云集成部分,点击特定云连接旁边的“删除”按钮来删除该云连接。 ## 设置指南 ### AWS:使用 `deno deploy setup-aws` 简单设置 有关如何使用 `deno deploy setup-aws` 命令设置 AWS 与 Deno Deploy 集成的说明,请参阅应用设置中的 AWS 云集成设置页面的说明。 ### AWS:使用 `aws` CLI 您可以使用 AWS CLI 手动设置 AWS 资源。此方法要求已安装并配置 AWS CLI,且拥有创建 IAM 角色、OIDC 提供者和附加策略的权限。 #### 前提条件 - 已安装并配置 AWS CLI - 拥有创建 IAM 角色、OIDC 提供者和附加策略的权限 #### 第 1 步:创建 OIDC 提供者 如果还未创建 OIDC 提供者,请运行: ```bash aws iam create-open-id-connect-provider \ --url https://oidc.deno.com \ --client-id-list sts.amazonaws.com ``` #### 第 2 步:创建带信任策略的 IAM 角色 创建一个信任策略文件,允许您的 Deno Deploy 应用切换角色。您可以选择允许所有上下文访问,或者只允许特定上下文。 **针对应用中所有上下文:** ```bash # 为整个应用创建信任策略文件 cat > trust-policy-all-contexts.json << EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::YOUR_ACCOUNT_ID:oidc-provider/oidc.deno.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringLike": { "oidc.deno.com:sub": "deployment:YOUR_ORG/YOUR_APP/*" } } } ] } EOF ``` **针对特定上下文:** ```bash # 为特定上下文创建信任策略文件 cat > trust-policy-specific-contexts.json << EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::YOUR_ACCOUNT_ID:oidc-provider/oidc.deno.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.deno.com:sub": [ "deployment:YOUR_ORG/YOUR_APP/production", "deployment:YOUR_ORG/YOUR_APP/staging" ] } } } ] } EOF ``` #### 第 3 步:创建 IAM 角色 使用相应的信任策略创建角色: ```bash # 针对整个应用 aws iam create-role \ --role-name DenoDeploy-YourOrg-YourApp \ --assume-role-policy-document file://trust-policy-all-contexts.json # 或针对特定上下文 aws iam create-role \ --role-name DenoDeploy-YourOrg-YourApp \ --assume-role-policy-document file://trust-policy-specific-contexts.json ``` #### 第 4 步:附加策略 附加所需策略,以授予应用访问 AWS 资源的权限: ```bash aws iam attach-role-policy \ --role-name DenoDeploy-YourOrg-YourApp \ --policy-arn arn:aws:iam::aws:policy/POLICY_NAME ``` 将 `POLICY_NAME` 替换为合适的 AWS 策略(例如 `AmazonS3ReadOnlyAccess`、`AmazonDynamoDBReadOnlyAccess` 等),根据您的需求选择。 完成上述步骤后,在 Deno Deploy 云连接配置中使用角色 ARN。 ### AWS:使用 AWS 控制台 您也可以通过 AWS 管理控制台网页界面设置 AWS 资源,此方式提供了一个直观的配置方法。 #### 第 1 步:创建 OIDC 身份提供者 1. 进入 IAM 控制台 → 身份提供者 2. 点击“添加提供者” 3. 选择“OpenID Connect” 4. 填写提供者 URL:`https://oidc.deno.com` 5. 受众:`sts.amazonaws.com` 6. 点击“添加提供者” #### 第 2 步:创建 IAM 角色 1. 进入 IAM 控制台 → 角色 2. 点击“创建角色” 3. 选择受信实体类型为 **Web 身份** 4. 选择刚才创建的 OIDC 提供者(`oidc.deno.com`) 5. 受众填写 `sts.amazonaws.com` #### 第 3 步:配置信任策略条件 添加条件限制哪些 Deno Deploy 应用可切换此角色。选择一种方式: **应用中所有上下文:** - 条件键:`oidc.deno.com:sub` - 操作符:`StringLike` - 值:`deployment:YOUR_ORG/YOUR_APP/*` **特定上下文:** - 条件键:`oidc.deno.com:sub` - 操作符:`StringEquals` - 值:`deployment:YOUR_ORG/YOUR_APP/production` - 对每个上下文(如 staging、development)添加额外条件 点击“下一步”继续。 #### 第 4 步:附加权限策略 1. 根据需求搜索并选择合适的策略: - S3 访问:`AmazonS3ReadOnlyAccess` 或 `AmazonS3FullAccess` - DynamoDB 访问:`AmazonDynamoDBReadOnlyAccess` 或 `AmazonDynamoDBFullAccess` - 其他服务:选择相应策略 2. 点击“下一步” #### 第 5 步:命名并创建角色 1. 角色名:`DenoDeploy-YourOrg-YourApp`(替换为您的组织及应用名) 2. 说明:可选填写角色用途描述 3. 审核信任策略和权限 4. 点击“创建角色” #### 第 6 步:复制角色 ARN 创建完成后: 1. 进入角色详情页 2. 复制角色 ARN(格式为 `arn:aws:iam::123456789012:role/DenoDeploy-YourOrg-YourApp`) 3. 在 Deno Deploy 云连接配置中使用该 ARN ### AWS:使用 Terraform 您可以使用 Terraform 编程方式创建所需的 AWS 资源。此方案适合基础设施即代码的工作流程。 #### Terraform 配置示例 创建一个 Terraform 配置文件,内容如下: ```hcl # 变量定义 variable "org" { description = "Deno Deploy 组织名称" type = string } variable "app" { description = "Deno Deploy 应用名称" type = string } variable "contexts" { description = "允许的特定上下文列表(留空表示允许所有上下文)" type = list(string) default = [] } # OIDC 提供者资源 resource "aws_iam_openid_connect_provider" "deno_deploy" { url = "https://oidc.deno.com" client_id_list = ["sts.amazonaws.com"] } # 根据上下文动态生成的 IAM 角色 resource "aws_iam_role" "deno_deploy_role" { name = "DenoDeploy-${var.org}-${var.app}" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Principal = { Federated = aws_iam_openid_connect_provider.deno_deploy.arn } Action = "sts:AssumeRoleWithWebIdentity" Condition = length(var.contexts) > 0 ? { # 只允许特定上下文 StringEquals = { "oidc.deno.com:sub" = [ for context in var.contexts : "deployment:${var.org}/${var.app}/${context}" ] } } : { # 允许所有上下文(通配符) StringLike = { "oidc.deno.com:sub" = "deployment:${var.org}/${var.app}/*" } } } ] }) } # 附加策略 resource "aws_iam_role_policy_attachment" "example" { role = aws_iam_role.deno_deploy_role.name policy_arn = "arn:aws:iam::aws:policy/POLICY_NAME" } # 输出角色 ARN output "role_arn" { value = aws_iam_role.deno_deploy_role.arn } ``` #### 使用示例 **针对整个应用(所有上下文)访问:** ```hcl module "deno_deploy_aws" { source = "./path-to-terraform-module" org = "your-org" app = "your-app" contexts = [] # 空表示所有上下文 } ``` **仅针对特定上下文:** ```hcl module "deno_deploy_aws" { source = "./path-to-terraform-module" org = "your-org" app = "your-app" contexts = ["production", "staging"] } ``` #### 应用配置 1. 初始化 Terraform: ```bash terraform init ``` 2. 计划部署: ```bash terraform plan ``` 3. 应用配置: ```bash terraform apply ``` 应用完成后,Terraform 会输出角色 ARN,供您在 Deno Deploy 云连接配置中使用。 #### 自定义策略 将 `aws_iam_role_policy_attachment` 资源中的 `POLICY_NAME` 替换为适合您的 AWS 托管策略,或根据需求创建自定义策略。您可以通过创建多个策略附件资源添加多个策略。 ### GCP:使用 `deno deploy setup-gcp` 简单设置 有关如何使用 `deno deploy setup-gcp` 命令设置 GCP 与 Deno Deploy 集成的说明,请参阅应用设置中的 Google 云集成设置页面的说明。 ### GCP:使用 `gcloud` CLI 您可以使用 gcloud CLI 手动设置 GCP 资源。此方法需要安装并认证 gcloud CLI,且拥有创建工作负载身份池、服务账号和授予 IAM 角色的权限。 #### 前提条件 - 安装并认证 gcloud CLI - 拥有创建工作负载身份池、服务账号和授予 IAM 角色权限 - 启用以下必需 API: - `iam.googleapis.com` - `iamcredentials.googleapis.com` - `sts.googleapis.com` #### 第 1 步:启用必需 API 先为项目启用必需的 API: ```bash gcloud services enable iam.googleapis.com gcloud services enable iamcredentials.googleapis.com gcloud services enable sts.googleapis.com ``` #### 第 2 步:创建工作负载身份池 创建工作负载身份池以管理外部身份: ```bash gcloud iam workload-identity-pools create oidc-deno-com \ --location=global \ --display-name="Deno Deploy Workload Identity Pool" ``` #### 第 3 步:创建工作负载身份提供者 在工作负载身份池中配置 OIDC 提供者: ```bash gcloud iam workload-identity-pools providers create-oidc oidc-deno-com \ --workload-identity-pool=oidc-deno-com \ --location=global \ --issuer-uri=https://oidc.deno.com \ --attribute-mapping="google.subject=assertion.sub,attribute.org_slug=assertion.org_slug,attribute.app_slug=assertion.app_slug,attribute.full_slug=assertion.org_slug+\"/\"+assertion.app_slug" ``` #### 第 4 步:创建服务账号 创建供您的 Deno Deploy 应用使用的服务账号: ```bash gcloud iam service-accounts create deno-your-org-your-app \ --display-name="Deno Deploy YourOrg/YourApp" ``` #### 第 5 步:配置工作负载身份绑定 获取项目编号,并配置工作负载身份绑定。可选择允许所有上下文访问或仅特定上下文访问。 ```bash # 获取项目编号 PROJECT_NUMBER=$(gcloud projects describe PROJECT_ID --format="value(projectNumber)") ``` **应用中所有上下文:** ```bash gcloud iam service-accounts add-iam-policy-binding \ deno-your-org-your-app@PROJECT_ID.iam.gserviceaccount.com \ --role=roles/iam.workloadIdentityUser \ --member="principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-deno-com/attribute.full_slug/YOUR_ORG/YOUR_APP" ``` **仅特定上下文:** ```bash # 绑定到生产上下文 gcloud iam service-accounts add-iam-policy-binding \ deno-your-org-your-app@PROJECT_ID.iam.gserviceaccount.com \ --role=roles/iam.workloadIdentityUser \ --member="principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-deno-com/subject/deployment:YOUR_ORG/YOUR_APP/production" # 绑定到预发布上下文 gcloud iam service-accounts add-iam-policy-binding \ deno-your-org-your-app@PROJECT_ID.iam.gserviceaccount.com \ --role=roles/iam.workloadIdentityUser \ --member="principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-deno-com/subject/deployment:YOUR_ORG/YOUR_APP/staging" # 根据需要为每个特定上下文添加更多绑定 ``` #### 第 6 步:授予服务账号角色 授予服务账号访问 GCP 资源所需的角色: ```bash gcloud projects add-iam-policy-binding PROJECT_ID \ --member="serviceAccount:deno-your-org-your-app@PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/ROLE_NAME" ``` 将 `ROLE_NAME` 替换为适当的角色,例如: - `roles/storage.objectViewer`(Cloud Storage 读取访问) - `roles/storage.objectAdmin`(Cloud Storage 完全访问) - `roles/cloudsql.client`(Cloud SQL 访问) - 根据需求选择其他角色 #### 第 7 步:获取必要的值 设置完成后,您需获取两个值用于 Deno Deploy 配置: 1. **工作负载提供者 ID**: `projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-deno-com/providers/oidc-deno-com` 2. **服务账号邮箱地址**: `deno-your-org-your-app@PROJECT_ID.iam.gserviceaccount.com` 将这些值用于 Deno Deploy 云连接配置。 ### GCP:使用 GCP 控制台 您也可以通过 Google Cloud 控制台网页界面设置 GCP 资源,此方式提供工作负载身份联合和服务账号的可视化配置。 #### 第 1 步:启用必需 API 1. 进入 API 与服务 → 库 2. 搜索并启用以下 API: - “身份和访问管理 (IAM) API” - “IAM 服务账号凭证 API” - “安全令牌服务 API” #### 第 2 步:创建工作负载身份池 1. 进入 IAM 与管理员 → 工作负载身份联合 2. 点击“创建池” 3. 填写: - 池名称:`Deno Deploy Workload Id Pool` - 池 ID:`oidc-deno-com` 4. 点击“继续” #### 第 3 步:添加提供者到池中 1. 点击“添加提供者” 2. 选择提供者类型为 **OpenID Connect (OIDC)** 3. 填写: - 提供者名称:`Deno Deploy OIDC Provider` - 提供者 ID:`oidc-deno-com` - 签发者 URL:`https://oidc.deno.com` 4. 配置属性映射: - `google.subject` → `assertion.sub` - `attribute.org_slug` → `assertion.org_slug` - `attribute.app_slug` → `assertion.app_slug` - `attribute.full_slug` → `assertion.org_slug + "/" + assertion.app_slug` 5. 点击“保存” #### 第 4 步:创建服务账号 1. 进入 IAM 与管理员 → 服务账号 2. 点击“创建服务账号” 3. 填写: - 服务账号名称:`deno-your-org-your-app` - 服务账号 ID:`deno-your-org-your-app` - 描述:`Deno Deploy 项目 your-org/your-app 的服务账号` 4. 点击“创建并继续” #### 第 5 步:授予服务账号角色 1. 根据需要选择角色: - Cloud Storage:`Storage 对象查看者` 或 `Storage 管理员` - Cloud SQL:`Cloud SQL 客户端` - 其他服务:选择相关角色 2. 点击“继续”,然后“完成” #### 第 6 步:配置工作负载身份绑定 1. 回到已创建的服务账号 2. 点击“有访问权限的主体”标签 3. 点击“授予访问权限” 4. 配置主体 - 选择一种方式: **应用中所有上下文:** - 新主体: `principalSet://iam.googleapis.com/projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-deno-com/attribute.full_slug/YOUR_ORG/YOUR_APP` **仅特定上下文:** - 新主体: `principal://iam.googleapis.com/projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-deno-com/subject/deployment:YOUR_ORG/YOUR_APP/production` - 对每个上下文(如 staging 等)重复添加 5. 角色:**工作负载身份用户** 6. 点击“保存” #### 第 7 步:获取必要的值 您需要两个值用于 Deno Deploy 配置: 1. **工作负载提供者 ID**: - 回到工作负载身份联合 - 点击您的池,然后点击您的提供者 - 复制提供者资源名称(完整路径,以 `projects/` 开头) 2. **服务账号邮箱**:在服务账号详情页复制 #### 第 8 步:验证配置 最终工作负载身份池概览应显示: - 您的工作负载身份池与 OIDC 提供者 - 已连接的服务账号 - 已正确配置的绑定 在 Deno Deploy 云连接配置中使用服务账号邮箱和工作负载提供者 ID。 ### GCP:使用 Terraform 您可以使用 Terraform 编程方式创建 GCP 所需的资源。此方案适合基础设施即代码的工作流程。 #### Terraform 配置示例 创建一个 Terraform 配置文件,内容如下: ```hcl # 变量定义 variable "org" { description = "Deno Deploy 组织名称" type = string } variable "app" { description = "Deno Deploy 应用名称" type = string } variable "contexts" { description = "允许的特定上下文列表(留空表示允许所有上下文)" type = list(string) default = [] } variable "project_id" { description = "GCP 项目 ID" type = string } variable "roles" { description = "授予服务账号的 IAM 角色列表" type = list(string) default = [] } # 项目信息数据源 data "google_project" "project" { project_id = var.project_id } # 工作负载身份池 resource "google_iam_workload_identity_pool" "deno_deploy" { workload_identity_pool_id = "oidc-deno-com" display_name = "Deno Deploy Workload Id Pool" } # 工作负载身份提供者 resource "google_iam_workload_identity_pool_provider" "deno_deploy" { workload_identity_pool_id = google_iam_workload_identity_pool.deno_deploy.workload_identity_pool_id workload_identity_pool_provider_id = "oidc-deno-com" display_name = "Deno Deploy OIDC Provider" attribute_mapping = { "google.subject" = "assertion.sub" "attribute.org_slug" = "assertion.org_slug" "attribute.app_slug" = "assertion.app_slug" "attribute.full_slug" = "assertion.org_slug + \"/\" + assertion.app_slug" } oidc { issuer_uri = "https://oidc.deno.com" } } # 服务账号 resource "google_service_account" "deno_deploy" { account_id = "deno-${var.org}-${var.app}" display_name = "Deno Deploy ${var.org}/${var.app}" } # 基于上下文动态配置的工作负载身份绑定 resource "google_service_account_iam_binding" "workload_identity" { service_account_id = google_service_account.deno_deploy.name role = "roles/iam.workloadIdentityUser" members = length(var.contexts) > 0 ? [ # 仅允许特定上下文 for context in var.contexts : "principal://iam.googleapis.com/projects/${data.google_project.project.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.deno_deploy.workload_identity_pool_id}/subject/deployment:${var.org}/${var.app}/${context}" ] : [ # 允许所有上下文(使用属性映射) "principalSet://iam.googleapis.com/projects/${data.google_project.project.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.deno_deploy.workload_identity_pool_id}/attribute.full_slug/${var.org}/${var.app}" ] } # 授予服务账号权限 resource "google_project_iam_member" "service_account_roles" { for_each = toset(var.roles) project = var.project_id role = each.value member = "serviceAccount:${google_service_account.deno_deploy.email}" } # 输出工作负载提供者 ID output "workload_provider_id" { value = "projects/${data.google_project.project.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.deno_deploy.workload_identity_pool_id}/providers/${google_iam_workload_identity_pool_provider.deno_deploy.workload_identity_pool_provider_id}" } # 输出服务账号邮箱 output "service_account_email" { value = google_service_account.deno_deploy.email } ``` #### 使用示例 **针对整个应用(所有上下文)访问:** ```hcl module "deno_deploy_gcp" { source = "./path-to-terraform-module" org = "your-org" app = "your-app" project_id = "your-gcp-project-id" contexts = [] # 空列表表示允许所有上下文 roles = [ "roles/storage.objectViewer", "roles/cloudsql.client" ] } ``` **仅针对特定上下文:** ```hcl module "deno_deploy_gcp" { source = "./path-to-terraform-module" org = "your-org" app = "your-app" project_id = "your-gcp-project-id" contexts = ["production", "staging"] roles = [ "roles/storage.objectAdmin", "roles/cloudsql.client" ] } ``` #### 应用配置 1. 初始化 Terraform: ```bash terraform init ``` 2. 计划部署: ```bash terraform plan ``` 3. 应用配置: ```bash terraform apply ``` 应用完成后,Terraform 会输出工作负载提供者 ID 和服务账号邮箱,供您在 Deno Deploy 云连接配置中使用。 #### 自定义角色 `roles` 变量接受一个 GCP IAM 角色列表。常见角色包括: - `roles/storage.objectViewer` - Cloud Storage 读取权限 - `roles/storage.objectAdmin` - Cloud Storage 对象完全权限 - `roles/cloudsql.client` - Cloud SQL 访问权限 - `roles/secretmanager.secretAccessor` - Secret Manager 访问权限 - 也可指定自定义角色 --- # 数据库 > 连接到外部数据库实例,实现您的应用及其环境无缝集成。 URL: https://docs.deno.com/deploy/reference/databases Deno Deploy 的数据库功能让您的应用轻松连接多种数据库,实现应用状态的无缝管理。目前支持 PostgreSQL 和 Deno KV。 创建或链接数据库实例后,Deno Deploy 会在该实例内自动为每个部署环境创建隔离的(逻辑)数据库,包括生产环境、Git 分支和预览时间线。您的应用代码根据当前环境,使用自动注入的环境变量连接到相应的数据库。这确保了数据在不同的开发和部署阶段保持一致且隔离。 Deno Deploy 当前支持两种数据库引擎: - **PostgreSQL** — 连接已有的外部托管 PostgreSQL 实例,或者通过 Deno Deploy 由 Prisma 托管的托管 PostgreSQL 数据库。 - **Deno KV** — 一个快速的、全球分布的面向边缘计算的键值存储。 ## 创建数据库实例 有两种方式将数据库实例添加到您的 Deno Deploy 组织: - **链接数据库**:连接已有的外部数据库实例(例如您自行运行的 PostgreSQL 服务器或云服务商托管的实例)。 - **供应数据库**:创建并附加 Deno Deploy 提供的托管数据存储(Deno KV 或 Prisma Postgres)。 ### 链接外部数据库 要链接已有的外部数据库实例,可以: - 访问组织仪表盘中的“Databases”页面,点击“Link Database”按钮; - 进入应用设置的“Databases”标签页,点击“Attach Database”,然后在数据库实例选择下拉菜单中选择“Link Database”。 接下来,填写外部数据库实例的连接详情。您需要提供: - **引擎**:选择数据库引擎(当前仅支持 PostgreSQL)。 - **连接详情**:输入主机名、端口、用户名、密码,必要时提供 CA 证书。您也可以粘贴连接字符串,自动填充以上字段。 - **Slug**:给数据库实例起一个描述性名称,在仪表盘中识别此实例。该名称仅供 Deno Deploy 内部使用,不影响实际数据库服务器。 填写完成后,点击“Test Connection”验证设置。连接成功后,点击“Save”将实例添加到组织。 如果连接失败,请核对连接详情,确认数据库服务器能从 Deno Deploy 网络访问。目前无法提供 Deno Deploy 的 IP 地址列表,请确保数据库服务器允许所有 IP 连接。如有困难,可[联系客服](/deploy/support/)寻求帮助。 :::info 由于 Deno Deploy 会为每个环境(生产、Git 分支和预览)创建隔离数据库,请确保您提供的数据库用户拥有在服务器上创建新数据库的足够权限。 ::: #### TLS/SSL 配置 链接外部数据库时,Deno Deploy 支持安全的 SSL/TLS 连接。根据您的数据库提供商,您可能需要上传 CA 证书以验证服务器身份。 如果数据库提供商使用受信任的根 CA (如 Let's Encrypt),则无需上传证书,SSL 连接会自动生效。 使用 AWS RDS 的用户,Deno Deploy 会自动检测 RDS 实例,并提供“使用 AWS 证书包”的选项,无需手动下载证书。 Google Cloud SQL 用户需从 Google Cloud 控制台下载 Google Cloud SQL CA 证书,并在链接数据库时上传。 其他使用自签名证书或私有 CA 的提供商,您需上传用于签署数据库证书的具体 CA 证书。通常可从提供商的文档或控制台获得。 ### 供应托管数据库 要创建并附加 Deno Deploy 托管的数据库实例,您可以: - 在组织仪表盘的“Databases”页面点击“Provision Database”按钮; - 进入应用设置的“Databases”标签页,点击“Attach Database”,然后在下拉菜单中选择“Provision Database”。 然后选择要供应的数据库引擎。当前可用: - **Deno KV** — 一个快速的、全球分布的边缘键值存储,由 Deno 代表您托管。 - **Prisma Postgres** — 世界上最先进的开源关系型数据库,由 [Prisma](https://www.prisma.io/) 托管。 您需要提供一个 **Slug**,用于在仪表盘中标识数据库实例。该名称仅供 Deno Deploy 内部使用,不影响实际数据库服务器。 根据所选引擎,可能还会有额外配置选项,比如选择区域。选择靠近应用用户的区域有助于降低延迟,提升查询性能。 准备好后,点击“Provision”创建数据库实例。Deno Deploy 会处理配置过程并为您搭建必要基础设施。 ## 将数据库关联到应用 创建或链接数据库实例后,您可以将它们分配给应用。每个数据库实例可分配给多个应用。每个应用在实例中对应环境(生产、Git 分支、预览时间线)会拥有自己的隔离数据库。 :::info 当前单个应用不能关联多个数据库实例。因此暂不支持同时关联 Deno KV 和 PostgreSQL 数据库到同一应用。 ::: 分配后,Deno Deploy 会为应用中的每个时间线自动创建隔离数据库,命名规则如下: - 生产环境数据库格式为 `{app-id}-production` - 每个 Git 分支数据库格式为 `{app-id}--{branch-name}` - 单个预览数据库格式为 `{app-id}-preview` :::info 当前每个应用仅创建一个预览数据库,所有预览部署共享该数据库。未来版本将为每个预览部署创建独立数据库。 ::: 要分配数据库给应用,您可以: - 在组织仪表盘“Databases”页面找到数据库实例,点击“Assign”,然后从下拉菜单选择应用。 - 进入应用设置的“Databases”标签页,点击“Attach Database”,然后选择数据库实例。 分配后,Deno Deploy 会自动为应用每个环境创建所需的隔离数据库。 ## 从代码连接数据库 分配数据库给应用后,连接数据库非常简单。Deno Deploy 会自动处理连接详情、凭证和环境变量。 ### Deno KV 对于 Deno KV,您可以使用内置的 `Deno.openKv()` API 连接分配的 Deno KV 实例。无需额外配置,Deno Deploy 会根据当前环境自动连接到正确的实例。 ```typescript // 无需参数,Deno Deploy 自动处理 const kv = await Deno.openKv(); Deno.serve(async () => { // 使用 Deno KV 实例 await kv.set(["user", "123"], { name: "Alice", age: 30 }); const user = await kv.get(["user", "123"]); return new Response(JSON.stringify(user.value), { headers: { "content-type": "application/json" }, }); }); ``` ### PostgreSQL 针对 PostgreSQL 数据库(包括外部和供应的),Deno Deploy 会自动将标准数据库环境变量注入应用运行环境: - `DATABASE_URL`:当前环境的完整连接字符串,格式为 `postgresql://username:password@hostname:port/database`。 - `PGHOST`:数据库服务器主机名。 - `PGPORT`:数据库服务器端口。 - `PGDATABASE`:当前环境数据库名。 - `PGUSER`:数据库用户名。 - `PGPASSWORD`:数据库密码。 若您的数据库需要自定义 SSL/TLS 证书,Deno Deploy 也会将该证书注入默认证书存储,确保 SSL 连接自动生效。 您可以使用喜欢的 PostgreSQL 客户端库(如 npm 包 `pg`)通过这些环境变量连接数据库。大多数库都会自动识别并使用这些标准环境变量,无需额外配置。 以下是示例代码,展示在 Deno Deploy 应用中如何连接 PostgreSQL: ```typescript import { Pool } from "npm:pg"; // 无需参数,库自动从环境变量读取连接信息 const pool = new Pool(); Deno.serve(async () => { // 使用数据库 const result = await pool.query("SELECT * FROM users WHERE id = $1", [123]); return new Response(JSON.stringify(result.rows), { headers: { "content-type": "application/json" }, }); }); ``` ## 执行迁移和数据填充(seeding) 由于每个环境都有自己的隔离数据库,通常每个应用都会涉及多个独立数据库。每次部署新版本时,手动对每个数据库执行迁移或插入种子数据不现实。 为简化该过程,Deno Deploy 允许您配置自动化预部署命令,每当版本发布到时间线时,在部署开始前运行该命令。 该命令拥有与应用相同的环境变量可用,包括 `PGHOST`、`PGPORT`、`PGDATABASE` 等,便于使用现有迁移工具执行迁移或填充数据。 要设置自动化迁移命令,前往应用设置页面的“App Config”部分,在“Pre-Deploy Command”字段编辑预部署命令(例如 `deno task migrate` 或 `npm run migrate`)。 您可以在修订构建日志的“Deployment”部分查看预部署命令执行的详细日志。 例如,您可以使用 [`node-pg-migrate`](https://github.com/salsita/node-pg-migrate) 设置迁移脚本: 1. 在 `deno.json` 中添加任务: ```json { "tasks": { "migrate": "deno run --allow-net --allow-env --allow-read --allow-write npm:node-pg-migrate up" } } ``` 2. 创建 migrations 目录并添加迁移文件,如 `migrations/1234567890_create-users-table.js`: ```javascript exports.up = (pgm) => { pgm.createTable("users", { id: "id", name: { type: "varchar(100)", notNull: true }, email: { type: "varchar(100)", notNull: true }, created_at: { type: "timestamp", notNull: true, default: pgm.func("current_timestamp"), }, }); }; exports.down = (pgm) => { pgm.dropTable("users"); }; ``` 3. 在应用设置中将预部署命令设置为 `deno task migrate`。 Deno Deploy 会在每次部署之前自动运行该命令,确保您的环境特定数据库保持最新。 其他迁移工具,如 Prisma Migrate、Drizzle 或 Kysely,也可以用类似方式配置。 ## 本地开发 本地开发时,您有两种方案配置数据库: - 使用本地运行的数据库实例,如本地 PostgreSQL 服务器或 Deno 内置的 Deno KV 后端。 - 通过 `--tunnel` 连接 Deno Deploy 上供应的托管隔离本地开发实例。 ### 使用本地数据库实例 #### Deno KV 对于 Deno KV,您可以调用内置的 `Deno.openKv()` API 连接本地 Deno KV 实例。默认使用存储在主目录的本地文件后端。 ```typescript const kv = await Deno.openKv(); // 连接本地 Deno KV 实例 Deno.serve(async () => { // 使用 Deno KV 实例 await kv.set(["user", "123"], { name: "Alice", age: 30 }); const user = await kv.get(["user", "123"]); return new Response(JSON.stringify(user.value), { headers: { "content-type": "application/json" }, }); }); ``` #### PostgreSQL 要在本地安装 PostgreSQL,请参考 [postgresql.org](https://www.postgresql.org/download/) 上针对您操作系统的说明。macOS 用户可使用 `brew install postgresql`,多数 Linux 发行版通过软件包管理器提供 PostgreSQL 包。 安装后,为本地开发创建新数据库和用户: ```bash createdb myapp_dev createuser myuser --pwprompt ``` 在项目根目录设置 `.env` 文件,填写本地 PostgreSQL 连接信息: ```bash DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/myapp_dev ``` 然后,您可以使用喜欢的 PostgreSQL 客户端库(例如 npm 包 `pg`)通过 `DATABASE_URL` 环境变量连接本地数据库。 若使用 Deno 本地开发,可通过 `--env-file` 标志从 `.env` 文件自动加载环境变量: ```bash deno run -A --env-file main.ts ``` ### 使用托管隔离的本地开发实例 Deno Deploy 支持使用 [`--tunnel` 标志](https://docs.deno.com/deploy/reference/tunnel) 连接托管隔离的本地开发数据库。 操作流程: 1. 在您的 Deno Deploy 组织中供应一个数据库实例,像往常一样分配给应用。 2. 使用 `--tunnel` 标志启动本地开发服务器: ```bash deno task --tunnel dev # 或 deno run -A --tunnel main.ts ``` Deno Deploy 会自动注入相应的数据库环境变量,使代码能连接托管的隔离数据库。请注意,该实例在使用同一应用的所有开发者间共享,修改数据时请谨慎。 :::info 使用 `--tunnel` 标志时,Deno Deploy 同时启用其他功能,如: - 将日志、跟踪和指标从本地导出到 Deno Deploy 仪表盘 - 将“Local”环境变量拉取至本地环境 - 将本地服务器暴露到公共 URL,方便分享和测试 若不需要这些功能,建议使用本地数据库实例。 ::: ## 数据库管理 ### 组织层面 您可在组织仪表盘的“Databases”页面查看和管理所有数据库实例。这里列出所有链接和供应的数据库实例及其状态、分配的应用和连接详情(如适用)。 点击数据库实例进入详细页面,可查看实例信息,包括分配的应用和实例内创建的各个数据库。每个数据库显示名称、状态及关联的应用和时间线。 若数据库创建失败,将显示错误状态和“Fix”按钮,您可以重试或查看详细错误信息。 在该页面,您还可将数据库实例从应用分离,或完全删除实例。 每个数据库旁边可以打开数据库浏览器(仅限 PostgreSQL),或复制连接字符串(`DATABASE_URL`),方便在本地调试时连接数据库。 ### 应用层面 在应用设置的“Databases”标签页,也可以管理数据库实例。这里能看到分配给应用的数据库实例及其状态和连接详情。 可以查看为应用内所分配实例创建的所有数据库,包括生产环境、Git 分支和预览时间线。每个数据库显示名称和状态。 支持打开数据库浏览器(PostgreSQL)以及复制每个数据库的连接字符串(`DATABASE_URL`),方便本地调试。 ## 数据库浏览器 Deno Deploy 为 PostgreSQL 数据库内置数据库浏览器,允许您直接在仪表盘浏览和管理数据。 访问方法: 进入应用设置“Databases”标签页,找到分配的 PostgreSQL 数据库实例,点击“Explore Database”打开数据库浏览界面,您可以查看表格、执行查询及管理数据。 :::info 数据库浏览器目前不支持 Deno KV 数据库。 ::: ## Prisma Postgres 实例及“认领”操作 通过 Deno Deploy 供应的托管 Prisma Postgres 实例初始为“未认领”状态。这意味着实例由 Deno Deploy 代表您管理,您无法直接访问拥有该实例的 Prisma 账户。未认领实例会根据 Deno Deploy 计划有限制数据库数量和大小。 如果希望完全控制 Prisma Postgres 实例(包括升级 Prisma 计划),可以“认领”该实例到您自己的 Prisma 账户。操作方法:在 Deno Deploy 仪表盘数据库实例详情页点击“Claim Instance”按钮,按照指引将实例关联到您的 Prisma 账户。 :::warning 认领 Prisma Postgres 实例为永久操作,无法撤销。认领后,实例归您的 Prisma 账户管理,并适用您的 Prisma 账户配额(基于计划)。Deno Deploy 自动为每个应用创建隔离数据库的功能依然正常工作。 ::: ## 限制 您在组织内可以创建无限数量的链接数据库实例和 Deno KV 实例。 托管的 Prisma Postgres 实例,每个组织只能创建少数几个实例。每个实例有数据库数量限制,直接影响可分配给该实例的应用数量。达到限制后,该实例将无法再创建新数据库。 若 Prisma Postgres 实例被“认领”,数据库数量限制由您的 Prisma 计划决定。未认领实例则适用默认限制。 托管 Prisma Postgres 实例的数据库大小和操作次数限制也根据您所选计划而设。在实例详情页底部展示使用情况。若实例被认领,限制由 Prisma 计划决定,未认领实例则适用默认限制。 ## 常见问题 **问:多个应用可以共享同一个数据库实例吗?** 答:可以!多个应用可以分配到同一个数据库实例,每个应用在该实例内拥有自己的隔离数据库。 **问:将应用从数据库实例中移除后,数据会怎样?** 答:数据库仍保留在服务器上,仅移除应用与数据库实例的连接关系。 **问:多个环境可以使用同一个数据库吗?** 答:默认每个环境(生产、分支、预览)拥有独立数据库,以确保隔离和防止数据冲突。但您可通过数据库库中的选项自定义代码连接的数据库。 **问:如何直接访问我的数据库?** 答:您可以使用提供的连接详情直接连接数据库服务器。请使用 Deno Deploy 仪表盘中显示的数据库名称。 **问:可以修改数据库连接信息吗?** 答:可以,点击任意数据库实例的“Edit”更新连接详情。保存前请测试连接确保有效。 **问:如何删除数据库实例?** 答:先移除实例的所有应用分配,然后点击数据库实例的“Delete”。此操作仅从 Deno Deploy 中移除连接,实际数据库服务器不受影响。 --- # Deno KV > 在你的应用中使用 Deno KV,每个时间线配备专用数据库 URL: https://docs.deno.com/deploy/reference/deno_kv [Deno KV] 是 Deno Deploy 支持的一种键值数据库引擎选项,属于 [databases] 功能的一部分。借助 Deno Deploy 早期访问(EA)中的新功能 [timelines],你的应用可以完全控制所使用的 Deno KV 数据库(例如,一个用于生产环境,每个 Git 分支一个),确保不同环境间的数据隔离和安全。 与其他数据库引擎类似,代码会自动连接到对应环境的正确数据库——无需手动检测时间线或命名数据库。 ## 入门指南 ### 添加 KV 数据库 进入你的组织面板,点击导航栏中的“Databases”。点击“Provision Database”,选择 Deno KV 作为数据库引擎,填写一个易记的名称,然后保存。 ### 将应用连接到 KV 数据库 拥有数据库实例后,可以为应用分配数据库。从数据库实例列表中,点击想使用数据库旁的“Assign”,然后从下拉菜单中选择应用。 Deno Deploy 会自动为每个时间线创建独立的数据库。在开发和测试阶段,这可以保护你的生产数据安全。你可以监控配置过程,等待状态变为“Connected”。如遇错误,点击“Fix”重试。 ## 在代码中使用 Deno KV 分配数据库给应用后,从代码连接非常简单。Deno Deploy 会根据当前环境设置到正确数据库的连接。 ### 示例 以下是在你的 Deno Deploy 应用中连接 Deno KV 的示例: ```typescript const kv = await Deno.openKv(); Deno.serve(async () => { const res = await kv.get(["requests"]); const requests = res.value + 1; await kv.set(["requests"], requests); return new Response(JSON.stringify(requests)); }); ``` 有关 Deno KV 及其功能的详细信息,请参阅 [Deno KV 文档][Deno KV]。 ## 取消分配 KV 数据库 如果你从应用中移除数据库分配,该应用将无法访问该数据库。但数据库及其数据依然保留,可以以后重新分配给其他应用或同一应用。将鼠标悬停在数据库列表中已分配应用名称上,点击“取消应用分配”图标即可取消分配。 ## 数据分布和 GDPR Deno KV 数据库会在主区域北弗吉尼亚(us-east4)至少三个数据中心之间进行复制。一旦写操作提交,其变更会在主区域的数据中心法定数量内被持久存储,且在欧洲和亚洲设有只读副本。 因此,写入 KV 的数据会存储并传输经过美国。对于要求仅限欧盟数据驻留或严格符合 GDPR 规定的工作负载,Deno KV 目前不适用。数据在传输和静态时均被加密,且 KV 在每个项目间隔离,但我们目前不提供专门针对 KV 的正式 GDPR 或合规文档。 对于有严格 GDPR 合规要求的项目,我们推荐使用我们的[Postgres 数据库解决方案](https://docs.deno.com/deploy/reference/databases/),例如我们的[Prisma 集成](/deploy/reference/databases/#prisma-postgres-instances-and-claiming)。 ## 数据存储 在本地开发环境中,数据保存在内存中。你无需提前创建或分配数据库即可本地使用 KV API,且 KV 代码在各环境间保持一致。 ## 删除数据库实例 在数据库实例列表中点击 Deno KV 项目旁的“Delete”。与其他数据库引擎不同,此操作会删除所有已有的 Deno KV 数据库及其数据。请务必先备份数据后再操作。 [Deno KV]: /deploy/kv/ [databases]: /deploy/reference/databases/ [timelines]: /deploy/reference/timelines/ --- # 域名 > Deno Deploy 早期访问中的域管理完整指南,包括组织域、自定义域、DNS 配置、TLS 证书和域分配。 URL: https://docs.deno.com/deploy/reference/domains :::info 您正在查看 Deno DeployEA 的文档。想查看 Deploy Classic 的文档吗?[请点击这里](/deploy/)。 ::: 每个组织都有一个默认域,用于该组织内部部署的所有应用。例如,一个标识为 `acme-inc` 的组织,其默认域为 `acme-inc.deno.net`。名为 `my-app` 的应用将自动获得生产域名 `my-app.acme-inc.deno.net`。 除了这些默认域之外,您还可以为应用添加自定义域。自定义域是您拥有并控制的域。要使用自定义域,您必须: 1. 拥有该域(已从域名注册商购买) 2. 能够编辑其 DNS 记录 自定义域属于某个组织,可以附加到该组织内的任何应用。 自定义域可以添加为: - 基础域(例如,`example.com` 或特定子域) - 通配符域(例如,`*.example.com`) 基础域仅适用于单一应用,而通配符域提供更灵活的使用方式。您可以: - 将整个通配符域分配给一个应用(所有子域指向同一应用) - 部分分配给多个应用(不同子域指向不同应用) 所有自定义域都需要有效的 TLS 证书。Deno DeployEA 可以通过 Let's Encrypt 自动生成这些证书。 ## 添加自定义域 1. 进入组织域页面(点击左上角的组织名称,然后切换到“域”标签) 2. 点击“添加域” 3. 输入您的域(例如 `example.com`) 4. 选择是仅添加该域还是同时包含通配符子域 5. 点击“添加域” 这将打开域配置抽屉。 ### DNS 配置 域配置抽屉显示了所需的 DNS 记录,用于: - 验证域所有权 - 生成 TLS 证书 - 将流量路由到 Deno DeployEA 根据您的域名注册商支持的能力,有三种可能的配置方法: #### ANAME/ALIAS 方法(首选) 如果您的注册商支持 `ANAME` 或 `ALIAS` 记录,这是最佳选择: - 添加一个 `ANAME`/`ALIAS` 记录 - 添加一个用于验证的 `CNAME` 记录 #### CNAME 方法 适合子域,但不适用于根域(顶级域): - 添加两个 `CNAME` 记录 - 注意:此方法不允许在同一域上使用其他 DNS 记录(如 `MX` 记录) #### A 记录方法 兼容性最强,但配置较复杂: - 添加一个 `A` 记录 - 添加一个用于验证的 `CNAME` 记录 > 注意:当前 Deno DeployEA 不支持 IPv6。使用 `ANAME/ALIAS` 或 `CNAME` 方法时,当支持 IPv6 时,您的域名将自动使用 IPv6。使用 `A` 方法时,将在需要添加 `AAAA` 记录时通过邮件通知您。 :::caution 当您使用 Cloudflare 作为 DNS 提供商时,**必须**禁用 `_acme-challenge` CNAME 记录的代理功能(即关闭橙色云图标),否则验证和证书发放会失败。 ::: ### 验证 添加 DNS 记录后,Deno DeployEA 将验证您的域所有权。此过程根据您的 DNS 提供商可能需要几分钟时间。您可以在验证期间保持域配置抽屉打开——验证完成后它会自动刷新。 您可以通过点击“生成证书”按钮手动触发验证。验证成功同时会启动 TLS 证书的生成。 ### TLS 证书生成 域名验证后,点击“生成证书”按钮,通过 Let's Encrypt 生成 TLS 证书。这个过程最多需 90 秒钟。 生成后,您将看到证书的详细信息,包括到期时间和颁发时间。 证书会在接近到期时自动续签。您可以在域配置抽屉中查看当前证书状态。 如果自动续签失败(例如,DNS 记录发生变化),您将在证书到期前 14 天收到电子邮件通知。您可以修复问题并联系支持团队尝试重新续签。如果在到期前未续签,域名将停止工作。 #### 自行提供证书 如果您希望使用自己的 TLS 证书,可以在域配置抽屉中上传。您需要提供: - 证书文件(PEM 格式) - 私钥文件(PEM 格式) 上传后,将使用该证书为域名服务。您需自行负责在证书过期前续签并更新证书。 您将在证书到期前 14 天收到邮件通知,提醒您更新证书。若证书过期,域名将停止工作。 上传时,TLS 证书必须有效,且通过证书中的通用名称或主题备用名称覆盖基础域(及通配符子域,如果有通配符域)。私钥与证书必须匹配,并且类型必须是 RSA(2048、3072 或 4096 位)或 ECDSA(P-256、P-384 或 P-521)。 ## 将自定义域分配给应用 添加自定义域到组织后: 1. 进入组织域页面 2. 点击自定义域旁的“分配”按钮 3. 选择目标应用 4. 若使用通配符域,选择是附加基础域、通配符子域还是特定子域 5. 点击“分配域名” ## 从应用中撤销自定义域分配 1. 进入应用设置页面 2. 找到“自定义域”部分 3. 点击要撤销分配的域名旁的“移除”按钮 这将从该应用移除域名,但该域名仍在您的组织中保留,可用于其他应用。 ## 删除自定义域 1. 进入组织域页面 2. 打开域配置抽屉 3. 点击“删除”并确认 这会将自定义域从您的组织中移除,并删除该域在所有应用中的所有分配。 ## 将自定义域从 Deploy Classic 迁移到 Deno Deploy 如果您之前在 Deploy Classic 上设置过自定义域,并希望迁移到 Deno Deploy,我们提供了一个[分步教程](/examples/migrate_custom_domain_tutorial/)指导您完成此过程。 --- # 环境变量和上下文 > Deno Deploy 早期访问版中管理环境变量和上下文的指南,包括变量类型、创建、编辑以及在代码中访问它们的方式。 URL: https://docs.deno.com/deploy/reference/env_vars_and_contexts :::info 您正在查看 Deno DeployEA 的文档。正在寻找 Deploy Classic 的文档?[请点击这里查看](/deploy/)。 ::: Deno DeployEA 中的环境变量允许您使用静态值配置应用程序,例如 API 密钥或数据库连接字符串。 ## 环境变量类型 环境变量可以以以下形式存储: - **纯文本**:在 UI 中可见,适用于非敏感值,如特征标志 - **密钥**:创建后在 UI 中不可见,仅能从应用代码中读取,适用于敏感值,如 API 密钥 变量可以设置在: - **应用级别**:特定于单个应用 - **组织级别**:应用于组织中的所有应用,但可以被应用级变量覆盖 ## 上下文 每个环境变量适用于一个或多个上下文。上下文代表代码运行的逻辑“环境”,每个环境拥有自己的一组变量和密钥。 默认情况下,有两个上下文: - **生产**:用于生产时间线,服务生产流量 - **开发**:用于开发时间线,服务非生产流量(预览 URL 和分支 URL) :::info 需要额外的上下文?请联系 [支持](../support)。 ::: 此外,还有一个用于构建过程中的 **构建** 上下文。构建上下文中的环境变量仅在构建期间可用,在生产和开发上下文中不可访问(反之亦然)。这种分离使得构建时和运行时可以有不同的配置。 在单个应用或者组织内,同一上下文中不能存在多个同名的环境变量;但可以在不同且不重叠的上下文中存在同名变量。 ## 添加、编辑和删除环境变量 您可以从多个位置管理环境变量: - 在创建应用时的 “新建应用” 页面 - 在应用设置的 “环境变量” 部分 - 在组织设置的 “环境变量” 部分 在每个位置,点击相应的编辑按钮打开环境变量抽屉。更改仅在点击 “保存” 后生效。点击 “取消” 会放弃更改。 添加变量步骤: 1. 点击 “添加环境变量” 2. 输入名称和值 3. 指定是否为密钥 4. 选择适用的上下文 您也可以从 `.env` 文件批量导入变量: 1. 点击 “+ 从 .env 文件添加” 2. 粘贴 `.env` 文件内容 3. 点击 “导入变量” 注意以 `#` 开头的行会被视为注释。 删除变量,点击其旁边的 “删除” 按钮。 编辑变量,点击其旁边的 “编辑” 按钮,可修改名称、值、密钥状态或适用上下文。 ## 在代码中使用环境变量 通过 `Deno.env.get` API 访问环境变量: ```ts const myEnvVar = Deno.env.get("MY_ENV_VAR"); ``` ## 限制 环境变量有以下限制: - 环境变量键最大长度为 128 字节。 - 环境变量键不能以以下前缀开头: - `DENO_`,但允许以下除外:`DENO_AUTH_TOKENS`、`DENO_COMPAT`、`DENO_CONDITIONS`、 `DENO_DEPLOY_ENDPOINT` 或 `DENO_DEPLOY_TOKEN` - `LD_` - `OTEL_` - 环境变量值最大长度为 16 KB(16,384 字节)。 - 环境变量键不能为以下任一键。请改用 [云连接](/deploy/early-access/reference/cloud-connections) - `AWS_ROLE_ARN` - `AWS_WEB_IDENTITY_TOKEN_FILE` - `GCP_WORKLOAD_PROVIDER_ID` - `GCP_SERVICE_ACCOUNT_EMAIL` - `GCP_PROJECT_ID` - `AZURE_CLIENT_ID` - `AZURE_TENANT_ID` - `AZURE_FEDERATED_TOKEN_FILE` ## 预定义环境变量 Deno DeployEA 在所有上下文中提供以下预定义环境变量: - `DENO_DEPLOY=1`:表示应用正在 Deno Deploy 环境中运行。 - `DENO_DEPLOYMENT_ID`:表示整个配置集(应用 ID、修订 ID、上下文和环境变量)的唯一标识符。当其中任何组件更改时此值也会变化。 - `DENO_DEPLOY_ORG_ID`:应用所属组织的 ID。 - `DENO_DEPLOY_ORG_SLUG`:应用所属组织的标识符。 - `DENO_DEPLOY_APP_ID`:应用的 ID。 - `DENO_DEPLOY_APP_SLUG`:应用的标识符。 - `DENO_DEPLOY_BUILD_ID`:当前运行的修订版本 ID。 - `DENO_TIMELINE`:应用当前运行的时间线。可能的值包括 `production`、`git-branch/` 和 `preview/`。构建期间不设置此变量,因为构建不针对特定时间线。 构建期间,环境变量中还会额外设置 `CI=1`。 --- # 框架 > Deno Deploy 早期访问中支持的 JavaScript 和 TypeScript 框架的详细指南,包括 Next.js、Astro、Nuxt、SvelteKit 等。 URL: https://docs.deno.com/deploy/reference/frameworks Deno Deploy 支持多种 JavaScript 和 TypeScript 框架的开箱即用。这意味着您可以在无需额外配置或设置的情况下使用这些框架。 您正在查看 Deno DeployEA 的文档。想要查看 Deploy Classic 文档?[点击这里查看](/deploy/)。 ::: Deno DeployEA 开箱即支持多种 JavaScript 和 TypeScript 框架。这意味着您可以在无需额外配置或设置的情况下使用这些框架。 原生支持的框架已经过测试,能在 Deno DeployEA 上正常运行,并且在创建新应用时会被自动检测。Deno DeployEA 会自动优化这些框架的构建和运行时配置,以达到最佳状态。 未列出的框架仍很可能可用,但可能需要您手动配置安装和/或构建命令,以及构建设置中的运行时配置。 觉得有什么框架缺失?欢迎在[Deno Deploy Discord 频道](https://discord.gg/deno)告诉我们,或[联系 Deno 支持](../support)。 ## 支持的框架 ### Next.js (`nextjs`) Next.js 是用于构建全栈 Web 应用的 React 框架。您使用 React 组件构建用户界面,同时使用 Next.js 提供的额外功能和优化。 页面路由和应用路由均开箱即支持。支持 ISR、SSG、SSR 和 PPR。缓存开箱即支持,包括使用新的 `"use cache"`。 `next/image` 开箱即用。 Deno DeployEA 上的 Next.js 始终在独立模式下构建。 支持开箱即用的追踪功能,Next.js 会自动为传入请求、路由、渲染及其他操作发出一些跨度(spans)。 ### Astro (`astro`) Astro 是一个用于构建内容驱动网站(如博客、营销和电商)的 Web 框架。Astro 尽可能多地利用服务器渲染而非浏览器端渲染。 对于静态 Astro 网站,使用 Deno DeployEA 无需额外配置。 在 Deno DeployEA 上使用 Astro 的 SSR 时,您需要安装 [@deno/astro-adapter](https://github.com/denoland/deno-astro-adapter) 包,并将您的 `astro.config.mjs` 文件配置为使用该适配器: ```bash $ deno add npm:@deno/astro-adapter # 或 npm install @deno/astro-adapter # 或 yarn add @deno/astro-adapter # 或 pnpm add @deno/astro-adapter ``` ```diff title="astro.config.mjs" import { defineConfig } from 'astro/config'; + import deno from '@deno/astro-adapter'; export default defineConfig({ + output: 'server', + adapter: deno(), }); ``` 支持 Sharp 图像优化。 支持 `astro:env` API。 ### Nuxt (`nuxt`) 使用 Nuxt 轻松创建高质量 Web 应用,Nuxt 是一个开源框架,使基于 Vue.js 的全栈开发变得直观。 Nuxt 无需额外设置。Deno Deploy 会自动配置 Nitro,因此无需额外的预设配置。 ### SolidStart (`solidstart`) SolidStart 是一个开源元框架,旨在统一组成 Web 应用的组件。它建立于 Solid 之上。 SolidStart 无需额外设置。Deno Deploy 会自动配置 Nitro,因此无需额外的预设配置。 ### TanStack Start (`tanstackstart`) TanStack Start 是一个基于 TanStack Router 的全栈 React 或 Solid 框架。它使用 Nitro 作为服务器层,从而支持在 Deno Deploy 上部署。 要在 Deno Deploy 上部署 TanStack Start,您需要安装 `nitro` 并配置 Nitro Vite 插件: 1. 将 `nitro` 添加到您的依赖中: ```bash deno add npm:nitro-nightly@latest # 或 npm install nitro-nightly@latest ``` 2. 配置您的 `vite.config.ts` 使用 Nitro 插件: ```ts title="vite.config.ts" import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import viteReact from "@vitejs/plugin-react"; export default defineConfig({ plugins: [tanstackStart(), nitro(), viteReact()], }); ``` Deno Deploy 会自动配置 Nitro,因此无需额外的预设配置。 ### SvelteKit (`sveltekit`) SvelteKit 是一个使用 Svelte 快速开发健壮且高性能 Web 应用的框架。 SvelteKit 无需额外设置。 ### Fresh (`fresh`) Fresh 是一个面向 JavaScript 和 TypeScript 开发者的全栈现代 Web 框架。Fresh 使用 Preact 作为 JSX 渲染引擎。 Fresh 无需额外设置。 ### Lume (`lume`) Lume 是一个用于利用 Deno 构建快速且现代网站的静态站点生成器。 Lume 无需额外设置。 ### Remix (`remix`) > ⚠️ **实验性质**:Remix 目前尚未完全支持。它正在被集成进 Deno DeployEA,某些功能可能无法按预期工作。如遇任何问题,请向 Deno 团队反馈。 --- # 可观测性 > Deno Deploy 中监控功能的全面概述,包括日志、跟踪、指标和过滤选项。 URL: https://docs.deno.com/deploy/reference/observability Deno Deploy 提供全面的可观测性功能,帮助您了解应用性能、调试错误和监控使用情况。这些功能利用了 OpenTelemetry 以及 [Deno 内置的 OpenTelemetry 集成](/runtime/fundamentals/open_telemetry/)。 Deno Deploy 中的三大主要可观测性功能是: - **日志**:应用代码发出的非结构化调试信息 - **跟踪**:关于请求处理的结构化信息,包括每个步骤的执行时间及自动捕获的出站 I/O 操作 - **指标**:关于应用性能和使用情况的结构化高层数据,例如请求数、错误数和延迟 ## 日志 Deno Deploy 中的日志通过标准的 `console` API 捕获,并可以在控制面板的日志页面中查询。 日志按应用组织。您可以使用搜索栏根据各种属性和消息内容过滤日志。 当日志在跟踪上下文中发出时,它们会与特定的跟踪和跨度关联。对于这类日志,日志界面会显示“查看跟踪”按钮,允许您在覆盖抽屉中打开相关跟踪,进行详细检查。 ## 跟踪 Deno Deploy 中的跟踪通过三种方式捕获: - **内置操作自动跟踪**:入站 HTTP 请求、出站 fetch 调用及其他系统操作会自动跟踪,且无法禁用。 - **支持框架自动跟踪**:如 Next.js、Fresh 和 Astro 等框架内置了自动监控。具体支持的框架和操作可能随时间变化。 - **手动自定义监控**:您的应用代码可以通过 OpenTelemetry API 创建新的跟踪或跨度。 跟踪按应用组织。搜索栏可让您根据各种属性和跨度名称过滤。 点击某个跟踪会打开跟踪覆盖抽屉,显示该跟踪中的所有跨度的瀑布视图。该视图展示每个跨度的开始时间、结束时间和持续时间,并按父跨度分组,根跨度位于最上方。 点击任一跨度,会在抽屉底部展示其详细信息,包括所有捕获的属性。例如,出站 HTTP 请求包含方法、URL 和状态码。 跨度详情部分还包括 “日志” 标签页,显示在该跨度上下文中发出的所有日志。 您可点击任意跟踪上的“查看日志”,在日志页打开该跟踪 ID 预填的搜索栏,显示与该跟踪相关的所有日志。 ## 指标 Deno Deploy 中的指标自动捕获多种操作数据,如入站 HTTP 请求和出站 fetch 调用,且无法禁用。 指标按应用组织,并以时间序列图的形式展示其随时间变化的数值。您可通过搜索栏按各种属性过滤指标。 ## 过滤 日志、跟踪和指标可使用以下通用属性进行过滤: - **版本**:发出数据的应用版本 ID - **上下文**:数据发出的上下文(“生产”或“开发”) 对日志和跟踪,额外提供以下过滤器: - **跟踪**:包含该日志或跨度的跟踪 ID 仅对跟踪提供以下额外过滤器: - **HTTP 方法**:触发跟踪的请求的 HTTP 方法 - **HTTP 路径**:触发跟踪的请求路径 - **HTTP 状态**:响应的 HTTP 状态码 ### 时间范围过滤 默认情况下,可观测性页面显示过去一小时的数据。您可以通过页面右上角的时间范围过滤器更改此设置。 您可以选择预定义的时间范围,如“最近 1 小时”、“最近 24 小时”或“最近 7 天”,也可以点击“自定义”按钮设置自定义时间范围。 自定义时间范围可以是绝对时间段(具体的开始和结束时间),也可以是相对时间段(例如 3 天前、1 小时后等)。相对时间段使用与 Grafana 相同的语法: - `now` - 当前时间 - `now-1h` - 1 小时前 - `now/h` - 当前小时的开始时间 - `now-1h/h` - 前一个小时的开始时间 - `now/d+3h` - 当天开始时间加 3 小时 - `now-1d/d` - 前一天的开始时间 --- # OIDC > Deno Deploy 运行时环境充当 OpenID Connect (OIDC) 提供者,使您能够与支持 OIDC 认证的第三方服务集成。 URL: https://docs.deno.com/deploy/reference/oidc Deno DeployEA 是一个 OIDC 提供者。Deno DeployEA 的每个正在运行的应用程序都可以获得由 Deno DeployEA 签发的短期 JWT 令牌。这些令牌包含有关应用程序的信息,例如组织和应用的 ID 与 slug、应用程序执行的上下文以及正在运行的修订版本 ID。 这些令牌可以用来与支持 OIDC 认证的第三方服务进行身份验证,例如主要的云提供商,也包括 HashiCorp Vault、NPM 等。 :::tip 想要使用 OIDC 令牌进行 AWS 或 Google Cloud 的身份验证?请使用[云连接](./cloud-connections)功能,而不是手动配置 OIDC 认证。云连接会为您处理整个配置流程,包括建立信任关系和权限设置。底层依然使用的是 OIDC。 ::: ## 签发令牌 要为当前正在运行的应用程序签发令牌,请使用来自 [`@deno/oidc` 模块(在 JSR 上)](http://jsr.io/@deno/oidc) 的 `getIdToken()` 函数。 首先,将 `@deno/oidc` 作为您的应用依赖安装: ```sh deno add jsr:@deno/oidc ``` 然后,引入 `getIdToken()` 函数,并使用所需的目标受众调用它: ```ts import { getIdToken } from "jsr:@deno/oidc"; const token = await getIdToken("https://example.com/"); console.log(token); ``` `audience` 参数是一个字符串,用于标识令牌的预期接收方。通常是一个 URL 或表示将消费该令牌的服务或应用的标识符。受众值必须与您希望进行身份验证的第三方服务中配置的值相匹配。该值会放入签发的 JWT 令牌的 `aud` 声明中。 `getIdToken()` 函数返回一个 Promise,解析后为字符串形式的 JWT 令牌。 若要检查当前环境是否支持 OIDC(即您的应用是否运行在 Deno DeployEA 上),可使用 `supportsIssuingIdTokens` 命名空间属性: ```ts import { supportsIssuingIdTokens } from "jsr:@deno/oidc"; if (supportsIssuingIdTokens) { // 支持 OIDC } else { // 不支持 OIDC } ``` ## 令牌结构 发放的令牌是采用 RS256 算法签名的 JWT 令牌。令牌包含以下声明: | 声明名称 | 示例值 | 描述 | | -------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------- | | `iss` | `https://oidc.deno.com` | 令牌的签发者,总是 `https://oidc.deno.com`。 | | `aud` | `https://example.com/` | 令牌的受众,即传入 `getIdToken()` 函数的值。 | | `iat` | `1757924011` | 令牌签发时间,Unix 时间戳,表示令牌签发时刻。 | | `exp` | `1757924311` | 令牌过期时间,Unix 时间戳,表示令牌失效时刻。 | | `nbf` | `1757923951` | 令牌生效时间,Unix 时间戳,表示令牌开始有效时刻。 | | `sub` | `deployment:deno/astro-app/production` | 令牌主题,是字符串拼接的形式:`deployment://` | | `org_id` | `729adb8f-20d6-4b09-bb14-fac14cb260d1` | 拥有该应用的组织的唯一标识符。 | | `org_slug` | `deno` | 拥有该应用的组织的 slug。 | | `app_id` | `16ad21d8-7aeb-4155-8aa3-9f58df87cd3e` | 应用的唯一标识符。 | | `app_slug` | `astro-app` | 应用的 slug。 | | `context_id` | `1d685676-92d7-418d-b103-75b46f1a58b4` | 应用运行的上下文的唯一标识符。 | | `context_name` | `production` | 应用运行的上下文。 | | `revision_id` | `rh2r15rgy802` | 当前运行的应用修订版本的唯一标识符。 | | `deployment_id`| <随机字符串> | 包含整个部署元数据(包括应用、修订和上下文 ID)的唯一哈希。 | 令牌会在签发后 5 分钟过期。为考虑时钟偏差,令牌中的 `nbf` 声明设置为比 `iat` 提前 1 分钟。 ## 验证令牌 要验证由 Deno DeployEA 签发的令牌,您需要从 OIDC 提供者的 JWKS 端点获取公钥。Deno DeployEA 的 JWKS 端点为: ``` https://oidc.deno.com/.well-known/jwks.json ``` 使用 JWT 令牌头中的 `kid`(密钥 ID)选择 JWKS 响应中的正确密钥。 Deno DeployEA 还提供了标准的 OIDC 发现文档: ``` https://oidc.deno.com/.well-known/openid-configuration ``` Deno DeployEA 会定期更换其签名密钥,因此应动态从 JWKS 端点获取密钥,而不要将密钥硬编码。 当前,Deno DeployEA 的签名密钥使用 `ES256` 算法。未来可能会根据安全需求、最佳实践及第三方服务支持情况进行更改。 验证令牌时,可以使用支持 OIDC 和 JWKS 的 JWT 库。在 TypeScript 中,您可以使用 [`jose`](https://jsr.io/@panva/jose) 库。 --- # 组织 > 在 Deno Deploy 中创建和管理组织的指南,包括成员、权限和组织管理。 URL: https://docs.deno.com/deploy/reference/organizations 组织是由一组用户组成的,他们共同拥有应用和域名。当注册 Deno Deploy 时,每个用户可以创建一个组织,或通过邀请加入现有组织。 所有用户必须属于某个组织才能使用 Deno Deploy,因为所有资源均归属于组织层级。 组织具有名称和 slug。名称仅对组织成员可见,并显示在 Deno Deploy 和 Deploy Classic 的组织下拉菜单中。slug 是该组织内所有应用默认域名的一部分。 :::caution 组织创建后,名称和 slug 均不可更改。 ::: 每个组织都有默认域名,用于该组织中项目的生产环境、git 分支及预览 URL。例如,slug 为 `acme-inc` 的组织,其默认域名为 `acme-inc.deno.net`。 组织可包含多个成员。目前,所有成员在组织中均拥有所有者权限,意味着他们可以邀请其他成员、创建和删除应用以及管理域名。 ## 创建组织 Deno Deploy 中的组织是在注册 Deno Deploy 账号时创建的。 如果您还没有 Deno Deploy 账号,可以访问 [Deno Deploy 仪表盘](https://console.deno.com) 并使用您的 GitHub 账号登录。在注册流程中,系统会提示您创建一个组织。 :::info 组织 slug 必须在所有 Deno Deploy 组织中唯一,且不能与任何现有的 Deno Deploy Classic 项目名称相同。 ::: ## 更新组织名称 组织名称可以在 Deno Deploy 仪表盘的组织设置页面中更新。组织名称只是一个展示用的名称,修改它不会影响任何应用或域名。它会显示在仪表盘和邀请邮件中,作为组织的显示名称。 ## 更新组织 slug 组织 slug 可以在 Deno Deploy 仪表盘的组织设置页面中更新。更改组织 slug 会更新该组织内所有应用的默认域名。例如,将 slug 从 `acme-inc` 改为 `acme-corp`,默认域名将从 `acme-inc.deno.net` 变为 `acme-corp.deno.net`。 :::warning 更改组织 slug 会立即影响使用默认域名的所有应用。任何使用旧域名的现有 URL 将停止工作。您需要更新所有使用旧域名的书签或链接。 更改组织 slug 后,由于 TLS 证书的配置,新的默认域名生效可能需要数分钟。在此期间,应用将无法通过新默认域名访问。 自定义域名不受组织 slug 更改的影响。 ::: 这也会影响 Deno Deploy 仪表盘中组织内资源的 URL。例如,应用的 URL 会从 `https://console.deno.com/acme-inc/my-app` 变为 `https://console.deno.com/acme-corp/my-app`。 ## 删除组织 目前无法通过仪表盘删除组织。如需删除组织,请[联系 Deno 支持](/deploy/support/)。 ## 邀请用户加入组织 邀请用户操作步骤: 1. 进入组织设置页面并点击“+ 邀请用户”。 2. 输入用户的 GitHub 账号用户名(例如 `ry`)。 3. 可选:输入一个电子邮件地址用于发送邀请。 4. 点击“邀请”。 如果未指定电子邮件,我们将尝试发送至用户公开 GitHub 资料中的邮箱,或我们已有的其他邮箱地址。 邀请用户后(若有邮箱地址),用户会收到含邀请链接的邮件。用户必须点击该链接并接受邀请才能加入组织。您也可以直接分享成员列表中显示的个性化邀请链接。 您可以在邀请被接受之前取消邀请,方法是在成员列表中点击被邀请用户旁的删除按钮,然后点击“保存”确认,这将使之前发送的邀请链接失效。 ## 从组织中移除用户 要移除组织成员,请在组织设置的成员列表中找到该用户,点击移除按钮,然后点击“删除”确认。 --- # Playgrounds > Write and deploy code completely from Deno Deploy, without the need for a git repository. URL: https://docs.deno.com/deploy/reference/playgrounds Playground applications enable you to create, edit, and deploy applications entirely from the Deno Deploy web dashboard, without needing to create a GitHub repository. Playgrounds contain one or more files (JavaScript, TypeScript, TSX, JSON, etc.) that you can edit directly in the playground editor. ## Creating a playground You can create playgrounds from the "Applications" page in your organization. Click the "New Playground" button to create a basic "Hello World" playground. Using the dropdown on the "New Playground" button lets you create playgrounds from other templates, such as Next.js or Hono. ## Editing a playground To edit a playground, open it from the "Applications" page in your organization. The playground editor consists of five main sections: - **Code editor**: The central area where you edit code for the currently selected file. Above the editor is a navbar showing the current file name, which you can click to edit. - **File browser**: Located on the left of the code editor, this panel shows all files in the playground. Click any file to open it in the editor. Create new files by clicking the "New" icon at the top of the file browser. Delete files using the delete button next to each file name. - **Top bar**: Located above the code editor, this contains action buttons for the playground. The "Deploy" button saves current changes and triggers a build. "Build Config" and "Env Variables" buttons open their respective configuration drawers. The left side of the top bar displays the playground URL (unless the playground hasn't been deployed yet). - **Bottom drawer**: Located beneath the code editor, this contains debugging tools including "Build Logs" that show build progress during deployment, and tabs for viewing logs and traces. - **Right drawer**: Located to the right of the code editor, this contains tools for inspecting application output. The "Preview" tab displays an iframe showing the deployed application, while "HTTP Explorer" lets you send individual HTTP requests to your deployment. The playground content automatically saves when you click the "Deploy" button or when the editor loses focus. ## Uploading files You can upload a zip file containing files and directories to the playground by dragging it into the file browser area. The contents of the zip file will be extracted into the playground, preserving the directory structure. > ⚠️ The playground editor does not support uploading individual files or > directories. ## Using the HTTP explorer The HTTP Explorer tab in the playground allows you to make arbitrary HTTP requests to any URL served by the playground. This is useful for testing APIs or other services that do not serve a web page. To use the HTTP Explorer, enter the path and query parameters for the request you want to make, select the HTTP method (GET, POST, etc.), and click on the button labeled with the selected method. Additional request headers can be added by clicking the "Set Headers" button. After the response has been made, the HTTP Explorer will display the response status, headers, and body. To view the trace for the request, click on the "Trace" button in the response section. This will open the request trace for the request in a drawer on top of the playground editor. From there you can also view any `console.log` output that was captured during the request. ## Renaming a playground You can rename a playground by editing the playground slug on the playground settings page. This will update the default domain names associated with the playground since they are based on the playground slug. The new slug must be unique within the organization (i.e. must not be in use by another app or playground in the same organization). :::info Any previous `deno.net` URLs pointing to the playground will no longer work after renaming. Custom domains will continue to work, as they are not tied to the playground slug. ::: ## Deleting a playground Playgrounds can be deleted from the playground settings page. This will remove the playground and all its revisions from the organization. All existing deployments will immediately stop serving traffic, and all custom domain associations will be removed. The playground and its revisions will no longer be accessible after deletion. Deleted playgrounds cannot be restored through the Deno Deploy UI. :::info Deleted a playground by mistake? Contact Deno support within 30 days to restore it. ::: ## Limitations > ⚠️ Playgrounds cannot currently be transferred to another organization. --- # 运行时 > 关于 Deno Deploy 早期访问运行时环境的详细信息,包括应用程序生命周期、启动、关闭和冷启动优化. URL: https://docs.deno.com/deploy/reference/runtime 您正在查看 Deno DeployEA 的文档。 查找 Deploy Classic 的文档?[点击这里查看](/deploy/)。 ::: 在 Deno DeployEA 中,所有应用程序都使用标准的 Deno 运行时在安全、隔离的 Linux 环境中执行。 Deno DeployEA 使用的 Deno 运行时是标准的 Deno 运行时,完全支持 Deno CLI 的所有功能,包括 JSR 和 NPM 依赖、读写文件系统、发起网络请求、生成子进程,以及加载 FFI 和 node 原生插件。 Deno 运行时以 `--allow-all` 权限运行。 无法向 Deno 运行时传递自定义标志。 ## 运行时环境 运行时环境是基于 Linux 的环境,运行在 x64 或 ARM64 架构上。运行时环境中可用的具体工具集可能会发生变化,因此无法依赖其稳定性。 目前 Deno DeployEA 运行在 Deno 2.4.0 上。 ## 生命周期 Deno DeployEA 在无服务器环境中运行应用程序。 这意味着应用程序并不总是运行,只有在收到请求时才会启动。当长时间未收到任何流量时,应用程序将被停止。 应用程序可以随时启动和停止。它们应该快速启动,以便无延迟地响应传入请求。 同一个应用程序的多个实例可以同时运行。例如,一个实例可能运行在美国,另一个在欧洲。每个实例彼此完全隔离,不共享 CPU、内存或磁盘资源。必要时同一地区也可以启动多个实例,比如处理高流量或基础设施更新。 ### 启动 当系统决定启动一个应用程序时,会为该应用程序预置一个新的沙箱环境。该环境与其他所有应用程序隔离。 然后使用配置的入口点启动应用程序,并等待 HTTP 服务器启动。如果应用程序在 HTTP 服务器启动之前崩溃,触发启动的请求将失败并返回 502 Bad Gateway 错误。 应用程序启动后,传入请求将被路由到该应用程序,并将响应发送回客户端。 ### 关闭 应用程序会保持运行状态,直到一段时间内没有接收到新的传入请求或未发送响应(包括响应体字节)。具体超时时间介于 5 秒至 10 分钟之间。积极传输数据的 WebSocket 连接(包括 ping/pong 帧)也会保持应用程序存活。 当系统决定停止应用程序时,会向应用程序发送 `SIGINT` 信号,作为关闭的触发。此后,应用程序有 5 秒的时间优雅关闭,否则将被强制以 `SIGKILL` 信号终止。 ### 驱逐 有时即使应用程序仍在接收流量,其 isolate 也可能被关闭。以下是一些可能的情况: - 应用程序为了应对负载被扩展,但负载已减少回单实例可处理的水平。 - 执行该实例的底层服务器资源紧张,无法继续运行该应用实例。 - 底层基础设施正在更新或发生故障。 当系统决定驱逐一个应用程序时,会尽早尝试将流量从被驱逐的实例分流。有时这意味着请求会等待一个新实例启动,即便现有实例仍在运行。 当应用只处理快速完成的请求时,驱逐一般不易察觉。对于处理长时间请求或 WebSocket 的应用,驱逐可能更明显,因为可能需要在处理请求时驱逐应用。系统会尽力避免这种情况,但并不可避免。 在流量被重定向离开旧实例后,系统发送 `SIGINT` 信号触发优雅关闭。应用程序应快速完成剩余请求,并关闭 websockets 及其他长连接。发起长时间请求的客户端应准备好处理中断,并在断开时重新连接。 在发送 `SIGINT` 信号 5 秒后,如果旧实例尚未优雅关闭,将被强制以 `SIGKILL` 信号终止。 ## 冷启动 - Linux 微型虚拟机和 Deno 运行时已预先配置,以确保在启动应用程序时无需从头创建。 Deno DeployEA 使用多项优化来实现快速冷启动: - 沙箱和 Deno 运行时预先准备,确保启动应用时无需从零创建。 - 应用在客户端发送首个 TCP 包以建立 TLS 连接时立即启动。对于启动迅速的应用,依据网络往返延迟,应用可能在客户端发送 HTTP 请求之前已运行。 - 文件系统访问针对常用启动文件进行了优化。Deno DeployEA 在构建步骤的预热阶段分析文件访问模式,并优化文件系统以加快访问速度。 当冷启动缓慢时,会影响用户体验。为优化应用快速启动: 1. 减少应用依赖。 2. 使用动态 `import()` 延迟加载不常访问的代码和依赖。 3. 启动时尽量减少 I/O 操作,特别是顶层 `await` 和网络请求。 如果您的应用启动缓慢,请[联系 Deno 支持](../support) 获取帮助调查问题。 --- # 时间线 > 了解 Deno Deploy 早期访问中的部署时间线,包括生产和开发上下文、活动修订、回滚和时间线锁定。 URL: https://docs.deno.com/deploy/reference/timelines :::info 您正在查看 Deno DeployEA 的文档。正在寻找经典 Deploy 文档?[点此查看](/deploy/)。 ::: 时间线是应用程序某个分支历史的表示。每个时间线由一组修订组成,这些修订是时间线上的各个项目。某个修订(通常是最近的一个)为“活动”修订,即当前正在处理流量的修订。活动修订接收分配给该时间线的所有 URL 的流量。 每个时间线都与一个[上下文](./env-vars-and-contexts.md)相关联,该上下文决定在该时间线上运行的代码可用哪些环境变量。 默认情况下,每个应用程序会设置多个时间线: - **生产**:生产时间线包含默认 git 分支的所有修订。该时间线负责处理生产流量。此时间线关联至 `https://..deno.net`,以及映射至该应用的任何自定义域名。它使用生产环境上下文。 - **Git 分支 / ``**:每个 Git 分支都有自己的时间线。该时间线包含该分支的所有修订。此时间线关联至 `https://--..deno.net`。它使用开发环境上下文。 > 每个修订还有一个独立的时间线,该时间线仅包含该修订。这个时间线承载该修订的预览 URL。该时间线关联至 `https://-..deno.net`,使用开发环境上下文。 > > 预览时间线在 UI 的时间线页面中不可见。您可以在该修订的构建页面查看其预览 URL。 您可以在修订的构建页面查看该修订关联的时间线。也可以在时间线页面查看关联某个时间线的修订。 ## 活动修订 每个时间线都有一个活动修订。活动修订是当前为该时间线提供流量的修订。您可以在时间线页面查看时间线的活动修订。 通常,活动修订是该时间线中最近构建的修订。但也可以手动锁定其他修订作为活动修订。这使得能够回滚和锁定时间线成为可能: ### 回滚 回滚是将活动修订恢复到之前某个修订的过程,通常是因为较新的修订存在某种错误或问题。通过回滚到已知的良好修订,可以在无需通过 Git 部署新代码、等待构建完成的情况下恢复应用的正常状态。 有关如何回滚时间线的更多信息,请参阅下面的“更改活动修订”。 ### 时间线锁定 时间线锁定是将时间线锁定到特定修订,以确保新构建不会自动成为活动修订的过程。如果您处于功能冻结阶段,例如大型活动期间,想要降低风险而不允许新构建部署,这非常有用。当时间线锁定到特定修订时,您仍可通过 Git 推送创建新构建,但这些构建不会自动成为该时间线的活动修订。 有关如何锁定时间线至特定修订的更多信息,请参阅下面的“更改活动修订”。 ### 更改活动修订 在时间线页面,您可以将该时间线上的任意修订锁定为活动修订。这样会锁定时间线至该修订,且新构建将不再自动成为该时间线的活动修订。随后,您可以解锁该修订,恢复最新修订为活动修订的默认行为,或锁定其他修订为活动修订。 --- # 隧道 > 了解 Deno Deploy 的本地隧道功能,该功能允许您从互联网安全地访问本地开发服务器。 URL: https://docs.deno.com/deploy/reference/tunnel Deno Deploy 的隧道功能允许您安全地将本地开发服务器暴露到互联网。这对于测试 webhook、与协作者共享您的工作,或从远程位置访问本地服务器尤为有用。 除了提供对本地服务器的安全访问外,Deno Deploy 的隧道还可以: - 将 “本地” 上下文中的环境变量从您的 Deno Deploy 项目拉取到本地 Deno 进程。 - 将本地 Deno 进程的 Open Telemetry 跟踪、指标和日志推送到您的 Deno Deploy 应用,您可以在 Deno Deploy 控制面板中查看它们。 - 自动连接分配给您的 Deno Deploy 应用的本地开发数据库。 ## 入门 要开始使用隧道功能,您需要在本地机器上安装 Deno。然后,运行本地 Deno 应用时,可通过 `--tunnel` 标志启用隧道,无论是使用 `deno task` 还是 `deno run`。例如: ```sh deno run --tunnel -A main.ts ``` 首次运行此命令时,系统会提示您进行 Deno Deploy 认证,并选择要连接隧道的 Deno Deploy 应用。认证成功后,将建立安全隧道,并为您提供一个用于转发流量到本地服务器的公共 URL。 您也可以为 `deno.json` 或 `package.json` 文件中定义的 `deno task` 命令指定 `--tunnel` 参数: ```json { "tasks": { "dev": "astro dev" } } ``` 然后使用以下命令运行该任务: ```bash deno task --tunnel dev ``` ## 使用隧道 隧道建立后,任何对公共 URL 的请求都会转发到您的本地开发服务器。您可以使用此 URL 测试 webhook、与他人共享工作,或从远程位置访问本地服务器。 ## 停止隧道 要停止隧道,只需终止运行您的应用程序的 Deno 进程。这将关闭安全连接,并停止将流量转发到本地服务器。 ## 查看开启的隧道 Deno Deploy 应用面板中的 “隧道” 选项卡显示所有连接到您的应用的活动隧道。在此选项卡中,您可以查看每个隧道的详细信息,包括公共 URL、本地转发地址及建立时间。 ## 环境变量 使用隧道功能时,您的 Deno Deploy 应用中的 “本地” 上下文环境变量会提供给您本地的 Deno 进程。这允许您在本地使用与 Deno Deploy 应用相同的配置。 您可以在应用设置的 “环境变量” 选项卡中查看和管理您的 Deno Deploy 应用的环境变量。更多信息请参见[添加、编辑和移除环境变量文档](/deploy/reference/env_vars_and_contexts/#adding%2C-editing-and-removing-environment-variables)。 ## 查看跟踪和日志 使用隧道时,本地 Deno 进程的 Open Telemetry 跟踪、指标和日志会被推送到您的 Deno Deploy 应用。您可以在 Deno Deploy 应用面板的 “可观测性” 选项卡中查看这些跟踪和日志。 您可以通过在搜索栏中搜索 `context:local` 来过滤并仅查看本地进程的跟踪和日志。 --- # 安全与负责任的披露 > 如何报告 Deno Deploy 中的安全漏洞。 URL: https://docs.deno.com/deploy/security 我们将系统的安全性以及这些系统控制的所有数据视为重中之重。无论我们在系统安全上投入多大精力,仍然有可能存在安全漏洞。我们非常感谢那些出于善意、伦理的安全研究人员对系统安全进行的调查工作。如果您发现了一个漏洞,无论多小,我们都希望您能告知我们,以便我们能够尽快采取适当措施来解决此问题。此页面概述了我们与安全研究社区合作以解决系统安全问题的方法。 ## 报告漏洞 请将您的发现通过电子邮件发送至 security@deno.com。我们努力尽快解决所有问题,并非常乐意在问题解决后积极参与撰写报告的发布。 ## 请遵循以下事项: - 不要利用您发现的漏洞或问题。例如,仅下载必要的数据以演示漏洞 - 不要下载更多。也不要删除、修改或查看他人的数据。 - 在问题解决之前,请不要发布或透露该问题。 - 不要进行物理安全攻击、社会工程、分布式拒绝服务、垃圾邮件或第三方应用程序的攻击。 - 请提供足够的信息以重现问题,以便我们能尽快解决。通常,受影响系统的IP地址或URL以及漏洞的描述就足够了,但复杂的漏洞可能需要进一步解释。 ## 我们的承诺 - 如果您按照此政策行事,我们将不会对您的报告采取法律行动。 - 我们将对您的报告严格保密,并且在没有您许可的情况下不会将您的个人信息传递给第三方。 --- # 支持与反馈 URL: https://docs.deno.com/deploy/support/ 如果您对 Deno Deploy 有任何问题或反馈,请通过 [Deno Discord](https://discord.gg/deno) 的 `#deploy` 频道联系我们,或发送邮件至 support@deno.com。 我们正在积极改进这个平台,非常期待听到您的想法! ## 支持工单 当您发送邮件至 support@deno.com 时,会收到一封自动回复邮件,其中包含支持工单编号和一个领取工单的链接。 您可以通过该链接将工单关联到您的 Deno Deploy 账户,方便您使用 [工单仪表盘](https://console.deno.com/tickets) 来跟踪支持请求的状态。这也能让我们的支持团队知道您是通过哪个账户联系我们,从而提供更好的帮助。 您可以直接在工单仪表盘中查看工单状态更新并回复支持团队的任何问题。当工单状态有变化时,您还会收到电子邮件通知,您可以通过回复这些邮件提供更多信息。 如果您没有收到自动回复邮件,请检查您的垃圾邮件或广告邮件文件夹。 --- # 条款和条件 > Deno 条款和条件 URL: https://docs.deno.com/deploy/terms_and_conditions **DENO 条款和条件** 2024年9月9日 本条款和条件(“条款”)是您与 Deno Land Inc.(“Deno”,“我们”,“我们公司”或“我们的”)之间的法律协议。它们规定了您可以访问和使用(i)我们的网站 [https://deno.com](https://deno.com) (“网站”);(ii)任何链接到这些条款的网站、应用程序或其他数字属性;以及(iii)我们通过以下网站在我们的专有平台(“平台”)上向您提供的产品和服务(“Deno 产品”): - Deno Deploy ([https://deno.com/deploy](https://deno.com/deploy)) - Deno Deploy Classic ([https://deno.com/deploy/classic/](https://deno.com/deploy/classic)) - Deno Subhosting ([https://deno.com/subhosting](https://deno.com/subhosting)) 通过访问或使用网站或任何链接到这些条款的其他数字属性,您可以了解 Deno 及其技术平台,注册客户还可以访问 Deno 产品(统称为“服务”)。 请仔细阅读这些条款。通过访问和/或使用服务,您确认您已阅读、理解并同意受到这些条款、数据处理附录(“DPA”)和我们的隐私政策(“隐私政策”)条款和条件的法律约束,这些政策通过引用并入本条款并构成协议的一部分(统称为“协议”)。如果您不同意本协议中的任何条款,请不要使用服务。 如果您代表公司或其他法律实体接受或同意本协议,您声明并保证您有权利使该公司或其他法律实体受本协议的约束,在这种情况下,“您”和“您的”将指代并适用于该公司或其他法律实体。 我们保留在自行决定的情况下,随时修改、停止或终止任何服务的可用性,或修改本协议,且不需提前通知。我们鼓励您在每次访问或使用服务时检查这些条款及上面的“最后更新”日期。在我们发布对这些条款的修改后,继续访问或使用服务,即表示您同意受修改后协议的约束。如果修改后的协议您无法接受,您唯有停止访问或使用服务。 Deno 还提供收费的产品和服务(包括不时的免费试用),这些产品和服务可能提供对某些数据产品和/或服务的访问(“付费产品”)。我们根据您在购买时做出的适用付费产品的商业协议(每个“商业协议”)提供对我们的付费产品的访问和使用。如果本条款与您购买的付费产品的适用商业协议的条款和条件之间存在冲突,以商业协议的条款和条件为准。 未在这些条款中定义的专有名词应具有我们隐私政策中规定的含义。 **以下标题为“具有约束力的仲裁”和“集体诉讼豁免”的部分包含具有约束力的仲裁协议和集体诉讼豁免。它们会影响您的法律权利。请仔细阅读。** 1. **服务的描述;访问和使用服务的权利** **Deno Deploy** 和 **Deno Subhosting** 是全球分布的无服务器 JavaScript 应用程序平台。您的 JavaScript、TypeScript 和 WebAssembly 代码在地理位置接近用户的管理服务器上运行,提供低延迟和更快的响应时间。部署和子托管应用程序在快速、轻便的 V8 隔离环境中运行,而不是在虚拟机上,由 Deno 运行时提供支持。 在本协议的条款和条件的限制下,Deno 在本协议期间授予您有限的、非独占的、不可转让的、不可再许可的、可撤回的权利,仅为您的内部商业目的访问和使用服务。 Deno 保留在任何时候,且无须通知或对您承担责任的权利: 1. 阻止和禁用任何因某种原因使平台不稳定的部署; 2. 更改服务运行的区域; 3. 更改服务支持的功能; 4. 修改或停止其他与服务相关的任何功能、功能或内容的可用性。 您同意,我们不对您或任何第三方因服务或其任何部分的修改、暂停或中止而承担责任。您可以随时停止使用服务。 2. **账户凭证** 为了使用 Deno 产品,您必须是“授权用户”。要成为授权用户,您需要在平台上创建一个账户,并通过 GitHub 进行身份验证(统称为“账户凭证”)。在创建账户时,每个授权用户必须提供真实、准确、当前和完整的信息。每个账户凭证只能由一个授权用户使用。每个授权用户对其账户凭证的保密性和使用负责,包括与其账户凭证相关联的所有活动。授权用户必须及时通知我们需要停用任何账户凭证。Deno 没有义务接受任何人作为授权用户,并可全权接受或拒绝任何注册。我们有权在任何时候出于任何原因禁用任何账户凭证,包括在我们自行决定的情况下认为您未能遵守这些条款的情况下。 3. **个人信息的使用** 您使用服务可能涉及将某些个人信息传输给我们。我们关于个人信息收集和使用的政策根据我们的隐私政策进行,这里通过引用予以完全纳入。 4. **知识产权** 服务可能包含材料,例如软件、文本、图形、图像、声音录音、视听作品以及由 Deno 或代表 Deno 提供的其他材料(统称为“内容”)。内容可能属于我们或第三方。内容受美国和外国法律的保护。未经授权使用内容可能违反版权、商标和其他法律。您对内容没有任何权利,且除非本协议许可,您不得使用内容。未经我们事先书面同意,您不得以任何其他方式使用内容。您必须在您制作的任何内容副本上保留原始内容中包含的所有版权和其他专有声明。您不得销售、转让、分配、许可、再许可或修改内容,或以任何方式复制、展示、公开执行、制作衍生版本、分发或以其他方式使用内容,用于任何公共或商业目的。将内容用于任何其他网站或在网络计算机环境中发布的任何目的都是明确禁止的。 如果您违反本协议的任何部分,您访问和/或使用内容及服务的权限将自动终止,并且您必须立即销毁您制作的所有内容副本。 Deno 的商标、服务标志和徽标(“Deno 商标”)在服务中使用和展示,属于 Deno 的注册或未注册商标或服务标志。其他公司、产品和服务名称可能属于其他人的商标或服务标志(“第三方商标”,与 Deno 商标统称为“商标”)。服务中的任何内容不应被解读为隐含、禁止或以其他方式授予任何使用商标的许可或权利,除非我们为每种使用事先明确书面许可。除非我们事先书面批准建立链接的目的,否则使用商标作为从任何网站的链接的一部分是被禁止的。由 Deno 商标的使用所产生的所有商誉均为我们的利益。 服务的元素受到商业外观、商标、不正当竞争和其他州和联邦法律的保护,任何人不得以任何方式(包括但不限于使用框架或镜像的方式)全部或部分复制或模仿。内容的任何部分不得在没有我们的明确书面同意下进行转播。 5. **用户数据;使用数据;汇总数据** 就本协议而言,“用户数据”是指(i)我们通过连接到授权用户的业务系统所获取的任何数据和信息,包括但不限于事件日志;以及(ii)授权用户通过服务提交的任何数据和信息;而“使用数据”是指 Deno 收集的关于服务性能和您使用服务的匿名分析数据,包括但不限于您访问服务的日期和时间、访问的服务部分、访问这些页面的频率和次数、在特定时间段内使用服务的次数以及其他使用和性能数据。 在双方之间,授权用户拥有对用户数据的所有权、所有权利、标题和利益,包括对其所做的所有修改、改进、适应、增强或翻译以及其中的所有知识产权。授权用户特此授予 Deno 一项非独占的、全球性的、完全支付的、免版税的权利和许可,并有权授予再许可,以便在本协议的有效期内,复制、执行、使用、存储、归档、修改、执行、展示和分发用户数据;(i)在本协议的有效期内,为 Deno 在本协议下的义务服务;以及(ii)为 Deno 的内部商业目的,包括使用这些数据分析、更新和改进服务及 Deno 的分析能力以及进行基准测试。 尽管本协议另有规定,我们可能以匿名和汇总的形式(“汇总数据”)使用,并可能允许我们的第三方服务提供商访问和使用用户数据以及我们可能收集的任何使用数据,用于操作、维护、管理和改进我们的产品和服务,包括服务。汇总数据不会识别授权用户或任何个人。您特此同意,我们可以收集、使用、发布、传播、转让和以其他方式利用此类汇总数据。 6. **费用** Deno 提供并且授权用户可以为服务购买月度或年度订阅(“订阅”),费用在我们网站上规定(“订阅费”)。Deno 可以自行决定在任何时候增加新费用和收费或修订费用和收费。订阅的付款应在购买时立即支付。通过购买,您同意通过我们的第三方支付处理器(“第三方支付处理器”)向 Deno 支付所有当时适用的订阅费用。您向第三方支付处理器提供的任何信息将根据该第三方支付处理器的隐私政策和使用条款进行处理。您必须为您的账户提供最新、完整和准确的信息,并及时更新所有信息以保持该账户信息的最新、完整和准确(例如,账单地址、信用卡号码或信用卡到期日期的变化)。此外,如果付款方式被取消(例如,因丢失或失窃)或您意识到可能的安全漏洞(例如,未经授权披露或使用您的用户名或密码),您必须尽快通知我们。有关此类信息的更改可以通过您的账户进行。 通过购买订阅,您承认您的订阅有初始和重复的付款费用,按当时的订阅费率收取,您同意 Deno 可以在未再征得您进一步授权的情况下,向您选择的付款方式提起每月收费,直到您通知我们希望取消订阅或更改付款方式。您还接受在取消之前的所有定期费用的责任,包括在您的付款卡过期后,Deno 根据适用情况处理的任何费用。 您可以通过向我们发送电子邮件至 [support@deno.com](mailto:support@deno.com) 更改或终止您的订阅。如果您终止您的订阅,您可以在当前计费周期结束之前使用您的订阅,并且订阅将在该期限到期后不再续订。Deno 不会退还任何预付款项。Deno 可以根据这些条款立即终止或暂停您的订阅,无论有无理由,包括未按时付款的情况下。如果我们终止或暂停您的订阅,您使用任何与订阅相关的软件或内容的权利也将终止或暂停(如适用)。 Deno 有时可能会提供服务的免费试用。Deno 保留自行决定随时停止提供服务的免费试用的权利,且不对您承担任何责任。 7. **社区规范** 通过访问和/或使用服务,您特此同意遵守以下指南: - 您不会将服务用于任何非法目的; - 您不会访问或使用服务收集任何市场研究以支持竞争业务; - 您不会上传、发布、电子邮件、传输或以其他方式提供任何侵犯任何人或实体的版权、商标、公开权或其他专有权利的内容; - 您不会冒充任何人或实体或虚假陈述或以其他方式歪曲与任何人或实体的关系; - 您不会反编译、逆向工程、拆卸或以其他方式试图识别服务中任何软件或其他产品或过程的源代码或接口协议; - 您不会删除或修改任何放置在服务上的专有标记或限制性说明; - 您不会以任何可适用的法律违反情况下使用服务或其任何部分,以构建竞争产品或服务,或用于本条款中未明确允许的任何目的; - 您不会遮盖、遮挡、屏蔽或以任何方式干扰服务中的任何广告和/或安全功能; - 您不会规避、删除、修改、禁用、降级或阻碍服务中的任何保护措施; - 您不会引入、发布或上传任何有害代码。这里所称的“有害代码”是指故意设计用来干扰、修改、访问、删除、损坏、禁用、伤害或以任何方式阻碍服务,或任何其他相关软件、固件、硬件、计算机系统或网络的计算机代码、程序或编程设备(包括但不限于“特洛伊木马”、“病毒”、“蠕虫”、“定时炸弹”、“定时锁”、“设备”、“陷阱”、“访问代码”或“死掉”或“陷阱门”设备)或任何其他有害、恶意或隐藏的程序、例程或机制,导致服务停止运行或损坏或破坏数据、存储介质、程序、设备或通信,或以其他方式干扰服务的操作; - 您不会采取任何行动,施加或可能施加(由我们自行决定)不合理或不成比例的负担在我们的技术基础设施上; - 您不会干扰或尝试中断服务的正常运行,使用任何病毒、设备、信息收集或传输机制、软件或例程,或通过黑客、密码或数据挖掘或其他方式访问或尝试获取与服务有关的任何数据、文件或密码。 虽然我们没有义务监控对服务的访问或使用,但我们有权这样做以便安全运行,确保遵守这些条款,并遵守适用法律或其他法律要求。我们有权调查这些条款的违规情况或影响服务的行为。我们还可能咨询和配合执法机构,对违反法律的用户进行起诉。 如果您发现任何违反我们用户指南的内容,请告知我们,我们将进行审查。 8. **链接和引用内容** Deno 不反对第三方服务上指向我们主页的链接,只要它们处于适当的背景中。不过,“框架”或“镜像”服务或内容在没有 Deno 的事先书面同意的情况下是被禁止的。 9. **限制** 服务仅适用于年龄在18岁或以上的个人。如果您未满18岁,请不要访问和/或使用服务。通过签订本协议,您声明并保证您已满18岁。 10. **反馈** 我们欢迎并鼓励您提供对服务和我们服务的反馈、评论和改进建议(“反馈”)。尽管我们鼓励您通过电子邮件与我们联系,但我们不希望您向我们发送包含机密信息的任何内容。关于您提供的任何反馈,我们将可以自由使用和披露其中包含的任何想法、概念、知识、技术或其他材料,出于任何目的,包括但不限于处理、开发、生产和市场营销包含该信息的产品和服务,而无须向您支付补偿或给予署名。 11. **无保证;责任限制** 服务和内容是在“按现状”和“按可用性”基础上提供的,Deno 和 Deno 的供应商对其或与本协议相关的内容不作任何保证,并且 Deno 在此明确放弃所有明示、暗示或法定的保证,包括但不限于任何不侵权、适销性、特定用途适用性、可用性、无错误或无中断操作的保证,以及任何基于交易过程、执行过程或商业使用的保证。在适用法律不允许 Deno 和 Deno 的供应商放弃任何暗示保证的情况下,该保证的范围和持续时间将为适用法律允许的最低限度。 在不限制前述内容的情况下,我们不保证、也不作任何声明,亦不对以下事项负责:(A) 服务的正确性、准确性、可靠性、完整性或最新性;或(B) 您依赖服务或通过服务提供的内容或提示所取得的任何结果、采取的任何行动。您基于服务或通过服务提供的内容或提示所做的任何决定、行动或遗漏均由您自行承担风险。服务及通过服务提供的内容和提示仅作为方便,并不取代对其准确性、完整性和正确性的审查。 就任何保证、合同或普通法侵权索赔而言:(I) 我们不对因使用或无法访问和使用服务而造成的任何偶然或后果性损害、利润损失或因数据丢失或业务中断而造成的损失负责,即使我们已被告知可能会发生此类损害;以及(II) 您因使用服务所遭受的任何直接损害,应限于您在造成索赔时立即之前的十二(12)个月内支付给我们的费用或一百美元($100)。 12. **外部网站** 服务可能包含指向第三方网站(“外部网站”)的链接。这些链接仅为您的方便提供,并不表示我们认可该外部网站的内容。此类外部网站的内容由其他人开发和提供。如果您对这些链接或该等外部网站上的任何内容有任何疑虑,应联系该外部网站的管理员或网站负责人。我们对任何链接的外部网站的内容不承担责任,也不就该等外部网站上的内容或材料的准确性作出任何声明。您在从所有网站下载文件时应采取预防措施,以保护您的计算机免受病毒和其他破坏性程序的侵害。如果您决定访问链接的外部网站,风险由您自行承担。 13. **声明和保证** 您声明并保证您拥有:(i)提供给我们或授予我们访问和使用用户数据所需的所有权利和权限;以及(ii)根据所有适用法律和法规,已获得关于所提供用户数据的所有必要和适当的同意、权限和授权。 14. **赔偿** 您将赔偿、辩护并使 Deno、其关联公司及我们的及其各自的股东、成员、官员、董事、员工、代理人和代表(统称为“Deno 赔偿方”)免受与您(i)违反本协议,包括但不限于您声明和保证的任何违反;(ii)滥用服务和/或内容;(iii)疏忽、重大疏忽、故意不当行为、欺诈、虚假陈述或违法行为;或(iv)违反任何第三方权利(包括但不限于任何版权、商标、财产权或隐私权)相关的任何损害、责任、损失、费用和开支,包括合理的律师费(统称为“损失”)。一旦发生索赔,您应及时通知我们;在此过程中,为您承担合理的合作费用,并在您赔偿的控制下,进行辩护和和解谈判。 15. **遵守适用法律** 服务的基础是在美国。我们不对根据国家法律的冲突条款主张服务可以在美国以外的地方查看或适用。如果您从美国以外访问服务,风险由您自行承担。无论是在美国境内还是境外,您均需自行负责确保遵守特定管辖区的法律法规。 16. **期限;终止** 本条款以及您访问和使用服务的权利将在您接受这些条款时开始,并持续至您订阅和/或使用服务的期限。 我们保留在自行决定的情况下,随时限制、暂停或终止这些条款以及您对服务的部分或全部访问的权利,无需提前通知或承担责任。我们保留在任何时候无须提前通知或承担责任,变更、暂停或终止服务的全部或任何部分的权利。“服务的描述;访问和使用服务的权利”,“个人信息使用”,“知识产权”,“反馈”,“无保证;责任限制”,“赔偿”,“遵守适用法律”,“期限;终止”,“具有约束力的仲裁”,“集体诉讼豁免”,“平等救济”,“适用法律;专属论坛”和“其他条款”部分应在这些条款终止后继续有效。 17. **具有约束力的仲裁** 如果因本协议和/或服务所引起的任何争议(“争议”)应通过由《联邦仲裁法》(“FAA”)管辖的具有约束力的仲裁最终、独家解决。任何一方均无权在法庭上诉讼该索赔或要求陪审团审判,除非任何一方可根据当地小额索赔法庭规则在小额索赔法庭提起索赔。仲裁与法庭不同,发现和上诉权利也可能在仲裁中受到限制。所有争议将由双方共同选择的中立仲裁人解决,其决定为最后决定,除非根据FAA的有限上诉权。仲裁应由 JAMS 根据当时适用的综合仲裁规则和程序进行,并根据这些规则中的加快程序进行,或在适用情况下,根据 JAMS 精简仲裁规则和程序进行。所有适用的 JAMS 规则和程序可在 JAMS 网站 [www.jamsadr.com](http://www.jamsadr.com) 上获得。每一方负责根据 JAMS 规则支付任何 JAMS 费用、管理费和仲裁人费用。仲裁人裁定的判决可在任何有管辖权的法院进行登记。此条款不妨碍双方在适当管辖权的法院寻求临时救济以支持仲裁。仲裁可以亲自进行,也可以通过提交文件、电话或在线方式进行。如果现场进行仲裁,仲裁应在您居住的美国县进行。双方可以在法院仲裁请求、暂停仲裁程序或确认、修改、撤销或对仲裁人做出的裁决进行判决。在仲裁开始后,双方应善意合作,自愿和非正式地交换与争议相关的所有非特权文件和其他信息(包括电子存储信息)。如下面第18节所述,本协议中的任何内容均不得妨碍我们向任何有管辖权的法院寻求禁令救济,以保护我们的专有权益。 18. **集体诉讼豁免** 您同意任何仲裁或程序应限于您与我们之间的争议。在法律允许的最大范围内,(i)不应将任何仲裁或程序与任何其他诉讼联合;(ii)没有权利或权限以集体诉讼的方式仲裁或解决任何争议或利用集体诉讼程序;(iii)没有权利或权限代表公众或任何其他人提起的所谓代理诉讼。您同意仅以个人身份对我们提起索赔,而不是作为任何假定的集体或代表性诉讼中的原告或集体成员。 19. **平等救济** 您承认并同意,如果您违反或威胁违反我们的知识产权和机密及专有信息,我们将遭受无法弥补的损害,因此有权寻求禁令救济以执行本协议。我们可不放弃其他在本协议下的救济,向任何有管辖权的法院请求任何临时、平等、临时或禁令救济,以保护我们的权利和财产,等待上述仲裁结果。您特此不可撤销地、自愿地同意纽约州联邦和州法院的个人和主题管辖权,以便我们提起任何行动。 20. **适用法律;专属论坛** 本协议及与之相关的任何行为应受纽约州法律管辖,而不考虑其法律冲突条款。各方特此同意将纽约州的州和联邦法院作为所有直接或间接因本协议引起或与之相关的诉讼、行动或程序的专属管辖法院,并放弃对此类法院的任何和所有异议,包括但不限于基于不当地点或不便论坛的异议,并各方特此不可撤销地向以上法院的专属管辖权提交任何因本协议引起或与之相关的诉讼、行动或程序。 21. **其他条款** 尽管这些条款中有相反的规定,每一方可以在本协议有效期内出于市场营销和推广目的使用对方名称和/或标志,包括但不限于在 Deno 网站或其他地方将授权用户识别为 Deno 的客户。未经 Deno 书面同意,您不得将本条款下的任何权利、义务或责任转让给任何人或实体,无论是全部还是部分。我们未采取行动或执行本协议的任何条款不应被解释为对该条款或本协议中任何其他条款的放弃。任何放弃对我们无效,除非以书面形式作出,并且不应被解释为在其他或后续实例中放弃。除非我们和您以书面形式明示同意,该协议构成您和我们在主题方面的全部协议,并取代双方在主题方面的所有先前或同时存在的协议,无论是书面还是口头。各章节标题仅为方便而提供,不应被赋予任何法律意义。本协议将惠及我们的继承者、受让人、许可人和再许可人。 **版权所有 2025 Deno Land Inc. 保留所有权利。** --- # Deno Deploy 使用指南 > Deno Deploy 的重要限制、服务等级预期及使用条款。 URL: https://docs.deno.com/deploy/usage Deno Deploy 提供慷慨的免费额度,允许您以极低成本在边缘运行应用程序。然而,为了确保所有用户都能享有公平且可靠的服务,我们制定了一些使用指南和限制。有关定价详情,请访问 [Deno Deploy 定价页面](https://deno.com/deploy/pricing)。 如果您的应用遇到意外的流量激增,我们希望保护您免受意外高额账单的影响。同时,如果您愿意,我们也不希望在达到限制时自动暂停流量而影响您的成功。 在 2025 年 10 月 1 日之前,您可以直接从您的组织仪表板配置警报阈值和硬性支出上限。这些控制功能,结合及时的配额使用通知,将帮助您避免意外的账单费用。 您可以在控制台的 [账单](https://console.deno.com/go/billing) 页面查看和管理这些设置。 Deno 公司现已使用 Deno Deploy 托管我们自己的官网,并投入大量精力确保服务可靠性。 - [Deno Deploy 可接受使用政策](/deploy/acceptable_use_policy/) - [Deno Deploy 条款和条件](/deploy/terms_and_conditions/) Deno 保留终止任何违反条款和条件的用户、组织或应用的权利。 --- # 访问字符串和二进制输出 > 了解如何访问沙箱中命令的字符串和二进制输出。 URL: https://docs.deno.com/examples/sandbox_access_output/ 你可以访问沙箱中命令的字符串和二进制输出。此示例展示了如何捕获命令输出,无论你的工作流程需要哪种形式: ```ts import { Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create(); // 获取字符串和二进制数据 const result = await sandbox.sh`cat binary-file.png` .stdout("piped"); console.log("二进制长度:", result.stdout!.length); console.log("文本长度:", result.stdoutText!.length); // 使用二进制数据 import fs from "node:fs"; fs.writeFileSync("output.png", result.stdout!); ``` 通过管道传输 stdout,你可以从同一个命令获取原始的二进制缓冲区和解码后的文本视图,因此可以处理混合二进制和文本数据的文件,而不必重新运行命令。 一旦获得二进制结果,你可以将其直接传递给诸如 `fs.writeFileSync` 之类的 API,以保存沙箱内生成的文件,使数据在沙箱和主机环境之间的传输变得轻松。 当沙箱命令生成你需要以编程方式处理的文件(如图像、归档等),而不仅仅是在控制台打印时,这非常有用。 --- # 命令取消 > 了解如何在沙盒中取消命令。 URL: https://docs.deno.com/examples/sandbox_command_cancellation/ 你可以使用 `KillController` 类在沙箱中取消命令。 ```ts import { KillController, Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create(); // 启动一个长时间运行的命令 const controller = new KillController(); const cmd = sandbox.sh`sleep 30`.signal(controller.signal); const promise = cmd.text(); // 2 秒后取消 setTimeout(() => { controller.kill(); // 终止进程 }, 2000); try { await promise; } catch (error) { console.log("命令已被取消:", error); } ``` `KillController` 允许你向任何沙盒命令附加取消信号,这样当命令超时或用户取消操作时,可以中止长时间运行的进程。 在调用 `controller.kill()` 后,等待的调用会被拒绝;你可以捕获该拒绝来进行日志记录、清理或重试。 这种模式使沙盒自动化保持响应性,防止孤立进程无限消耗资源。 --- # 设置和获取环境变量 > 了解如何在沙箱中设置和获取环境变量。 URL: https://docs.deno.com/examples/sandboxes_environment_variables/ 你可以使用 `sandbox.env.set()` 方法在沙箱中设置环境变量。 ```ts import { Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create(); // 设置环境变量 await sandbox.env.set("API_KEY", "secret-key-123"); await sandbox.env.set("NODE_ENV", "production"); // 在脚本中使用它们 const apiKey = await sandbox.sh`echo $API_KEY`.text(); console.log("API_KEY:", apiKey.trim()); ``` 通过 `sandbox.env.set()` 设置环境变量可以将配置和秘密保持在沙箱内部,因此脚本运行时拥有预期的上下文,而不必在源文件中硬编码值。当你需要每次运行配置(API 密钥、像 NODE_ENV 这样的模式),或者想安全地向多个命令传递凭据时,这非常有用。变量仅在沙箱会话范围内有效,并可供你在该沙箱中执行的任何命令使用。 --- # 错误处理 > 学习如何在沙箱中处理错误。 URL: https://docs.deno.com/examples/sandbox_error_handling/ 明确处理沙箱命令失败可以为你提供可预测的恢复路径: ```ts import { Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create(); // 命令在非零退出时默认抛出错误 try { await sandbox.sh`exit 1`; } catch (error) { console.log("命令失败:", error); } // 使用 noThrow() 手动处理错误 const result = await sandbox.sh`exit 1`.noThrow(); console.log("退出码:", result.status.code); // → 1 console.log("成功:", result.status.success); // → false ``` Deno 沙箱命令在任何非零退出时都会抛出错误,因此将它们包裹在 try/catch 中, 可以让你显示清晰的错误信息或触发备用逻辑,而不是导致整个工作流崩溃。 当你想在不抛出错误的情况下检查失败时,`.noThrow()` 会返回完整的状态对象, 你可以基于 `status.code` 或 `status.success` 进行分支,记录诊断信息,或重试特定命令而不丢失上下文。 这种模式对于健壮的自动化来说至关重要,因为命令可能因用户输入、临时的网络问题或缺少依赖项而失败。 ## 自定义错误类 你可以在沙箱中使用自定义错误类来处理错误。 捕获 `SandboxCommandError` 让你能够区分沙箱命令失败和其他异常。当错误是 `SandboxCommandError` 类时, 你可以读取结构化字段,比如 `error.code`,或格式化 `error.message` 来决定是否重试、升级错误,或将退出码映射到你自己的领域特定错误: ```ts import { Sandbox, SandboxCommandError } from "@deno/sandbox"; await using sandbox = await Sandbox.create(); try { await sandbox.sh`exit 42`; } catch (error) { if (error instanceof SandboxCommandError) { console.log("退出码:", error.code); // → 42 console.log("错误信息:", error.message); } } ``` 这使得构建能够智能响应已知失败模式的高级自动化变得更容易,而不是对所有抛出的错误一视同仁。 --- # 评估 JavaScript > 了解如何在沙箱中评估 JavaScript 代码。 URL: https://docs.deno.com/examples/sandbox_evaluating_javascript/ 你可以使用 `eval` 函数在沙箱中评估 JavaScript 代码。 ```ts import { Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create(); const result = await sandbox.deno.eval(` const a = 1; const b = 2; a + b; `); console.log("result:", result); ``` 调用 `sandbox.deno.eval()` 允许你直接在沙箱的 Deno 运行时中运行任意 JavaScript 代码片段,而无需编写文件或调用外部命令。这在你想快速原型化逻辑、运行小型计算或检查沙箱环境时非常有用。适用于动态脚本或探索性调试,当创建完整模块显得过于繁琐时。 --- # 交互式 JavaScript REPL > 学习如何在沙箱中提供交互式的 Deno REPL。 URL: https://docs.deno.com/examples/sandbox_javascript_repl/ REPL(读取-求值-输出循环)是一种交互式执行会话,其中你输入代码,环境读取它,进行求值,打印结果,然后保持会话活跃,以便你可以继续运行更多代码并保持状态。 `sandbox.deno.repl()` 方法可以用于在沙箱中提供交互式的 JavaScript REPL。 ```ts import { Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create(); // 启动一个 Deno REPL const repl = await sandbox.deno.repl(); // 交互式执行代码,保持状态 await repl.eval("const x = 42;"); await repl.eval("const y = 8;"); const result = await repl.eval("x + y"); console.log("result:", result); // 50 ``` --- # 配置沙箱内存 > 了解如何配置分配给沙箱的内存 URL: https://docs.deno.com/examples/sandbox_memory/ 您可以使用 `memoryMb` 选项自定义分配给沙箱的内存量。这允许您为内存密集型工作负载分配更多资源,或为较轻的任务减少内存。 ```ts import { Sandbox } from "@deno/sandbox"; // 创建一个具有 1GB 内存的沙箱 await using sandbox = await Sandbox.create({ memoryMb: 1024 }); ``` ```ts import { Sandbox } from "@deno/sandbox"; // 为内存密集型工作负载创建一个具有 4GB 内存的沙箱 await using sandbox = await Sandbox.create({ memoryMb: 4096 }); // 检查可用内存 const memInfo = await sandbox.deno.eval<{ total: number }>( "Deno.systemMemoryInfo()", ); console.log("总内存:", memInfo.total); ``` 配置 `memoryMb` 选项用于创建沙箱,可以让您根据工作负载调整资源使用。轻量级任务可以在较小的沙箱中运行以节省资源,而数据密集型脚本或编译任务则可以请求高达 4 GB 的内存,以避免内存不足的错误。 由于您可以通过 `Deno.systemMemoryInfo()` 以编程方式检查沙箱的内存,您可以根据测量到的限制来验证内存分配或调整行为。此控制有助于将沙箱容量匹配到您的需求,同时保持性能的可预测性和成本管理。 内存限制(未来可能会变): - 最小:768MB - 最大:4096MB 由于系统开销,沙箱内实际可用的内存可能会略低于配置值。 > 想要分配更多内存?请联系 > deploy@deno.com。 --- # 生成子进程,并获取缓冲输出 > 学习如何生成子进程,并在沙箱中获取缓冲输出。 URL: https://docs.deno.com/examples/sandboxes_spawn_subprocess/ 你可以在沙箱中生成子进程,并获取缓冲输出,如下所示。 ```ts import { Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create(); const cwd = await sandbox.sh`pwd`; ``` 对于长时间运行的进程或大量输出,可以使用 stdout/stderr 流。 --- # 为沙箱提供 SSH 访问权限 > 了解如何为沙箱提供 SSH 访问权限。 URL: https://docs.deno.com/examples/sandbox_ssh_access/ SSH 访问允许您通过 SSH 协议安全地连接到沙箱环境。`sandbox.create({ ssh: true })` 方法可以用来为沙箱提供 SSH 访问权限。 ```ts import { Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create({ ssh: true }); // 等待 Deploy 配置 SSH 访问信息。 const creds = sandbox.ssh ?? await sandbox.exposeSsh(); if (!creds) { throw new Error("未为此沙箱配置 SSH 凭据"); } const { hostname, username } = creds; console.log(`ssh ${username}@${hostname}`); // 通过睡眠保持进程存活,否则脚本退出时沙箱将被销毁。 await new Promise((resolve) => setTimeout(resolve, 10 * 60 * 1000)); // 10 分钟 ``` --- # 将输出流传输到本地文件 > 了解如何在沙箱中将输出流传输到本地文件。 URL: https://docs.deno.com/examples/sandbox_stream_output/ 您可以在沙箱中将输出流传输到本地文件。这避免了在内存中缓冲整个大型产物。 如果你在沙箱内生成了较大的文件(如下例中的 `big.txt`),你可以通过 `ReadableStream` 分块输出来传输,将 Node 的 `fs.WriteStream` 转换为 Web 的 `WritableStream`,以实现高效传输。 ```ts import { Sandbox } from "@deno/sandbox"; import fs from "node:fs"; import { Writable } from "node:stream"; await using sandbox = await Sandbox.create(); // 在沙箱中创建一个大文件 await sandbox.fs.writeTextFile("big.txt", "#".repeat(5_000_000)); // 将其流式传输到本地文件 const child = await sandbox.spawn("cat", { args: ["big.txt"], stdout: "piped", }); const file = fs.createWriteStream("./big-local-copy.txt"); await child.stdout.pipeTo(Writable.toWeb(file)); const status = await child.status; console.log("完成:", status); ``` 这种模式保持内存使用量稳定,适合日志或大型二进制文件,并且允许你在主机上持久化沙箱结果,而无需临时文件或限制标准输出。 --- # 带变量插值的模板字面量命令 > 学习如何在沙箱中使用带变量插值的模板字面量命令。 URL: https://docs.deno.com/examples/sandboxes_template_literals/ 你可以在沙箱中使用带变量插值的模板字面量命令。 ```ts import { Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create(); // 变量会自动进行转义 const filename = "带空格的文件.txt"; const content = "你好,世界!"; await sandbox.sh`echo ${content} > ${filename}`; // 数组会被展开为多个参数 const files = ["file1.txt", "file2.txt", "file3.txt"]; await sandbox.sh`rm ${files}`; // 获取 JSON 输出 const data = await sandbox.sh`echo '{"count": 42}'`.json<{ count: number }>(); console.log(data.count); // → 42 ``` 插入到模板字面量中的变量会自动进行转义,因此即使是带空格的文件名等复杂值也可以传递,而不必担心引用或注入问题。 数组会自动展开成多个参数,使批量操作(例如删除多个文件)简洁明了,无需手动拼接。你还可以链式调用诸如 `.json()` 的辅助函数,将命令输出直接解析为类型化的数据结构,避免脆弱的字符串解析,并保持结果的强类型。 --- # 控制沙箱超时 > 了解如何使用 timeout 选项控制沙箱的存活时间。 URL: https://docs.deno.com/examples/sandbox_timeout_control/ 你可以使用 timeout 选项来控制沙箱的存活时间: 控制超时可以让你决定沙箱在脚本结束时是否立即消失,还是保持运行一段设定的时间: ```ts import { Sandbox } from "@deno/sandbox"; // 默认:"session" - 当你关闭/释放客户端时,沙箱关闭 await using sandbox = await Sandbox.create({ timeout: "session" }); ``` 支持的时间单位后缀:`s`(秒),`m`(分钟)。 示例:"30s"、"5m"、"90s"。 ```ts import { Sandbox } from "@deno/sandbox"; // 基于时长:让沙箱在特定时间段内保持运行 // 当你希望脚本退出后沙箱仍然存在时非常有用 const sandbox = await Sandbox.create({ timeout: "5m" }); // 5 分钟 const id = sandbox.id; // 关闭沙箱的*连接*;沙箱继续运行 await sandbox.close(); // 之后,使用沙箱 ID 重新连接到同一个沙箱 const reconnected = await Sandbox.connect({ id }); await reconnected.sh`echo 'Still alive!'`; // 你仍然可以在超时结束前强制终止它 await reconnected.kill(); // 此时,沙箱已无法重新连接 ``` 默认的 "session" 模式适合短期自动化——资源会在客户端释放时立即清理。 基于时长的超时(如 "30s"、"5m" 等)允许你关闭客户端连接,而沙箱保持状态活着,这样你可以在超时到期前重新连接(例如,查看日志、重新运行命令,或将沙箱 ID 分享给其他进程)。 ## 根据需要随时延长超时 你并不局限于最初设定的时长。只要你仍持有一个 `Sandbox` 实例(无论是原始句柄还是通过 `Sandbox.connect()` 重新连接的实例),调用 `sandbox.extendTimeout()` 并传入新的时长字符串即可将关闭时间往后推。每次调用最多能延长 30 分钟,并返回一个 `Date` 表示新的关闭时间。 ```ts import { Sandbox } from "@deno/sandbox"; const sandbox = await Sandbox.create({ timeout: "5m" }); // 需要更多时间?无需中断正在运行的工作,直接延长超时。 const newExpiry = await sandbox.extendTimeout("30m"); console.log(`Sandbox now lives until ${newExpiry.toISOString()}`); ``` 如果不再需要沙箱,也可以显式调用 `kill()` 提前结束它,这在任务比预期提前完成时非常有用。 > 需要其他超时模式?请联系 > deploy@deno.com。 --- # 上传文件和目录 > 了解如何将文件和目录上传到沙箱。 URL: https://docs.deno.com/examples/sandbox_upload_files/ 使用 `sandbox.fs.upload(localPath, sandboxPath)` 将文件从您的机器复制到沙箱中。 ```ts import { Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create(); // 将单个文件上传到沙箱中的指定路径 await sandbox.fs.upload("./README.md", "./readme-copy.md"); // 将本地目录树上传到沙箱当前目录 await sandbox.fs.upload("./my-project", "."); ``` 使用 `sandbox.fs.upload()` 上传文件或整个目录,允许您在运行命令之前将本地资源带入沙箱环境中。 当您的工作流程依赖于现有的源文件夹、配置文件或测试数据时,这非常有用——上传完成后,沙箱可以编译、测试或处理这些内容,无需远程 Git 访问或手动复制粘贴。 --- # 在沙箱中提供一个 VSCode 实例 > 了解如何在沙箱中提供一个 VSCode 实例。 URL: https://docs.deno.com/examples/sandbox_vscode_instance/ 运行 `sandbox.exposeVscode()` 会在一个隔离的沙箱环境中启动一个完整的 VS Code 实例,并暴露其 URL,您可以在浏览器中打开它。当您需要一个轻量级、可丢弃的编辑器来进行演示、研讨会或远程调试时,这非常方便:您可以按需提供 VS Code,无需在本地安装任何东西,在受限的工作区内安全地实验代码,并在完成后自动拆除。 ```ts import { Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create(); // 启动一个 VSCode 实例 const vscode = await sandbox.exposeVscode(); console.log(vscode.url); // 打印正在运行实例的 URL await vscode.status; // 等待直到它退出 ``` --- # 服务一个网页框架 > 创建 package.json,安装依赖,运行一个网页框架(Express),并从沙箱公开它 URL: https://docs.deno.com/examples/sandbox_web_framework/ 使用 Deno Sandbox,你可以创建一个 `package.json`,安装依赖,运行网页框架(例如 Express),并通过 HTTP 公开它。 此示例展示如何在沙箱内创建一个最简 Express 应用,运行在 3000 端口,并通过 `sandbox.exposeHttp()` 对外公开。 ```ts import { Sandbox } from "@deno/sandbox"; await using sandbox = await Sandbox.create(); // 1) 在沙箱中写入 package.json 和 server.js const PACKAGE_JSON = { name: "sandbox-express-demo", private: true, type: "module", dependencies: { express: "^4.19.2" }, }; await sandbox.fs.writeTextFile( "package.json", JSON.stringify(PACKAGE_JSON, null, 2), ); await sandbox.fs.writeTextFile( "server.js", `import express from 'express'; const app = express(); app.get('/', (req, res) => res.send('来自 @deno/sandbox 的 Express 你好!')); app.get('/time', (req, res) => res.json({ now: new Date().toISOString() })); app.listen(3000, () => console.log('监听端口 :3000')); `, ); // 2) 安装依赖 await sandbox.sh`deno install`; // 3) 启动服务器 const server = await sandbox.deno.run({ entrypoint: "server.js" }); // 4) 对外发布 const publicUrl = await sandbox.exposeHttp({ port: 3000 }); console.log("公开 URL:", publicUrl); // 例如 https://.sandbox.deno.net // 从你的本地机器 fetch 以验证 const resp = await fetch(`${publicUrl}/time`); console.log(await resp.json()); // 在你需要时保持进程活跃;完成时,关闭沙箱 // 会关闭服务器。 ``` --- # 如何在 Deno 中使用 Apollo > Step-by-step tutorial on integrating Apollo GraphQL with Deno. Learn how to set up an Apollo Server, define schemas, implement resolvers, and build a complete GraphQL API using TypeScript. URL: https://docs.deno.com/examples/apollo_tutorial/ [Apollo Server](https://www.apollographql.com/) 是一个 GraphQL 服务器,您可以在几分钟内设置并与现有数据源(或 REST API)一起使用。然后,您可以将任何 GraphQL 客户端连接到它,以接收数据并利用 GraphQL 的好处,例如类型检查和高效获取。 我们将启动一个简单的 Apollo 服务器,使我们能够查询一些本地数据。我们只需要三个文件: 1. `schema.ts` 用于设置我们的数据模型 2. `resolvers.ts` 用于设置我们如何填充模式中的数据字段 3. 我们的 `main.ts`,服务器将在此启动 我们将开始创建它们: ```shell touch schema.ts resolvers.ts main.ts ``` 让我们逐一设置。 [查看源代码。](https://github.com/denoland/examples/tree/main/with-apollo) ## schema.ts 我们的 `schema.ts` 文件描述了我们的数据。在这种情况下,我们的数据是一系列恐龙。我们希望用户能够获取每个恐龙的名称和简短描述。在 GraphQL 语言中,这意味着 `Dinosaur` 是我们的 **类型**,而 `name` 和 `description` 是我们的 **字段**。我们还可以为每个字段定义数据类型。在这种情况下,这两个字段均为字符串。 这里也是我们使用 GraphQL 特殊 **Query** 类型描述我们允许查询的数据的地方。我们有两个查询: - `dinosaurs` 用于获取所有恐龙的列表 - `dinosaur` 需要传入恐龙的 `name` 作为参数,并返回关于该种类恐龙的信息。 我们将在 `typeDefs` 类型定义变量中导出所有这些内容: ```tsx export const typeDefs = ` type Dinosaur { name: String description: String } type Query { dinosaurs: [Dinosaur] dinosaur(name: String): Dinosaur } `; ``` 如果我们想要写入数据,这里也是我们描述 **Mutation** 的地方。Mutation 是使用 GraphQL 写入数据的方式。由于我们在这里使用的是静态数据集,因此我们不会写入任何内容。 ## resolvers.ts 解析器负责填充每个查询的数据。在这里,我们有我们的恐龙列表,解析器要做的就是 a) 如果用户请求 `dinosaurs` 查询,则将整个列表传递给客户端,或者 b) 如果用户请求 `dinosaur` 查询,则仅传递一个。 ```tsx const dinosaurs = [ { name: "Aardonyx", description: "爬行动物演化的早期阶段。", }, { name: "Abelisaurus", description: '"阿贝尔的蜥蜴" 是从一具单一的头骨重建而来的。', }, ]; export const resolvers = { Query: { dinosaurs: () => dinosaurs, dinosaur: (_: any, args: any) => { return dinosaurs.find((dinosaur) => dinosaur.name === args.name); }, }, }; ``` 对于后者,我们将客户端的参数传递到一个函数中,以匹配名称和我们数据集中的名称。 ## main.ts 在我们的 `main.ts` 中,我们将导入 `ApolloServer` 以及 `graphql` 和我们的模式中的 `typeDefs` 与解析器: ```tsx import { ApolloServer } from "npm:@apollo/server@^4.1"; import { startStandaloneServer } from "npm:@apollo/server@4.1/standalone"; import { graphql } from "npm:graphql@16.6"; import { typeDefs } from "./schema.ts"; import { resolvers } from "./resolvers.ts"; const server = new ApolloServer({ typeDefs, resolvers, }); const { url } = await startStandaloneServer(server, { listen: { port: 8000 }, }); console.log(`服务器运行在: ${url}`); ``` 我们将 `typeDefs` 和 `resolvers` 传递给 `ApolloServer` 以启动一个新服务器。最后,`startStandaloneServer` 是一个帮助函数,用于快速启动服务器。 ## 运行服务器 现在剩下的就是运行服务器: ```shell deno run --allow-net --allow-read --allow-env main.ts ``` 您应该在终端中看到 `服务器运行在: 127.0.0.1:8000`。如果您访问该地址,您将看到 Apollo 沙盒,在那里我们可以输入我们的 `dinosaurs` 查询: ```graphql query { dinosaurs { name description } } ``` 这将返回我们的数据集: ```graphql { "data": { "dinosaurs": [ { "name": "Aardonyx", "description": "爬行动物演化的早期阶段。" }, { "name": "Abelisaurus", "description": "\"阿贝尔的蜥蜴\" 是从一具单一的头骨重建而来的。" } ] } } ``` 或者如果我们想要只获取一个 `dinosaur`: ```graphql query { dinosaur(name:"Aardonyx") { name description } } ``` 这将返回: ```graphql { "data": { "dinosaur": { "name": "Aardonyx", "description": "爬行动物演化的早期阶段。" } } } ``` 太棒了! [了解有关使用 Apollo 和 GraphQL 的更多信息,请查看他们的教程](https://www.apollographql.com/tutorials/)。 --- # 使用 Deno 构建 Astro > 逐步教程:使用 Astro 和 Deno 构建 Web 应用程序。学习如何搭建项目、创建动态页面、实现服务器端渲染(SSR),以及使用 Deno 的 Node.js 兼容性部署你的 Astro 网站。 URL: https://docs.deno.com/examples/astro_tutorial/ [Astro](https://astro.build/) 是一个专注于内容驱动型网站的现代 Web 框架,采用 Islands 架构,默认情况下不会向客户端发送任何 JavaScript。你可以查看[GitHub 上的完整应用](https://github.com/denoland/tutorial-with-astro)。 你可以在 [Deno Deploy](https://tutorial-with-astro.deno.deno.net/) 上看到该应用的在线版本。 :::info 部署你自己的应用 想跳过教程,立即部署完整的 Astro 恐龙应用?点击以下按钮,立即将完整的 Astro 恐龙应用副本部署到 Deno Deploy。你将获得一个可在线运行、可自定义并修改的应用,边学边用! [![Deploy on Deno](https://deno.com/button)](https://console.deno.com/new?clone=https://github.com/denoland/tutorial-with-astro) ::: ## 创建一个 Astro 项目 Astro 提供了一个 CLI 工具,可快速生成新的 Astro 项目。在你的终端运行以下命令,使用 Deno 创建一个新的 Astro 项目。 ```sh deno init --npm astro@latest ``` 本教程中,我们选择“Empty”模板,方便从零开始搭建,然后安装依赖。 此操作将为我们搭建一个基础的 Astro 项目结构,包括一个 `package.json` 文件,以及存放应用代码的 `src` 目录。 ## 启动 Astro 服务器 我们可以使用 `dev` 任务启动本地 Astro 开发服务器。在终端切换到新项目目录,运行: ```sh deno task dev ``` 这将启动 Astro 开发服务器,监视文件改动并自动刷新浏览器页面。你会看到服务器运行在 `http://localhost:4321` 的提示信息。 在浏览器访问该 URL,你应该看到一个非常基础的 Astro 欢迎页面。 ## 构建应用架构 现在我们已搭建好基础 Astro 项目,接下来构建应用架构。我们将创建几个目录以组织代码,并设置基础路由。创建以下目录结构: ```text src/ ├── data/ ├── lib/ └── pages/ └── index.astro ``` ## 添加恐龙数据 在 `data` 目录下新建一个名为 `data.json` 的文件,用于存放硬编码的恐龙数据。 复制以下[这个 json 文件](https://raw.githubusercontent.com/denoland/tutorial-with-astro/refs/heads/main/src/data/data.json)内容,粘贴到 `data.json` 文件中。(如果是实际项目,你可能会从数据库或外部 API 拉取这些数据。) ## 设置业务逻辑 接着,我们创建一个 `lib` 目录,放置业务逻辑代码。在这里我们创建 `dinosaur-service.ts` 文件,包含用于获取恐龙数据的函数。新建 `src/lib/dinosaur-service.ts` 并写入如下代码: ```ts title="src/lib/dinosaur-service.ts" // 简单的恐龙数据处理工具函数 import dinosaursData from "../data/data.json"; export interface Dinosaur { name?: string; description: string; } export class DinosaurService { private static dinosaurs: Dinosaur[] = dinosaursData; // 获取所有有名称的恐龙(过滤掉无名恐龙) static getNamedDinosaurs(): Dinosaur[] { return this.dinosaurs.filter((dino) => dino.name); } // 根据恐龙名称创建 URL 友好的 slug static createSlug(name: string): string { return name .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } // 根据 slug 获取恐龙数据 static getDinosaurBySlug(slug: string): Dinosaur | undefined { return this.dinosaurs.find((dino) => { if (!dino.name) return false; return this.createSlug(dino.name) === slug; }); } // 获取带 slug 的恐龙数据列表以用作链接 static getDinosaursWithSlugs() { return this.getNamedDinosaurs().map((dino) => ({ ...dino, slug: this.createSlug(dino.name!), })); } } export default DinosaurService; ``` 该文件定义了一个 `DinosaurService` 类,包含获取所有恐龙、创建 URL 友好 slug 和根据 slug 获取恐龙数据的方法。 ## 更新首页使用服务 现在可以更新 `index.astro` 页面,调用 `DinosaurService` 获取恐龙数据并渲染为链接列表。更新 `src/pages/index.astro` 文件内容如下: ```jsx title="src/pages/index.astro" --- import DinosaurService from '../lib/dinosaur-service'; import '../../styles/index.css'; // 获取带 slug 的所有恐龙,用于创建链接 const dinosaursWithSlugs = DinosaurService.getDinosaursWithSlugs(); --- 恐龙目录

🦕 恐龙目录

点击任意恐龙名称了解更多信息!

{dinosaursWithSlugs.map((dinosaur) => ( {dinosaur.name} ))}
``` 我们导入了 `DinosaurService`,然后遍历恐龙数据,创建指向单个恐龙页面的链接。 ## 创建单个恐龙详情页 接下来为每只恐龙创建独立页面。在 `src/pages` 目录中创建一个 `dinosaurs` 文件夹,在该文件夹内创建名为 `[slug].astro` 的文件,内容如下: ```jsx title="src/pages/dinosaurs/[slug].astro" --- import DinosaurService from '../../lib/dinosaur-service'; import '../../styles/index.css'; export async function getStaticPaths() { const dinosaursWithSlugs = DinosaurService.getDinosaursWithSlugs(); return dinosaursWithSlugs.map((dinosaur) => ({ params: { slug: dinosaur.slug }, props: { dinosaur } })); } const { dinosaur } = Astro.props; --- {dinosaur.name} - 恐龙目录

🦕 {dinosaur.name}

{dinosaur.description}

返回目录
``` 该文件使用 `getStaticPaths` 生成所有恐龙的静态路由路径,`Astro.props` 会携带当前 slug 对应的恐龙数据,我们在页面中进行渲染。 ## 添加样式 你可以在 `src/styles/index.css` 文件中为应用添加个性化样式。该文件在 `index.astro` 和 `[slug].astro` 文件中都被导入,因此所添加的样式会应用于这两个页面。 ## 构建和部署 Astro 内置了用于生产构建的命令: ```sh deno run build ``` 此命令将: - 在 `dist` 目录中生成每个页面对应的静态 HTML 文件。 - 优化你的资源文件(CSS、JavaScript、图片等),适配生产环境。 你可以将该应用部署到你喜欢的云提供商。我们推荐使用 [Deno Deploy](https://deno.com/deploy),体验简单便捷。你可以直接从 GitHub 部署,只需新建一个 GitHub 仓库,推送代码后连接到 Deno Deploy 即可。 ### 创建 GitHub 仓库 [创建一个新的 GitHub 仓库](https://github.com/new),然后初始化并推送你的项目: ```sh git init -b main git remote add origin https://github.com/<你的_github_用户名>/<你的仓库名>.git git add . git commit -am 'initial commit' git push -u origin main ``` ### 部署到 Deno Deploy 代码托管至 GitHub 后,你可以在 [Deno DeployEA 控制面板](https://console.deno.com/) 上进行部署。 如果想了解部署流程,请查看 [Deno Deploy 教程](/examples/deno_deploy_tutorial/)。 🦕 现在,你可以使用 Deno 搭建并开发一个 Astro 应用!你可以继续扩展该应用,比如添加用户认证、数据库、甚至 CMS。我们期待看到你基于 Astro 和 Deno 创造的精彩项目! --- # 如何将 Deno 部署到 AWS Lambda > Step-by-step tutorial on deploying Deno applications to AWS Lambda. Learn about Docker containerization, ECR repositories, function configuration, and how to set up serverless Deno apps on AWS. URL: https://docs.deno.com/examples/aws_lambda_tutorial/ AWS Lambda 是由亚马逊网络服务提供的一种无服务器计算服务。它允许您在无需配置或管理服务器的情况下运行代码。 以下是将 Deno 应用程序部署到 AWS Lambda 的逐步指南,使用 Docker。 这需要的前提条件是: - [`docker` CLI](https://docs.docker.com/reference/cli/docker/) - 一个 [AWS 账户](https://aws.amazon.com) - [`aws` CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) ## 第一步:创建一个 Deno 应用 使用以下代码创建一个新的 Deno 应用: ```ts title="main.ts" Deno.serve((req) => new Response("Hello World!")); ``` 将此代码保存在名为 `main.ts` 的文件中。 ## 第二步:创建一个 Dockerfile 创建一个名为 `Dockerfile` 的新文件,内容如下: ```Dockerfile # 设置基础镜像 FROM public.ecr.aws/awsguru/aws-lambda-adapter:0.9.0 AS aws-lambda-adapter FROM denoland/deno:bin-1.45.2 AS deno_bin FROM debian:bookworm-20230703-slim AS deno_runtime COPY --from=aws-lambda-adapter /lambda-adapter /opt/extensions/lambda-adapter COPY --from=deno_bin /deno /usr/local/bin/deno ENV PORT=8000 EXPOSE 8000 RUN mkdir /var/deno_dir ENV DENO_DIR=/var/deno_dir # 复制功能代码 WORKDIR "/var/task" COPY . /var/task # 预热缓存 RUN timeout 10s deno run -A main.ts || [ $? -eq 124 ] || exit 1 CMD ["deno", "run", "-A", "main.ts"] ``` 此 Dockerfile 使用 [`aws-lambda-adapter`](https://github.com/awslabs/aws-lambda-web-adapter) 项目将常规 HTTP 服务器(如 Deno 的 `Deno.serve`)适配到 AWS Lambda 运行时 API。 我们还使用 `denoland/deno:bin-1.45.2` 镜像获取 Deno 二进制文件,使用 `debian:bookworm-20230703-slim` 作为基础镜像。`debian:bookworm-20230703-slim` 镜像用于保持镜像大小较小。 将 `PORT` 环境变量设置为 `8000`,以通知 AWS Lambda 适配器我们正在监听端口 `8000`。 将 `DENO_DIR` 环境变量设置为 `/var/deno_dir`,以在 `/var/deno_dir` 目录中存储缓存的 Deno 源代码和转译模块。 预热缓存步骤用于在调用函数之前预热 Deno 缓存。这样做是为了减少函数的冷启动时间。这些缓存包含您函数代码的编译代码和依赖项。此步骤启动您的服务器 10 秒钟,然后退出。 在使用 package.json 时,请记得在预热缓存或运行函数之前运行 `deno install` 以从 `package.json` 文件中安装 `node_modules`。 ## 第三步:构建 Docker 镜像 使用以下命令构建 Docker 镜像: ```bash docker build -t hello-world . ``` ## 第四步:创建 ECR Docker 存储库并推送镜像 使用 AWS CLI,创建一个 ECR 存储库并将 Docker 镜像推送到其中: ```bash aws ecr create-repository --repository-name hello-world --region us-east-1 | grep repositoryUri ``` 这应该会输出一个类似 `.dkr.ecr.us-east-1.amazonaws.com/hello-world` 的存储库 URI。 使用上一步的存储库 URI 对 Docker 进行 ECR 身份验证: ```bash aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin .dkr.ecr.us-east-1.amazonaws.com ``` 使用存储库 URI 对 Docker 镜像进行标记,再次使用上一步的存储库 URI: ```bash docker tag hello-world:latest .dkr.ecr.us-east-1.amazonaws.com/hello-world:latest ``` 最后,使用上一步的存储库 URI 将 Docker 镜像推送到 ECR 存储库: ```bash docker push .dkr.ecr.us-east-1.amazonaws.com/hello-world:latest ``` ## 第五步:创建 AWS Lambda 函数 现在您可以通过 AWS 管理控制台创建一个新的 AWS Lambda 函数。 1. 转到 AWS 管理控制台并 [导航到 Lambda 服务](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1)。 2. 点击 "创建函数" 按钮。 3. 选择 "容器镜像"。 4. 输入函数的名称,例如 "hello-world"。 5. 点击 "浏览镜像" 按钮并选择您推送到 ECR 的镜像。 6. 点击 "创建函数" 按钮。 7. 等待函数创建完成。 8. 在 "配置" 选项卡中,转到 "函数 URL" 部分并点击 "创建函数 URL"。 9. 选择 "无" 作为身份验证类型(这将使 Lambda 函数公开可访问)。 10. 点击 "保存" 按钮。 ## 第六步:测试 Lambda 函数 您现在可以访问 Lambda 函数的 URL,以查看来自 Deno 应用的响应。 🦕 您已成功使用 Docker 将 Deno 应用程序部署到 AWS Lambda。现在您可以使用此设置将更复杂的 Deno 应用程序部署到 AWS Lambda。 --- # 将 Deno 部署到 Amazon Lightsail > Step-by-step tutorial on deploying Deno applications to AWS Lightsail. Learn about Docker containers, GitHub Actions automation, continuous deployment, and how to set up cost-effective cloud hosting for Deno apps. URL: https://docs.deno.com/examples/aws_lightsail_tutorial/ [Amazon Lightsail](https://aws.amazon.com/lightsail/) 是开始使用 Amazon Web Services 最简单和最便宜的方式。它允许您托管虚拟机甚至整个容器服务。 本教程将向您展示如何使用 Docker、Docker Hub 和 GitHub Actions 将 Deno 应用部署到 Amazon Lightsail。 在继续之前,请确保您已经准备好: - [`docker` CLI](https://docs.docker.com/engine/reference/commandline/cli/) - 一个 [Docker Hub 帐户](https://hub.docker.com) - 一个 [GitHub 帐户](https://github.com) - 一个 [AWS 帐户](https://aws.amazon.com/) ## 创建 Dockerfile 和 docker-compose.yml 为了专注于部署,我们的应用程序将简单地是一个返回字符串作为 HTTP 响应的 `main.ts` 文件: ```ts import { Application } from "jsr:@oak/oak"; const app = new Application(); app.use((ctx) => { ctx.response.body = "Hello from Deno and AWS Lightsail!"; }); await app.listen({ port: 8000 }); ``` 然后,我们将创建两个文件 -- `Dockerfile` 和 `docker-compose.yml` -- 来构建 Docker 镜像。 在我们的 `Dockerfile` 中,我们将添加: ```Dockerfile FROM denoland/deno EXPOSE 8000 WORKDIR /app ADD . /app RUN deno install --entrypoint main.ts CMD ["run", "--allow-net", "main.ts"] ``` 然后,在我们的 `docker-compose.yml` 中: ```yml version: "3" services: web: build: . container_name: deno-container image: deno-image ports: - "8000:8000" ``` 让我们通过运行 `docker compose -f docker-compose.yml build` 来在本地测试,然后 `docker compose up`,并前往 `localhost:8000`。 ![hello world from localhost](./images/how-to/aws-lightsail/hello-world-from-localhost.png) 它工作正常! ## 构建、标记并推送到 Docker Hub 首先,让我们登录到 [Docker Hub](https://hub.docker.com/repositories) 并创建一个仓库。我们将其命名为 `deno-on-aws-lightsail`。 然后,我们将标记并推送我们的新镜像,将 `username` 替换为您的用户名: 然后,让我们在本地构建镜像。请注意我们的 `docker-compose.yml` 文件将名称构建为 `deno-image`。 ```shell docker compose -f docker-compose.yml build ``` 让我们 [标记](https://docs.docker.com/engine/reference/commandline/tag/) 本地镜像为 `{{ username }}/deno-on-aws-lightsail`: ```shell docker tag deno-image {{ username }}/deno-on-aws-lightsail ``` 现在我们可以将镜像推送到 Docker Hub: ```shell docker push {{ username }}/deno-on-aws-lightsail ``` 在成功之后,您应该能在您的 Docker Hub 仓库中看到新镜像: ![new image on docker hub](./images/how-to/aws-lightsail/new-image-on-docker-hub.png) ## 创建并部署到 Lightsail 容器 让我们前往 [Amazon Lightsail 控制台](https://lightsail.aws.amazon.com/ls/webapp/home/container-services)。 然后单击“容器”和“创建容器服务”。在页面中间,单击“设置您的第一次部署”,选择“指定自定义部署”。 您可以输入任何您想要的容器名称。 在 `Image` 中,请务必使用您在 Docker Hub 中设置的 `{{ username }}/{{ image }}`。在本例中,它是 `lambtron/deno-on-aws-lightsail`。 让我们单击 `添加开放端口` 并添加 `8000`。 最后,在 `公共端点` 下,选择您刚创建的容器名称。 完整的表单应如下所示: ![create container service interface](./images/how-to/aws-lightsail/create-container-service-on-aws.png) 当您准备好时,单击“创建容器服务”。 几秒钟后,您的新容器应该被部署。单击公共地址,您应该能够看到您的 Deno 应用: ![Hello world from Deno and AWS Lightsail](./images/how-to/aws-lightsail/hello-world-from-deno-and-aws-lightsail.png) ## 使用 GitHub Actions 进行自动化 为了自动化该过程,我们将使用 `aws` CLI 及其 [`lightsail` 子命令](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lightsail/push-container-image.html)。 我们在 GitHub Actions 工作流中的步骤将是: 1. 检出仓库 2. 在本地构建我们的应用程序为 Docker 镜像 3. 安装并验证 AWS CLI 4. 通过 CLI 将本地 Docker 镜像推送到 AWS Lightsail 容器服务 让这个 GitHub Action 工作流正常工作的前提条件: - 已创建 AWS Lightsail 容器实例(见上文) - 设置了 IAM 用户和相关权限。 ([了解有关为 IAM 用户管理 Amazon Lightsail 访问权限的更多信息。](https://docs.aws.amazon.com/lightsail/latest/userguide/amazon-lightsail-managing-access-for-an-iam-user.html)) - 为您的用户拥有权限的 `AWS_ACCESS_KEY_ID` 和 `AWS_SECRET_ACCESS_KEY`。 (请按照 [此 AWS 指南](https://lightsail.aws.amazon.com/ls/docs/en_us/articles/lightsail-how-to-set-up-access-keys-to-use-sdk-api-cli) 来获取生成的 `AWS_ACCESS_KEY_ID` 和 `AWS_SUCCESS_ACCESS_KEY`。) 让我们创建一个新文件 `container.template.json`,其中包含关于如何进行服务容器部署的配置。请注意这些选项值与我们在上一节手动输入的值的相似性。 ```json { "containers": { "app": { "image": "", "environment": { "APP_ENV": "release" }, "ports": { "8000": "HTTP" } } }, "publicEndpoint": { "containerName": "app", "containerPort": 8000, "healthCheck": { "healthyThreshold": 2, "unhealthyThreshold": 2, "timeoutSeconds": 5, "intervalSeconds": 10, "path": "/", "successCodes": "200-499" } } } ``` 让我们将以下内容添加到 `.github/workflows/deploy.yml` 文件中: ```yml name: Build and Deploy to AWS Lightsail on: push: branches: - main env: AWS_REGION: us-west-2 AWS_LIGHTSAIL_SERVICE_NAME: container-service-2 jobs: build_and_deploy: name: Build and Deploy runs-on: ubuntu-latest steps: - name: Checkout main uses: actions/checkout@v4 - name: Install Utilities run: | sudo apt-get update sudo apt-get install -y jq unzip - name: Install AWS Client run: | curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install || true aws --version curl "https://s3.us-west-2.amazonaws.com/lightsailctl/latest/linux-amd64/lightsailctl" -o "lightsailctl" sudo mv "lightsailctl" "/usr/local/bin/lightsailctl" sudo chmod +x /usr/local/bin/lightsailctl - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-region: ${{ env.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - name: Build Docker Image run: docker build -t ${{ env.AWS_LIGHTSAIL_SERVICE_NAME }}:release . - name: Push and Deploy run: | service_name=${{ env.AWS_LIGHTSAIL_SERVICE_NAME }} aws lightsail push-container-image \ --region ${{ env.AWS_REGION }} \ --service-name ${service_name} \ --label ${service_name} \ --image ${service_name}:release aws lightsail get-container-images --service-name ${service_name} | jq --raw-output ".containerImages[0].image" > image.txt jq --arg image $(cat image.txt) '.containers.app.image = $image' container.template.json > container.json aws lightsail create-container-service-deployment --service-name ${service_name} --cli-input-json file://$(pwd)/container.json ``` 哇,这里有很多内容!最后两个步骤是最重要的: `Build Docker Image` 和 `Push and Deploy`。 ```shell docker build -t ${{ env.AWS_LIGHTSAIL_SERVICE_NAME }}:release . ``` 此命令使用名称 `container-service-2` 构建我们的 Docker 镜像并标记为 `release`。 ```shell aws lightsail push-container-image ... ``` 此命令将本地镜像推送到我们的 Lightsail 容器。 ```shell aws lightsail get-container-images --service-name ${service_name} | jq --raw-output ".containerImages[0].image" > image.txt ``` 此命令检索镜像信息,并使用 [`jq`](https://stedolan.github.io/jq/) 进行解析,将镜像名称保存在本地文件 `image.txt` 中。 ```shell jq --arg image $(cat image.txt) '.containers.app.image = $image' container.template.json > container.json ``` 此命令使用保存在 `image.txt` 中的镜像名称和 `container.template.json` 创建一个名为 `container.json` 的新选项文件。这个选项文件将被传递给 `aws lightsail` 进行下一步的最终部署。 ```shell aws lightsail create-container-service-deployment --service-name ${service_name} --cli-input-json file://$(pwd)/container.json ``` 最后,此命令使用 `service_name` 创建一个新的部署,以及 `container.json` 中的配置设置。 当您将代码推送到 GitHub 并且 Action 成功后,您将能够在 AWS 上看到您的新 Deno 应用: ![deno on aws](./images/how-to/aws-lightsail/hello-world-from-deno-and-aws-lightsail.png) 🦕 现在您可以使用 Docker、Docker Hub 和 GitHub Actions 将 Deno 应用程序部署到 Amazon Lightsail。 --- # 在 Deno 中入门 OpenTelemetry > 在 Deno 应用中设置基本的 OpenTelemetry 监测。本教程涵盖创建一个带有自定义指标和跟踪的简单 HTTP 服务器,以及查看遥测数据。 URL: https://docs.deno.com/examples/basic_opentelemetry_tutorial/ OpenTelemetry 为您的应用提供强大的可观察性工具。借助 Deno 内置的 OpenTelemetry 支持,您可以轻松地对代码进行监测,收集指标、跟踪和日志。 本教程将指导您如何设置一个带有 OpenTelemetry 监测的简单 Deno 应用。 ## 前提条件 - Deno 2.3 或更高版本 ## 第一步:创建一个简单的 HTTP 服务器 让我们开始创建一个基础的 HTTP 服务器,模拟一个小型的网页应用: ```ts title="server.ts" import { metrics, trace } from "npm:@opentelemetry/api@1"; // 为我们的应用创建 tracer 和 meter const tracer = trace.getTracer("my-server", "1.0.0"); const meter = metrics.getMeter("my-server", "1.0.0"); // 创建一些指标 const requestCounter = meter.createCounter("http_requests_total", { description: "HTTP 请求总数", }); const requestDuration = meter.createHistogram("http_request_duration_ms", { description: "HTTP 请求持续时间(毫秒)", unit: "ms", }); // 启动服务器 Deno.serve({ port: 8000 }, (req) => { // 记录请求开始时间,以测量请求持续时间 const startTime = performance.now(); // 为该请求创建一个 span return tracer.startActiveSpan("handle_request", async (span) => { try { // 从 URL 中提取路径 const url = new URL(req.url); const path = url.pathname; // 为 span 添加属性 span.setAttribute("http.route", path); span.setAttribute("http.method", req.method); span.updateName(`${req.method} ${path}`); // 为 span 添加事件 span.addEvent("request_started", { timestamp: startTime, request_path: path, }); // 模拟一些处理时间 const waitTime = Math.random() * 100; await new Promise((resolve) => setTimeout(resolve, waitTime)); // 为 span 添加另一个事件 span.addEvent("processing_completed"); // 创建响应 const response = new Response(`Hello from ${path}!`, { headers: { "Content-Type": "text/plain" }, }); // 记录指标 requestCounter.add(1, { method: req.method, path, status: 200, }); const duration = performance.now() - startTime; requestDuration.record(duration, { method: req.method, path, }); span.setAttribute("request.duration_ms", duration); return response; } catch (error) { // 在 span 中记录错误 if (error instanceof Error) { span.recordException(error); span.setStatus({ code: trace.SpanStatusCode.ERROR, message: error.message, }); } return new Response("内部服务器错误", { status: 500 }); } finally { // 始终结束 span span.end(); } }); }); ``` 该服务器功能: 1. 为应用创建 tracer 和 meter 2. 设置指标以统计请求数量并测量请求持续时间 3. 为每个请求创建带有属性和事件的 span 4. 模拟处理时间 5. 记录每个请求的指标 ## 第二步:启用 OpenTelemetry 并运行服务器 使用以下命令行标志运行服务器以启用 OpenTelemetry: ```sh OTEL_DENO=true OTEL_SERVICE_NAME=my-server deno run --allow-net server.ts ``` ## 第三步:创建测试客户端 让我们创建一个简单客户端,向服务器发送请求: ```ts title="client.ts" // 向不同路径发送 10 个请求 for (let i = 0; i < 10; i++) { const path = ["", "about", "users", "products", "contact"][i % 5]; const url = `http://localhost:8000/${path}`; console.log(`正在向 ${url} 发送请求`); try { const response = await fetch(url); const text = await response.text(); console.log(`来自 ${url} 的响应:${text}`); } catch (error) { console.error(`获取 ${url} 时出错:`, error); } } ``` ## 第四步:运行客户端 在另一个终端中运行客户端: ```sh deno run --allow-net client.ts ``` ## 第五步:查看遥测数据 默认情况下,Deno 会使用 OTLP 协议将遥测数据导出到 `http://localhost:4318`。您需要一个 OpenTelemetry collector 来接收并可视化这些数据。 ### 安装本地 Collector 最快速的方式是使用 Docker 运行本地 LGTM 堆栈(Loki, Grafana, Tempo, Mimir): ```sh docker run --name lgtm -p 3000:3000 -p 4317:4317 -p 4318:4318 --rm -ti \ -v "$PWD"/lgtm/grafana:/data/grafana \ -v "$PWD"/lgtm/prometheus:/data/prometheus \ -v "$PWD"/lgtm/loki:/data/loki \ -e GF_PATHS_DATA=/data/grafana \ docker.io/grafana/otel-lgtm:0.8.1 ``` 然后访问 http://localhost:3000 登录 Grafana(用户名:admin,密码:admin)。 在 Grafana 中,您可以: 1. 在 Tempo 中查看 **Traces(跟踪)**,查看每个请求的 span 2. 在 Mimir/Prometheus 中查看 **Metrics(指标)**,查看请求计数和持续时间 3. 在 Loki 中查看 **Logs(日志)**,查看应用的任何日志 ## 理解你所看到的内容 ### 跟踪(Traces) 在 Traces 视图中,您将看到: - 服务器处理的每个 HTTP 请求的 span - 客户端发出的每个 fetch 请求的 span - 这些 span 之间的关联关系 点击任一 span 可查看详细信息,包括: - 持续时间 - 属性(如 http.route、http.method 等) - 事件(request_started、processing_completed) ### 指标(Metrics) 在 Metrics 视图中,您可以查询: - `http_requests_total` — 统计 HTTP 请求数量的计数器 - `http_request_duration_ms` — 请求持续时间的直方图 您还可以看到内置的 Deno 指标,如: - `http.server.request.duration` - `http.server.active_requests` ### 日志(Logs) 在 Logs 视图中,您将看到应用程序的所有控制台日志,且带有正确的跟踪上下文。 ## 故障排除 如果您在 Collector 中看不到数据: 1. 检查是否设置了 `OTEL_DENO=true` 2. 确认 Collector 正在运行且在默认端点可访问 3. 检查是否需要将 `OTEL_EXPORTER_OTLP_ENDPOINT` 设置为其他 URL 4. 查看 Deno 控制台输出中是否有错误信息 请记住,Deno 中的 OpenTelemetry 支持仍处于不稳定阶段,未来版本可能会有所变动。 🦕 本教程为希望在 Deno 中尝试 OpenTelemetry 的用户提供了一个简单的起点,无需立即深入复杂概念。 此基础示例可以通过多种方式扩展: - 为业务逻辑添加更多自定义指标 - 为重要操作创建额外的 span - 使用 baggage 在服务间传递上下文属性 - 基于指标阈值设置告警 欲了解更高级的用法,请参阅我们的 [分布式跟踪与上下文传播](/examples/otel_span_propagation_tutorial/) 教程。 --- # 行为驱动开发 (BDD) > 使用 Deno 标准库的 BDD 模块实现行为驱动开发。创建可读性强、组织良好的测试,并进行有效的断言。 URL: https://docs.deno.com/examples/bdd_tutorial/ 行为驱动开发 (BDD) 是一种软件开发方法,鼓励开发人员、质量保证人员和非技术利益相关者之间的协作。BDD 关注通过用所有利益相关者都能理解的自然语言编写的示例来定义应用程序的行为。 Deno 的标准库提供了一个 BDD 风格的测试模块,使您能够以对非技术利益相关者友好且在实现上实用的方式构建测试。在本教程中,我们将探索如何使用 BDD 模块为您的应用程序创建描述性测试套件。 ## BDD 简介 BDD 扩展了 [测试驱动开发](https://en.wikipedia.org/wiki/Test-driven_development) (TDD),通过使用易于阅读的自然语言编写测试。与其考虑“测试”,BDD 鼓励我们考虑“规范”或“规格”,这些规格描述软件应如何从用户的角度进行操作。这种方法有助于保持测试专注于代码应做什么,而不是它是如何实现的。 BDD 的基本元素包括: - **Describe** 块,用于分组相关的规范 - **It** 语句,表达单一的行为 - **Before/After** 钩子,用于设置和拆解操作 ## 使用 Deno 的 BDD 模块 要开始在 Deno 中进行 BDD 测试,我们将使用 [Deno 标准库](https://jsr.io/@std/testing/doc/bdd) 中的 `@std/testing/bdd` 模块。 首先,让我们导入所需的函数: ```ts import { afterAll, afterEach, beforeAll, beforeEach, describe, it, } from "jsr:@std/testing/bdd"; import { assertEquals, assertThrows } from "jsr:@std/assert"; ``` 这些导入提供了核心的 BDD 函数: - `describe` 创建一个块,分组相关的测试 - `it` 声明一个验证特定行为的测试用例 - `beforeEach`/`afterEach` 在每个测试用例前后运行 - `beforeAll`/`afterAll` 在描述块中的所有测试之前或之后运行一次 我们还将使用来自 [`@std/assert`](https://jsr.io/@std/assert) 的断言函数来验证我们的期望。 ### 编写您的第一个 BDD 测试 让我们创建一个简单的计算器模块并使用 BDD 对其进行测试: ```ts title="calculator.ts" export class Calculator { private value: number = 0; constructor(initialValue: number = 0) { this.value = initialValue; } add(number: number): Calculator { this.value += number; return this; } subtract(number: number): Calculator { this.value -= number; return this; } multiply(number: number): Calculator { this.value *= number; return this; } divide(number: number): Calculator { if (number === 0) { throw new Error("无法被零除"); } this.value /= number; return this; } get result(): number { return this.value; } } ``` 现在,让我们使用 BDD 风格测试这个计算器: ```ts title="calculator_test.ts" import { afterEach, beforeEach, describe, it } from "jsr:@std/testing/bdd"; import { assertEquals, assertThrows } from "jsr:@std/assert"; import { Calculator } from "./calculator.ts"; describe("计算器", () => { let calculator: Calculator; // 在每个测试之前创建一个新的 Calculator 实例 beforeEach(() => { calculator = new Calculator(); }); it("应初始化为零", () => { assertEquals(calculator.result, 0); }); it("应初始化为提供的值", () => { const initializedCalculator = new Calculator(10); assertEquals(initializedCalculator.result, 10); }); describe("加法方法", () => { it("应正确地加一个正数", () => { calculator.add(5); assertEquals(calculator.result, 5); }); it("应正确处理负数", () => { calculator.add(-5); assertEquals(calculator.result, -5); }); it("应支持链式调用", () => { calculator.add(5).add(10); assertEquals(calculator.result, 15); }); }); describe("减法方法", () => { it("应正确地减去一个数", () => { calculator.subtract(5); assertEquals(calculator.result, -5); }); it("应支持链式调用", () => { calculator.subtract(5).subtract(10); assertEquals(calculator.result, -15); }); }); describe("乘法方法", () => { beforeEach(() => { // 对于乘法测试,初始值为 10 calculator = new Calculator(10); }); it("应正确地乘以一个数", () => { calculator.multiply(5); assertEquals(calculator.result, 50); }); it("应支持链式调用", () => { calculator.multiply(2).multiply(3); assertEquals(calculator.result, 60); }); }); describe("除法方法", () => { beforeEach(() => { // 对于除法测试,初始值为 10 calculator = new Calculator(10); }); it("应正确地除以一个数", () => { calculator.divide(2); assertEquals(calculator.result, 5); }); it("应在除以零时抛出错误", () => { assertThrows( () => calculator.divide(0), Error, "无法被零除", ); }); }); }); ``` 要运行此测试,请使用 `deno test` 命令: ```sh deno test calculator_test.ts ``` 您将看到类似以下的输出: ```sh running 1 test from file:///path/to/calculator_test.ts 计算器 ✓ 应初始化为零 ✓ 应初始化为提供的值 加法方法 ✓ 应正确地加一个正数 ✓ 应正确处理负数 ✓ 应支持链式调用 减法方法 ✓ 应正确地减去一个数 ✓ 应支持链式调用 乘法方法 ✓ 应正确地乘以一个数 ✓ 应支持链式调用 除法方法 ✓ 应正确地除以一个数 ✓ 应在除以零时抛出错误 ok | 11 passed | 0 failed (234ms) ``` ## 使用嵌套的 describe 块组织测试 BDD 的一个强大特性是能够嵌套 `describe` 块,从而帮助以层次结构组织测试。在计算器示例中,我们在各自的 `describe` 块中分组了每个方法的测试。这不仅使测试更具可读性,而且在测试失败时更容易定位问题。 您可以嵌套 `describe` 块,但要注意不要嵌套得太深,因为过度嵌套可能使测试更难以理解。 ## 钩子 BDD 模块提供了四个钩子: - `beforeEach` 在当前 describe 块中的每个测试之前运行 - `afterEach` 在当前 describe 块中的每个测试之后运行 - `beforeAll` 在当前 describe 块中的所有测试之前运行一次 - `afterAll` 在当前 describe 块中的所有测试之后运行一次 ### beforeEach/afterEach 这些钩子非常适合于: - 为每个测试设置一个新的测试环境 - 在每个测试后清理资源 - 确保测试隔离 在计算器示例中,我们使用 `beforeEach` 在每个测试之前创建一个新的计算器实例,以确保每个测试都从干净的状态开始。 ### beforeAll/afterAll 这些钩子适用于: - 可以共享的昂贵设置操作 - 设置和拆除数据库连接 - 创建和清理共享资源 以下是如何使用 `beforeAll` 和 `afterAll` 的示例: ```ts describe("数据库操作", () => { let db: Database; beforeAll(async () => { // 在所有测试之前一次连接到数据库 db = await Database.connect(TEST_CONNECTION_STRING); await db.migrate(); }); afterAll(async () => { // 在所有测试完成后断开连接 await db.close(); }); it("应插入一条记录", async () => { const result = await db.insert({ name: "测试" }); assertEquals(result.success, true); }); it("应检索一条记录", async () => { const record = await db.findById(1); assertEquals(record.name, "测试"); }); }); ``` ## Gherkin 与 JavaScript 风格的 BDD 如果您熟悉 Cucumber 或其他 BDD 框架,您可能会期待使用 "Given-When-Then" 语句的 Gherkin 语法。 Deno 的 BDD 模块使用的是 JavaScript 风格的语法,而不是 Gherkin。这种方法类似于其他 JavaScript 测试框架,如 Mocha 或 Jasmine。然而,您仍然可以通过以下方式遵循 BDD 原则: 1. 编写清晰、以行为为中心的测试描述 2. 组织测试以反映用户故事 3. 在测试实现中遵循 "Arrange-Act-Assert" 模式 例如,您可以将您的 `it` 块构造为与 Given-When-Then 格式相对应: ```ts describe("计算器", () => { it("应正确加法运算", () => { // Given const calculator = new Calculator(); // When calculator.add(5); // Then assertEquals(calculator.result, 5); }); }); ``` 如果您需要完整的 Gherkin 支持和自然语言规范,请考虑使用与 Deno 兼容的专用 BDD 框架,例如 [cucumber-js](https://github.com/cucumber/cucumber-js)。 ## Deno 的 BDD 最佳实践 ### 编写易于阅读的测试 BDD 测试应像文档一样可读。在您的 `describe` 和 `it` 语句中使用清晰、描述性的语言: ```ts // 好 describe("用户认证", () => { it("应拒绝不正确密码的登录", () => { // 测试代码 }); }); // 不好 describe("auth", () => { it("bad pw fails", () => { // 测试代码 }); }); ``` ### 保持测试专注 每个测试应验证单一行为。避免在单个 `it` 块中测试多个行为: ```ts // 好 it("应将商品添加到购物车", () => { // 测试添加到购物车 }); it("应计算出正确的总数", () => { // 测试总数计算 }); // 不好 it("应添加商品并计算总数", () => { // 测试添加到购物车 // 测试总数计算 }); ``` ### 使用上下文特定的设置 当一个描述块中的测试需要不同的设置时,使用嵌套的描述和它们自己的 `beforeEach` 钩子,而不是条件逻辑: ```ts // 好 describe("用户操作", () => { describe("当用户已登录时", () => { beforeEach(() => { // 设置已登录用户 }); it("应显示仪表盘", () => { // 测试 }); }); describe("当用户未登录时", () => { beforeEach(() => { // 设置未登录状态 }); it("应重定向到登录", () => { // 测试 }); }); }); // 避免 describe("用户操作", () => { beforeEach(() => { // 设置基本状态 if (isLoggedInTest) { // 设置已登录状态 } else { // 设置未登录状态 } }); it("应在已登录时显示仪表盘", () => { isLoggedInTest = true; // 测试 }); it("应在未登录时重定向到登录", () => { isLoggedInTest = false; // 测试 }); }); ``` ### 正确处理异步测试 在测试异步代码时,请记住: - 将您的测试函数标记为 `async` - 对于 Promise 使用 `await` - 正确处理错误 ```ts it("应异步获取用户数据", async () => { const user = await fetchUser(1); assertEquals(user.name, "约翰·多"); }); ``` 🦕 通过遵循本教程中概述的 BDD 原则和实践,您可以构建更可靠的软件,并加深对代码业务逻辑的理解。 请记住,BDD 不仅仅是关于语法或工具,而是共同定义和验证应用程序行为的方法。最成功的 BDD 实施将这些技术实践与开发人员、测试人员、产品与业务利益相关者之间的定期对话结合在一起。 要继续学习 Deno 中的测试,请探索标准库测试套件中的其他模块,例如 [模拟](/examples/mocking_tutorial/) 和 [快照测试](/examples/snapshot_tutorial/)。 --- # 使用 WebSockets 的聊天应用程序 > A tutorial on building a real-time chat app using Deno WebSockets. Learn how to create a WebSocket server with Oak, handle multiple client connections, manage state, and build an interactive chat interface with HTML, CSS, and JavaScript. URL: https://docs.deno.com/examples/chat_app_tutorial/ WebSockets 是构建实时应用程序的强大工具。它们允许客户端和服务器之间的双向通信,而无需不断轮询。WebSockets 的一个常见用例是聊天应用程序。 在本教程中,我们将使用 Deno 和内置的 [WebSockets API](/api/web/websockets) 创建一个简单的聊天应用程序。该聊天应用程序将允许多个聊天客户端连接到同一后端并发送群组消息。在客户端输入用户名后,他们可以开始向其他在线客户端发送消息。每个客户端还会显示当前活跃用户的列表。 您可以在 [GitHub 上查看完成的聊天应用程序](https://github.com/denoland/tutorial-with-websockets)。 ![聊天应用程序 UI](./images/websockets.gif) ## 初始化新项目 首先,为您的项目创建一个新目录并导航到该目录。 ```sh deno init chat-app cd deno-chat-app ``` ## 构建后端 我们将首先构建处理 WebSocket 连接并向所有连接的客户端广播消息的后端服务器。我们将使用 [`oak`](https://jsr.io/@oak/oak) 中间件框架来设置我们的服务器,客户端可以连接到服务器,发送消息并接收有关其他连接用户的更新。此外,服务器将提供构成聊天客户端的静态 HTML、CSS 和 JavaScript 文件。 ### 导入依赖项 首先,我们需要导入必要的依赖项。使用 `deno add` 命令将 Oak 添加到您的项目中: ```sh deno add jsr:@oak/oak ``` ### 设置服务器 在您的 `main.ts` 文件中,添加以下代码: ```ts title="main.ts" import { Application, Context, Router } from "@oak/oak"; import ChatServer from "./ChatServer.ts"; const app = new Application(); const port = 8080; const router = new Router(); const server = new ChatServer(); router.get("/start_web_socket", (ctx: Context) => server.handleConnection(ctx)); app.use(router.routes()); app.use(router.allowedMethods()); app.use(async (context) => { await context.send({ root: Deno.cwd(), index: "public/index.html", }); }); console.log("Listening at http://localhost:" + port); await app.listen({ port }); ``` 接下来,在与 `main.ts` 文件相同的目录中创建一个名为 `ChatServer.ts` 的新文件。在此文件中,我们将放置处理 WebSocket 连接的逻辑: ```ts title="ChatServer.ts" import { Context } from "@oak/oak"; type WebSocketWithUsername = WebSocket & { username: string }; type AppEvent = { event: string; [key: string]: any }; export default class ChatServer { private connectedClients = new Map(); public async handleConnection(ctx: Context) { const socket = await ctx.upgrade() as WebSocketWithUsername; const username = ctx.request.url.searchParams.get("username"); if (this.connectedClients.has(username)) { socket.close(1008, `用户名 ${username} 已被占用`); return; } socket.username = username; socket.onopen = this.broadcastUsernames.bind(this); socket.onclose = () => { this.clientDisconnected(socket.username); }; socket.onmessage = (m) => { this.send(socket.username, m); }; this.connectedClients.set(username, socket); console.log(`新客户端连接:${username}`); } private send(username: string, message: any) { const data = JSON.parse(message.data); if (data.event !== "send-message") { return; } this.broadcast({ event: "send-message", username: username, message: data.message, }); } private clientDisconnected(username: string) { this.connectedClients.delete(username); this.broadcastUsernames(); console.log(`客户端 ${username} 已断开连接`); } private broadcastUsernames() { const usernames = [...this.connectedClients.keys()]; this.broadcast({ event: "update-users", usernames }); console.log("发送用户名列表:", JSON.stringify(usernames)); } private broadcast(message: AppEvent) { const messageString = JSON.stringify(message); for (const client of this.connectedClients.values()) { client.send(messageString); } } } ``` 这段代码设置了一个 `handleConnection` 方法,当建立新的 WebSocket 连接时会被调用。它从 Oak 框架接收一个 Context 对象,并将其升级为 WebSocket 连接。它从 URL 查询参数中提取用户名。如果用户名已经被占用(即存在于 connectedClients 中),它就用适当的消息关闭这个 socket。否则,它在 socket 上设置 username 属性,分配事件处理程序,并将 socket 添加到 `connectedClients`。 当 socket 打开时,它会触发 `broadcastUsernames` 方法,将连接的用户名列表发送给所有客户端。当 socket 关闭时,它会调用 `clientDisconnected` 方法,从连接的客户端列表中删除该客户端。 当收到类型为 `send-message` 的消息时,它将该消息广播给所有连接的客户端,包括发送者的用户名。 ## 构建前端 我们将构建一个简单的用户界面,显示文本输入框和发送按钮,并显示发送的消息,以及聊天中的用户列表。 ### HTML 在您的新项目目录中,创建一个 `public` 文件夹并添加一个 `index.html` 文件,并添加以下代码: ```html title="index.html" Deno 聊天应用程序

🦕 Deno 聊天应用程序

``` ### CSS 如果您想要为聊天应用程序添加样式,请在 `public` 文件夹中创建一个 `style.css` 文件,并添加此 [预制的 CSS](https://raw.githubusercontent.com/denoland/tutorial-with-websockets/refs/heads/main/public/style.css)。 ### JavaScript 我们将在 `app.js` 文件中设置客户端 JavaScript,您在刚刚编写的 HTML 文件中已经看到它的链接。在 `public` 文件夹中添加一个 `app.js` 文件,包含以下代码: ```js title="app.js" const myUsername = prompt("请输入您的名字") || "匿名"; const url = new URL(`./start_web_socket?username=${myUsername}`, location.href); url.protocol = url.protocol.replace("http", "ws"); const socket = new WebSocket(url); socket.onmessage = (event) => { const data = JSON.parse(event.data); switch (data.event) { case "update-users": updateUserList(data.usernames); break; case "send-message": addMessage(data.username, data.message); break; } }; function updateUserList(usernames) { const userList = document.getElementById("users"); userList.replaceChildren(); for (const username of usernames) { const listItem = document.createElement("li"); listItem.textContent = username; userList.appendChild(listItem); } } function addMessage(username, message) { const template = document.getElementById("message"); const clone = template.content.cloneNode(true); clone.querySelector("span").textContent = username; clone.querySelector("p").textContent = message; document.getElementById("conversation").prepend(clone); } const inputElement = document.getElementById("data"); inputElement.focus(); const form = document.getElementById("form"); form.onsubmit = (e) => { e.preventDefault(); const message = inputElement.value; inputElement.value = ""; socket.send(JSON.stringify({ event: "send-message", message })); }; ``` 这段代码提示用户输入用户名,然后使用该用户名作为查询参数创建与服务器的 WebSocket 连接。它监听来自服务器的消息,并根据需要更新连接用户列表或向聊天窗口添加新消息。当用户通过按下回车或点击发送按钮提交表单时,它还会将消息发送到服务器。我们使用 [HTML 模板](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template) 来构建将在聊天窗口中显示的新消息。 ## 运行服务器 要运行服务器,我们需要为 Deno 授予必要的权限。在您的 `deno.json` 文件中,更新 `dev` 任务以允许读取和网络访问: ```diff title="deno.json" -"dev": "deno run --watch main.ts" +"dev": "deno run --allow-net --allow-read --watch main.ts" ``` 现在,如果您访问 [http://localhost:8080](http://localhost:8080/),您将能够开始聊天会话。您可以同时打开 2 个标签页,尝试与自己聊天。 ![聊天应用程序 UI](./images/websockets.gif) 🦕 现在您可以使用 Deno 的 WebSockets,您准备好构建各种实时应用程序了!WebSockets 可用于构建实时仪表板、游戏和协作编辑工具等等!如果您想扩展聊天应用程序,可以考虑向消息中添加数据,以便在消息是由您发送还是其他人发送时能够以不同的样式显示。无论您在构建什么,Deno 都将 WebSocket 传递给您! --- # 从 CommonJS 更新到 ESM > Step-by-step guide to migrating Node.js projects from CommonJS to ESM modules. Learn about import/export syntax changes, module resolution differences, and how to use modern JavaScript features in Deno. URL: https://docs.deno.com/examples/cjs_to_esm_tutorial/ 如果您的 Node.js 项目使用 CommonJS 模块(例如,它使用 `require`),您将需要更新代码以使用 [ECMAScript 模块(ESM)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) 以便在 Deno 中运行。本指南将帮助您更新代码以使用 ESM 语法。 ## 模块导入和导出 Deno 独占支持 [ECMAScript 模块](/runtime/fundamentals/modules/)。 如果您的 Node.js 代码使用 [`require`](https://nodejs.org/api/modules.html#modules-commonjs-modules), 您应该将其更新为使用 `import` 语句。如果您的内部代码使用 CommonJS 风格的导出,则也需要更新这些。 一个典型的 CommonJS 风格项目可能看起来像这样: ```js title="add_numbers.js" module.exports = function addNumbers(num1, num2) { return num1 + num2; }; ``` ```js title="index.js" const addNumbers = require("./add_numbers"); console.log(addNumbers(2, 2)); ``` 要将它们转换为 [ECMAScript 模块](/runtime/fundamentals/modules/),我们将进行一些小改动: ```js title="add_numbers.js" export function addNumbers(num1, num2) { return num1 + num2; } ``` ```js title="index.js" import { addNumbers } from "./add_numbers.js"; console.log(addNumbers(2, 2)); ``` 导出: | CommonJS | ECMAScript 模块 | | ------------------------------------ | -------------------------------- | | `module.exports = function add() {}` | `export default function add() {}` | | `exports.add = function add() {}` | `export function add() {}` | 导入: | CommonJS | ECMAScript 模块 | | ------------------------------------------ | -------------------------------------- | | `const add = require("./add_numbers");` | `import add from "./add_numbers.js";` | | `const { add } = require("./add_numbers")` | `import { add } from "./add_numbers.js"` | ### 使用 VS Code 的快速修复 如果您使用 VS Code,可以利用其内置功能将 CommonJS 转换为 ES6 模块。右键单击 `require` 语句或灯泡图标,选择 `快速修复`,然后选择 `转换为 ES 模块`。 ![快速修复](./images/quick-fix.png) ### CommonJS 与 ECMAScript 解析 这两种模块系统之间的重要区别是 ECMAScript 解析要求完整的标识符 **包括文件扩展名**。 省略文件扩展名和特殊处理 `index.js` 是 CommonJS 独有的特性。 ECMAScript 解析的好处在于,它在浏览器、Deno 和其他运行时中具有一致性。 | CommonJS | ECMAScript 模块 | | -------------------- | --------------------------- | | `"./add_numbers"` | `"./add_numbers.js"` | | `"./some/directory"` | `"./some/directory/index.js"` | :::tip Deno 可以通过运行 `deno lint --fix` 为您添加所有缺失的文件扩展名。 Deno 的 linter 具有 `no-sloppy-imports` 规则,当导入路径不包含文件扩展名时,将显示 linting 错误。 ::: 🦕 现在您知道如何将 CJS 移植到 ESM,您可以利用 ESM 提供的现代功能,例如异步模块加载、与浏览器的互操作性、可读性更好、标准化和未来兼容性。 --- # 将 Deno 部署到 Cloudflare Workers > Step-by-step tutorial on deploying Deno functions to Cloudflare Workers. Learn how to configure denoflare, create worker modules, test locally, and deploy your code to Cloudflare's global edge network. URL: https://docs.deno.com/examples/cloudflare_workers_tutorial/ Cloudflare Workers 允许您在 Cloudflare 的边缘网络上运行 JavaScript。 这是一个关于将 Deno 函数部署到 Cloudflare Workers 的简短指南。 注意:您只能部署 [模块 Worker](https://developers.cloudflare.com/workers/learning/migrating-to-module-workers/) 而不是网页服务器或应用程序。 ## 设置 `denoflare` 为了将 Deno 部署到 Cloudflare,我们将使用这个社区创建的 CLI [`denoflare`](https://denoflare.dev/)。 [安装它](https://denoflare.dev/cli/#installation): ```shell deno install --unstable-worker-options --allow-read --allow-net --global --allow-env --allow-run --name denoflare --force \ https://raw.githubusercontent.com/skymethod/denoflare/v0.6.0/cli/cli.ts ``` ## 创建您的函数 在一个新目录中,创建一个 `main.ts` 文件,里面将包含我们的模块 Worker 函数: ```ts export default { fetch(request: Request): Response { return new Response("Hello, world!"); }, }; ``` 最基本的模块 Worker 函数必须 `export default` 一个对象,该对象暴露一个 `fetch` 函数,返回一个 `Response` 对象。 您可以通过运行以下命令在本地测试它: ```shell denoflare serve main.ts ``` 如果您在浏览器中访问 `localhost:8080`,您将看到响应内容为: ```console Hello, world! ``` ## 配置 `.denoflare` 下一步是创建一个 `.denoflare` 配置文件。在其中,添加以下内容: ```json { "$schema": "https://raw.githubusercontent.com/skymethod/denoflare/v0.5.11/common/config.schema.json", "scripts": { "main": { "path": "/absolute/path/to/main.ts", "localPort": 8000 } }, "profiles": { "myprofile": { "accountId": "abcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "apiToken": "abcxxxxxxxxx_-yyyyyyyyyyyy-11-dddddddd" } } } ``` 您可以通过访问您的 [Cloudflare 仪表板](https://dash.cloudflare.com/),点击 "Workers",并找到右侧的 "帐户 ID" 来查找您的 `accountId`。 您可以从您的 [Cloudflare API Token 设置](https://dash.cloudflare.com/profile/api-tokens)生成一个 `apiToken`。 创建 API 令牌时,请确保使用 "编辑 Cloudflare Workers" 模板。 在您将两者添加到 `.denoflare` 配置后,让我们尝试将其推送到 Cloudflare: ```console denoflare push main ``` 接下来,您可以在您的 Cloudflare 账户中查看您的新函数: ![Cloudflare Workers 上的新函数](./images/how-to/cloudflare-workers/main-on-cloudflare.png) 太好了! --- # 连接到数据库 > Deno 中数据库连接的指南。学习如何使用 MySQL、PostgreSQL、MongoDB、SQLite、Firebase、Supabase 以及流行的 ORM 来构建基于 TypeScript 的数据驱动应用。 URL: https://docs.deno.com/examples/connecting_to_databases_tutorial/ 应用程序通常会从数据库中存储和检索数据。Deno 支持连接到多种数据库管理系统。 Deno 支持多种第三方模块,让你可以连接到 SQL 和 NoSQL 数据库,包括 MySQL、PostgreSQL、MongoDB、SQLite、Firebase 和 Supabase。 你可以在 [JSR](https://jsr.io/@db) 上找到实用的数据库连接模块, 并且 Deno 通过使用 [npm 标识符](/runtime/fundamentals/node/#using-npm-packages) 支持许多 npm 包。 ## SQLite SQLite 是一个自包含、无服务器、零配置且支持事务的 SQL 数据库引擎。它是应用程序本地存储的热门选择。 你可以使用多个模块在 Deno 中连接 SQLite,包括 内置的 [`node:sqlite` 模块](/api/node_sqlite/) 和 JSR 上的 [sqlite](https://jsr.io/@db/sqlite) 模块。 要在你的 Deno 应用中使用 [sqlite](https://jsr.io/@db/sqlite) 模块连接 SQLite: ```sh deno add jsr:@db/sqlite ``` 然后,从模块导入 `Database` 类并创建一个新的数据库实例。你就可以对数据库执行 SQL 查询: ```ts title="main.ts" import { Database } from "@db/sqlite"; const db = new Database("test.db"); const [version] = db.prepare("select sqlite_version()").value<[string]>()!; console.log(version); db.close(); ``` 此模块依赖于 Deno FFI,因此你需要带上 `--allow-ffi` 标志运行你的脚本: ```sh deno run --allow-ffi main.ts ``` ## MySQL 你可以使用 [mysql npm 模块](https://www.npmjs.com/package/mysql) 连接到 MySQL 数据库。用 npm 标识符安装模块: ```sh deno add npm:mysql ``` 然后,导入 `mysql` 模块并创建与你的 MySQL 数据库的连接: ```ts import mysql from "mysql"; // 最简连接配置(根据需要编辑或使用环境变量) const connection = mysql.createConnection({ host: Deno.env.get("MYSQL_HOST") || "localhost", port: Number(Deno.env.get("MYSQL_PORT") || "3306"), user: Deno.env.get("MYSQL_USER") || "root", password: Deno.env.get("MYSQL_PASSWORD") || "", database: Deno.env.get("MYSQL_DATABASE") || "test", }); connection.connect((err) => { if (err) { console.error("连接错误:", err); return; } console.log("已连接!"); connection.query("SELECT VERSION() AS version", (err, results) => { if (err) { console.error("查询错误:", err); } else { console.log("MySQL 版本:", results[0].version); } connection.end(); }); }); ``` ## Postgres PostgreSQL 是一个强大且开源的对象关系型数据库系统。你可以使用多个模块在 Deno 中连接 PostgreSQL,包括 [pg](https://www.npmjs.com/package/pg) 或 [postgresjs](https://www.npmjs.com/package/postgres)。 用 npm 标识符安装模块: ```sh deno add npm:pg ``` 首先,从 `pg` 模块导入 `Client` 类并创建一个新的客户端实例。然后通过传递连接详情的对象连接数据库: ```ts import { Client } from "pg"; // 连接配置(编辑或使用环境变量) const client = new Client({ host: Deno.env.get("PGHOST") || "localhost", port: Number(Deno.env.get("PGPORT") || "5432"), user: Deno.env.get("PGUSER") || "postgres", password: Deno.env.get("PGPASSWORD") || "postgres", database: Deno.env.get("PGDATABASE") || "postgres", }); async function main() { try { await client.connect(); console.log("已连接!"); const res = await client.query("SELECT version() AS version"); console.log("Postgres 版本:", res.rows[0].version); } catch (err) { console.error("连接/查询错误:", err); } finally { await client.end(); } } main(); ``` ## MongoDB MongoDB 是一个流行的 NoSQL 数据库,以灵活的 JSON 类文档格式存储数据。你可以使用官方的 [MongoDB Node.js](https://www.npmjs.com/package/mongodb) 驱动连接 MongoDB, 或者使用来自 JSR 的 [Mongo db driver](https://jsr.io/@db/mongo)。 导入 MongoDB 驱动,设置连接配置,然后连接 MongoDB 实例: ```ts title="main.js" import { MongoClient } from "mongodb"; const url = "mongodb://mongo:mongo@localhost:27017"; // 用户名:密码@主机:端口 const client = new MongoClient(url); const dbName = "myProject"; await client.connect(); console.log("成功连接到服务器"); const db = client.db(dbName); const collection = db.collection("documents"); const insertResult = await collection.insertMany([{ a: 1 }, { a: 2 }]); console.log("插入的文档 =>", insertResult); await client.close(); ``` ## Firebase Firebase 是由 Google 开发的移动和网页应用平台。它提供多种服务,包括 NoSQL 数据库、认证和托管。 连接 Firebase,你可以使用 Firebase 提供的官方 npm 模块,需要更新你的 `deno.json` 告诉 Deno 使用 `node_modules` 目录,并在安装时允许脚本执行: ```json title="deno.json" "nodeModulesDir": auto ``` ```sh deno add npm:firebase --allow-scripts ``` 然后从 Firebase 模块导入所需函数,初始化你的应用和服务: ```js import { initializeApp } from "firebase/app"; import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore"; // 替换为你的 Firebase 配置(从 Firebase 控制台获取) const firebaseConfig = { apiKey: "YOUR_API_KEY", authDomain: "YOUR_PROJECT_ID.firebaseapp.com", projectId: "YOUR_PROJECT_ID", storageBucket: "YOUR_PROJECT_ID.appspot.com", messagingSenderId: "YOUR_MESSAGING_SENDER_ID", appId: "YOUR_APP_ID", }; // 初始化 Firebase const app = initializeApp(firebaseConfig); const db = getFirestore(app); // 示例:写入和读取文档 async function demo() { const ref = doc(db, "demo", "testdoc"); await setDoc(ref, { hello: "world", time: Date.now() }); const snap = await getDoc(ref); console.log("文档数据:", snap.data()); } demo().catch(console.error); ``` ## Supabase Supabase 是一个开源的 Firebase 替代品,提供一套帮助你构建和扩展应用的工具和服务。它提供托管的 PostgreSQL 数据库、认证、实时订阅和存储。 连接 Supabase,你可以使用 [@supabase/supabase-js](https://www.npmjs.com/package/@supabase/supabase-js) npm 模块。 首先,用 npm 标识符安装模块: ```sh deno add npm:@supabase/supabase-js --allow-scripts ``` 然后,从模块导入 `createClient` 函数并创建一个新的 Supabase 客户端实例。你需要你的 Supabase 项目 URL 和 API 密钥,详见项目设置: ```ts import { createClient } from "@supabase/supabase-js"; const url = Deno.env.get("SUPABASE_URL") ?? "https://YOUR-PROJECT.ref.supabase.co"; const key = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""; const supabase = createClient(url, key); async function main() { const { data, error } = await supabase .from("demo") .insert({ message: `Hello @ ${new Date().toISOString()}` }) .select() .maybeSingle(); if (error) { console.error("插入失败:", error.message); console.error( "提示:如果这是 RLS 错误,要么禁用 'demo' 上的 RLS,要么添加允许匿名插入的策略。", ); return; } console.log("插入的行:", data); } if (import.meta.main) main(); ``` ## ORM 对象关系映射 (ORM) 将你的数据模型定义为类,你可以将其实例持久化到数据库。你可以通过这些类的实例读取和写入数据库的数据。 Deno 支持多种 ORM,包括 Prisma、Drizzle 和 Kysely。 🦕 现在您可以将您的 Deno 项目连接到数据库,您将能够处理持久数据,执行 CRUD 操作并开始构建更复杂的应用程序。 --- # Better debugging with the console API > An in-depth guide to advanced console debugging in Deno. Learn about console.table, timers, counters, tracers, and how to leverage the full console API beyond basic logging for better debugging workflows. URL: https://docs.deno.com/examples/debugging_with_console_tutorial/ Some of the console API is probably muscle memory for web developers, but there is so much more than just `console.log()` for you to use. Deno has great support for this API, so whether you’re writing JavaScript for the browser of for the server it’s worth learning about these helpful utilities. Let’s take a look at some of this API’s most useful methods. Your debugging is going to get so much easier! ## `console.log()` Hello, old friend! You’ll most likely be using this to output logging messages to the console to help you debug. ```js console.log("Hello, world!"); // "Hello, world!" ``` You can output multiple items by separated by commas like so: ```jsx const person = { "name": "Jane", "city": "New York" }; console.log("Hello, ", person.name, "from ", person.city); // "Hello, Jane from New York" ``` Or you can use string literals: ```jsx const person = { "name": "Jane", "city": "New York" }; console.log(`Hello ${person.name} from ${person.city}`); // "Hello, Jane from New York" ``` You can also [apply some styling using CSS](/examples/color_logging/) using the `%c` directive: ```jsx console.log("Wild %cblue", "color: blue", "yonder"); // Applies a blue text color to the word "blue" ``` …but there is much more you can do with the console API. ## `console.table()` The `table` method is helpful for outputting structured data like objects for easier inspection. ```jsx const people = { "john": { "age": 30, "city": "New York", }, "jane": { "age": 25, "city": "Los Angeles", }, }; console.table(people); /* ┌───────┬─────┬───────────────┐ │ (idx) │ age │ city │ ├───────┼─────┼───────────────┤ │ john │ 30 │ "New York" │ │ jane │ 25 │ "Los Angeles" │ └───────┴─────┴───────────────┘ */ ``` You can also specify the properties of your object that you’d like to include in the table. Great for inspecting a summary of those detailed objects to see just the part you are concerned with. ```jsx console.table(people, ["city"]); /* outputs ┌───────┬───────────────┐ │ (idx) │ city │ ├───────┼───────────────┤ │ john │ "New York" │ │ jane │ "Los Angeles" │ └───────┴───────────────┘ */ ``` ## Timer methods Understanding how long specific parts of your application take is key to removing performance bottlenecks and expensive operations. If you’ve ever reached for JavaScript’s date method to make yourself a timer, you’ll wish you’d know this one long ago. It’s more convenient and more accurate. Try using[`console.time()`](https://developer.mozilla.org/en-US/docs/Web/API/console/time_static), [`console.timeLog()`](https://developer.mozilla.org/en-US/docs/Web/API/console/timeLog_static), and [`console.timeEnd()`](https://developer.mozilla.org/en-US/docs/Web/API/console/timeEnd_static) instead. ```jsx console.time("My timer"); // starts a timer with label "My timer" // do some work... console.timeLog("My timer"); // outputs the current timer value, e.g. "My timer: 9000ms" // do more work... console.timeEnd("My timer"); // stops "My timer" and reports its value, e.g. "My timer: 97338ms" ``` You can create multiple timers each with their own label. Very handy! ## Counting things with `console.count()` It can be helpful to keep a count of how many times specific operations in your code have been executed. Rather than doing this manually you can use [`console.count()`](https://developer.mozilla.org/en-US/docs/Web/API/console/count_static) which can maintain multiple counters for you based on the label you provide. ```jsx // increment the default counter console.count(); console.count(); console.count(); /* "default: 1" "default: 2" "default: 3" */ ``` This can be very handy inside a function and passing in a label, like so: ```jsx function pat(animal) { console.count(animal); return `Patting the ${animal}`; } pat("cat"); pat("cat"); pat("dog"); pat("cat"); /* "cat: 1" "cat: 2" "dog: 1" "cat: 3" */ ``` ## Going deeper with `console.trace()` For a detailed view of what is happening in your application, you can output a stack trace to the console with [`console.trace()`](https://developer.mozilla.org/en-US/docs/Web/API/console/trace_static): ```jsx // main.js function foo() { function bar() { console.trace(); } bar(); } foo(); /* Trace at bar (file:///PATH_TO/main.js:3:13) at foo (file:///PATH_TO/main.js:5:3) at file:///PATH_TO/main.js:8:1 */ ``` There’s more to explore, but these handy methods can give your JavaScript debugging a boost and they are ready and waiting for you to use right in your browser or in your Deno application. Take a look at [console support](/api/web/~/Console) in the API Reference docs. for more. --- # 使用 Deno Deploy 部署应用 > 一步步指导你将第一个 Deno 应用部署到 Deno Deploy 早期访问。 URL: https://docs.deno.com/examples/deno_deploy_tutorial/ Deno Deploy 允许你在全球边缘网络上托管 Deno 应用,内置遥测和 CI/CD 工具。 本教程将指导你使用 Deno DeployEA 创建并部署一个简单的 Deno 应用。 ## 先决条件 1. 一个 [GitHub](https://github.com) 账号 2. 在本地机器上 [安装 Deno](https://docs.deno.com/runtime/manual/getting_started/installation) 3. 访问 [Deno Deploy 早期访问计划](https://dash.deno.com/account#early-access) ## 用 Vite 创建一个简单的 Deno 应用 首先,使用 Vite 创建一个基本应用,初始化一个新的 [Vite](https://vite.dev/guide/) 项目: ```sh deno init --npm vite ``` 为你的项目命名,选择框架和变体。本教程中,我们将创建一个原生 TypeScript 应用。 使用 `cd my-project-name` 进入新建的项目目录,然后运行: ```sh deno install deno run dev ``` 你应该能在 [http://127.0.0.1:5173/](http://127.0.0.1:5173/) 看到一个基本运行的应用。 编辑 `main.ts` 文件即可在浏览器中查看修改效果。 ## 创建 GitHub 仓库 1. 访问 [GitHub](https://github.com) 并新建一个仓库。 2. 将本地目录初始化为 Git 仓库: ```sh git init git add . git commit -m "Initial commit" ``` 3. 添加 GitHub 仓库为远程仓库并推送代码: ```sh git remote add origin https://github.com/your-username/my-first-deno-app.git git branch -M main git push -u origin main ``` ## 注册 Deno Deploy 早期访问 1. 访问 [Deno Deploy 账户设置](https://dash.deno.com/account#early-access) 2. 点击“加入早期访问计划” 3. 审核通过后,你将收到含有访问说明的邮件 ![早期访问加入截图](./images/join.png) ## 创建 Deno Deploy 组织 1. 访问 [console.deno.com](https://console.deno.com) 2. 点击“+ 新建组织” 3. 选择“标准部署”组织类型 4. 输入组织名称和标识(slug,后续不可更改) 5. 点击“创建标准部署组织” ## 创建并部署应用 1. 从组织仪表盘点击“尝试新 Deno Deploy 早期访问” 2. 点击“+ 新建应用” 3. 选择你之前创建的 GitHub 仓库 4. 应用配置会自动检测,你可以点击“编辑构建配置”按钮确认: - 框架预设:无预设 - 运行时配置:静态站点 - 安装命令:`deno install` - 构建命令:`deno task build` - 静态目录:`dist` 5. 点击“创建应用”开始部署流程 ## 监控你的部署 1. 查看构建日志,了解应用部署进度 2. 部署完成后,你将看到预览链接(通常为 `https://your-app-name.your-org-name.deno.net`) 3. 点击链接浏览已部署的应用! ## 修改并重新部署 现在我们更新应用,并查看变动如何部署: 本地修改你的 `main.ts` 文件: ```ts title="main.ts" import './style.css' import typescriptLogo from './typescript.svg' import viteLogo from '/vite.svg' import { setupCounter } from './counter.ts' document.querySelector('#app')!.innerHTML = `

Hello from Deno Deploy!

点击 Vite 和 TypeScript 标志了解更多

setupCounter(document.querySelector('#counter')!) ``` 2. 提交并推送更改: ```sh git add . git commit -m "Update application" git push ``` 返回 Deno Deploy 仪表盘,你将看到自动触发的新构建。构建完成后访问应用链接查看更新。 ## 探索可观测性功能 Deno DeployEA 提供完整的可观测性工具: 1. 在应用仪表盘点击侧边栏的“日志” - 查看应用输出的控制台日志 - 通过搜索栏过滤日志(例如 `context:production`) 2. 点击“追踪”查看请求追踪 - 选择某个追踪查看详细时间信息 - 审查跨度以理解请求处理流程 3. 点击“指标”查看应用性能指标 - 监控请求次数、错误率和响应时间 🦕 既然你已经部署了第一个应用,接下来你可能会想: 1. 为应用添加 [自定义域名](/deploy/early-access/reference/domains/) 2. 探索对 Next.js、Astro 等框架的 [支持](/deploy/early-access/reference/frameworks/) 3. 学习如何使用 [缓存策略](/deploy/early-access/reference/caching/) 提升性能 4. 为开发和生产环境设置不同的 [环境变量和上下文](/deploy/early-access/reference/env-vars-and-contexts/) --- # 使用 deno doc 生成文档 > 学习如何使用内置的 deno doc 命令为您的 Deno 项目生成专业文档。本教程涵盖 JSDoc 注释、HTML 输出、代码规范检查以及编写文档的最佳实践。 URL: https://docs.deno.com/examples/deno_doc_tutorial/ 良好的文档对任何软件项目都至关重要。它帮助其他开发者理解您的代码,简化维护,提高项目的整体质量。Deno 内置了一个名为 `deno doc` 的文档生成器,可以从您的 TypeScript 和 JavaScript 代码自动生成可搜索的文档。 `deno doc` 开箱即用,无需任何配置,可生成 HTML、JSON 或终端输出。它利用 JSDoc 注释进行文档编写,同时会自动提取代码中 TypeScript 类型注解的信息。 :::info 使用 JSR 自动生成文档 如果您将包发布到 [JSR(JavaScript 注册中心)](https://jsr.io),则可以免费自动获得美观的文档生成!JSR 在底层使用相同的 `deno doc` 技术,为所有发布的包创建可搜索的网页文档。只需通过 `deno publish` 发布带有完善文档的代码,JSR 将为您完成剩余工作。 ::: ## 创建示例项目 让我们创建一个示例库来演示 `deno doc` 的功能。我们将构建一个带有良好文档的简单数学工具库。 ````ts title="math.ts" /** * 一组数学工具函数集合。 * @module */ /** * 将两个数字相加。 * * @example * ```ts * import { add } from "./math.ts"; * * const result = add(5, 3); * console.log(result); // 8 * ``` * * @param x 第一个数字 * @param y 第二个数字 * @returns x 和 y 之和 */ export function add(x: number, y: number): number { return x + y; } /** * 将两个数字相乘。 * * @example * ```ts * import { multiply } from "./math.ts"; * * const result = multiply(4, 3); * console.log(result); // 12 * ``` * * @param x 第一个数字 * @param y 第二个数字 * @returns x 和 y 的乘积 */ export function multiply(x: number, y: number): number { return x * y; } /** * 表示二维空间中的一个点。 * * @example * ```ts * import { Point } from "./math.ts"; * * const point = new Point(5, 10); * console.log(point.distance()); // 11.180339887498949 * ``` */ export class Point { /** * 创建一个新的 Point 实例。 * * @param x x 坐标 * @param y y 坐标 */ constructor(public x: number, public y: number) {} /** * 计算该点到原点 (0, 0) 的距离。 * * @returns 到原点的距离 */ distance(): number { return Math.sqrt(this.x * this.x + this.y * this.y); } /** * 计算该点到另一个点的距离。 * * @param other 另一个点 * @returns 两点之间的距离 */ distanceTo(other: Point): number { const dx = this.x - other.x; const dy = this.y - other.y; return Math.sqrt(dx * dx + dy * dy); } } /** * 数学操作的配置选项。 */ export interface MathConfig { /** 浮点计算的精度 */ precision?: number; /** 是否将结果四舍五入为整数 */ roundToInt?: boolean; } /** * 在配置下执行高级数学运算。 * * @example * ```ts * import { calculate } from "./math.ts"; * * const result = calculate(10, 3, { precision: 2, roundToInt: false }); * console.log(result); // 3.33 * ``` */ export function calculate( dividend: number, divisor: number, config: MathConfig = {}, ): number { const { precision = 10, roundToInt = false } = config; let result = dividend / divisor; if (roundToInt) { result = Math.round(result); } else { result = Math.round(result * Math.pow(10, precision)) / Math.pow(10, precision); } return result; } ```` ## 基础文档生成 生成文档最简单的方式是对源文件运行 `deno doc`: ```bash deno doc math.ts ``` 这会在您的终端输出文档,显示所有导出的函数、类和接口及其 JSDoc 注释。 ## 生成 HTML 格式文档 若要创建包含 HTML、CSS 和 JS 的文档网站,使用 `--html` 标志: ```bash deno doc --html --name="数学工具" math.ts ``` 这将在 `./docs/` 目录生成静态网站。该站点包括: - 可搜索的界面 - 语法高亮 - 符号间的交叉引用 - 移动端友好的响应式设计 您还可以指定自定义输出目录: ```bash deno doc --html --name="数学工具" --output=./documentation/ math.ts ``` ## 文档规范检查 使用 `--lint` 标志检查文档问题: ```bash deno doc --lint math.ts ``` 它会报告多种问题: 1. 导出函数、类或接口缺少 JSDoc 注释 2. 函数缺少返回类型注释 3. 导出符号引用了未导出的类型 我们创建一个带有一些文档问题的文件,来查看 linter 的表现: ```ts title="bad_example.ts" // 缺少 JSDoc 注释 export function badFunction(x) { return x * 2; } interface InternalType { value: string; } // 引用了未导出的类型 export function anotherFunction(param: InternalType) { return param.value; } ``` 运行 `deno doc --lint bad_example.ts` 会显示这些问题的错误。 ## 同时处理多个文件 您可以同时文档多个文件: ```bash deno doc --html --name="我的库" ./mod.ts ./utils.ts ./types.ts ``` 对于较大项目,创建一个重新导出所有内容的主模块文件: ````ts title="mod.ts" /** * 数学工具库 * * 一个完整的数学函数和工具集合。 * * @example * ```ts * import { add, multiply, Point } from "./mod.ts"; * * const sum = add(5, 3); * const product = multiply(4, 2); * const point = new Point(3, 4); * ``` * * @module */ export * from "./math.ts"; ```` 然后从主模块生成文档: ```bash deno doc --html --name="数学工具" mod.ts ``` ## 用于自动化的 JSON 输出 生成 JSON 格式的文档以便与其他工具配合使用: ```bash deno doc --json math.ts > documentation.json ``` JSON 输出提供了代码结构的底层表示,包括符号定义和基本类型信息。此格式主要用于构建自定义文档工具或集成需要程序化访问代码 API 的系统。 ## JSDoc 注释的最佳实践 使用 `deno doc` 时,请遵循以下 JSDoc 最佳实践: ### 1. 使用描述性的摘要 ```ts /** * 使用递归计算一个数字的阶乘。 * * @param n 要计算阶乘的数字 * @returns n 的阶乘 */ export function factorial(n: number): number { // 实现... } ``` ### 2. 提供具体示例 ````ts /** * 将数字格式化为货币形式。 * * @example * ```ts * formatCurrency(123.456); // "$123.46" * formatCurrency(1000); // "$1,000.00" * ``` * * @param amount 要格式化的金额 * @returns 格式化后的货币字符串 */ export function formatCurrency(amount: number): string { // 实现... } ```` ### 3. 注明参数和返回值 ```ts /** * 验证邮箱地址格式。 * * @param email 要验证的邮箱地址 * @returns 若格式有效返回 true,否则返回 false * @throws {Error} 当邮箱为 null 或 undefined 时抛出 */ export function validateEmail(email: string): boolean { // 实现... } ``` ### 4. 使用模块级文档 ```ts /** * 邮箱验证工具。 * * 本模块提供根据 RFC 5322 标准验证和格式化邮箱地址的函数。 * * @module */ ``` ### 5. 标记废弃或实验性功能 ```ts /** * 兼容旧版的遗留函数。 * * @deprecated 请使用 `newFunction()` 替代 * @param data 输入数据 */ export function oldFunction(data: string): void { // 实现... } /** * 新的实验性功能。 * * @experimental 此 API 未来版本可能发生变更 * @param options 配置选项 */ export function experimentalFunction(options: unknown): void { // 实现... } ``` ## 集成至 CI/CD 流程 您可以将文档生成集成到持续集成管道中: ```yaml title=".github/workflows/docs.yml" name: 生成文档 on: push: branches: [main] jobs: docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: 设置 Deno uses: denoland/setup-deno@v2 with: deno-version: v2.x - name: 生成文档 run: deno doc --html --name="我的库" --output=./docs/ mod.ts - name: 部署到 GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs ``` ## 在 deno.json 中配置 您可以在 `deno.json` 文件中配置文档生成: ```json title="deno.json" { "tasks": { "doc": "deno doc --html --name='数学工具' --output=./docs/ mod.ts", "doc:lint": "deno doc --lint mod.ts", "doc:json": "deno doc --json mod.ts > documentation.json" } } ``` 然后轻松运行文档相关任务: ```bash deno task doc deno task doc:lint deno task doc:json ``` 🦕 `deno doc` 命令是一个强大的工具,用于从您的 Deno 项目生成专业文档。 良好的文档让您的代码更易维护,帮助其他开发者有效理解和使用您的项目。使用 `deno doc`,全面的文档仅需一条命令! --- # 使用 deno deploy 命令部署应用 > 使用 deno deploy CLI 命令创建并部署第一个应用到 Deno Deploy 早期访问的分步教程。 URL: https://docs.deno.com/examples/deploy_command_tutorial/ `deno deploy` 命令为在 [Deno Deploy](https://deno.com/deploy) 上部署和管理应用提供了强大的 CLI 工具。 如果你已经有应用准备部署,可以跳转到[部署你的应用](#deploy-your-application),或者继续阅读制作并部署一个简单应用。 ## 前提条件 在使用 deploy 命令之前,你需要获得 Deno Deploy 的访问权限,并且需要一个 Deno Deploy 组织。 1. 访问 [Deno Deploy 账号设置](https://dash.deno.com/account#early-access) 2. 开启“启用早期访问”开关 3. 在 [Deno Deploy 控制台](https://console.deno.com/) 中创建一个 Deno Deploy 组织。 ## 创建一个简单的 Web 应用 首先,我们来创建一个基本的 HTTP 服务器作为我们的应用。 新建一个项目目录并进入: ```bash mkdir my-deploy-app cd my-deploy-app ``` 初始化一个新的 Deno 项目: ```bash deno init ``` 将 `main.ts` 的内容替换为一个简单的 HTTP 服务器: ```ts title="main.ts" Deno.serve({ port: 8000 }, (req) => { const url = new URL(req.url); const userAgent = req.headers.get("user-agent") || "unknown"; const timestamp = new Date().toISOString(); // 记录每个请求 console.log( `[${timestamp}] ${req.method} ${url.pathname} - User-Agent: ${userAgent}`, ); // 简单路由 if (url.pathname === "/") { console.log("Serving home page"); return new Response( ` My Deploy App

欢迎使用 My Deploy App!

此应用使用 deno deploy 命令部署。

`, { headers: { "content-type": "text/html" }, }, ); } if (url.pathname === "/about") { console.log("Serving about page"); return new Response( ` 关于 - My Deploy App

关于此应用

这是一个使用 deno deploy CLI 部署的简单示例。

查看日志以了解请求信息!

← 返回主页 `, { headers: { "content-type": "text/html" }, }, ); } if (url.pathname === "/api/status") { const responseData = { status: "ok", timestamp: new Date().toISOString(), message: "API 正常运行", requestCount: Math.floor(Math.random() * 1000) + 1, // 模拟请求计数器 }; console.log("API 状态检查 - 全系统正常"); console.log(`响应数据:`, responseData); return Response.json(responseData); } if (url.pathname === "/api/error") { // 此端点用于演示错误日志 console.error("访问错误端点 - 演示错误日志"); console.warn("这是将出现在日志中的警告信息"); return Response.json({ error: "这是用于演示的测试错误", timestamp: new Date().toISOString(), tip: "查看日志命令:deno deploy logs", }, { status: 500 }); } // 其它路由返回 404 console.warn(`404 - 未找到路由: ${url.pathname}`); return new Response("未找到", { status: 404 }); }); ``` ### 在本地测试应用 更新根目录下 `deno.json` 文件中的 `dev` 任务,允许网络访问: ```json "dev": "deno run -N --watch main.ts" ``` 然后运行 dev 命令: ```sh deno run dev ``` 访问 `http://localhost:8000` 查看运行中的应用。尝试访问不同路由(`/about`、`/api/status` 和 `/api/error`)以确认功能正常。你会看到每个请求都会被日志记录——这些日志在应用部署后也可以看到! ## 认证 `deno deploy` 命令自动处理认证。首次运行部署命令时会提示你进行认证。运行带有 `--help` 参数的部署命令查看所有可用选项: ```bash deno deploy --help ``` :::note Deno Deploy 组织要求 `deno deploy` 命令需要一个 Deno Deploy 组织。如果你的账号还未设置组织,可以通过 [Deno Deploy Web 应用](https://console.deno.com) 创建一个。 ::: ## 部署你的应用 现在让我们用 `deno deploy` 命令来部署应用!确保你在项目根目录,执行: ```bash deno deploy ``` 根据终端提示选择合适的选项。 部署过程将: 1. 打包你的应用代码为 tarball 2. 上传 tarball 到 Deno Deploy 3. 解包 tarball 4. 构建并部署到边缘网络 5. 返回一个可访问的在线 URL 你已经成功部署了应用!可以访问返回的 URL 查看应用效果。 如果需要修改应用,只需更新代码,然后再次运行 `deno deploy` 命令。 我们演示的应用内置了一些日志,可以通过 Deno Deploy 的日志功能监控应用。 ## 监控你的应用 ### 查看应用日志 部署后,可以实时获取日志流,了解应用具体情况: ```bash deno deploy logs ``` 访问你的应用 URL,浏览不同页面。你会看到类似的日志: - 显示 HTTP 方法、路径和用户代理的请求日志 - 来自 `console.log()` 的信息日志 - 来自 `console.warn()` 的警告日志 - 来自 `console.error()` 的错误日志 在浏览器打开应用 URL,尝试访问 `/api/error` 端点,体验错误日志效果。 ### 查看指定时间范围的日志 想查看特定时间范围的日志,可以使用 `--start` 和 `--end` 参数: ```bash deno deploy logs \ --start "2024-01-01T00:00:00Z" \ --end "2024-01-01T23:59:59Z" ``` ## 管理环境变量 你的应用可能需要环境变量进行配置。`deno deploy` 命令提供了完善的环境变量管理功能。 ### 列出环境变量 查看应用的所有环境变量: ```bash deno deploy env list ``` ### 添加与更新环境变量 添加单个环境变量,使用 `deno deploy env add` 命令,例如: ```bash deno deploy env add API_KEY "your-secret-key" deno deploy env add DATABASE_URL "postgresql://..." ``` 更新环境变量,使用 `deno deploy env update-value` 命令,例如: ```bash deno deploy env update-value API_KEY "new-secret-key" deno deploy env update-value DATABASE_URL "postgresql://new-user:new-pass@localhost/new-db" ``` ### 删除环境变量 删除环境变量,使用 `deno deploy env delete` 命令,例如: ```bash deno deploy env delete API_KEY deno deploy env delete DATABASE_URL ``` ### 从 .env 文件加载环境变量 你也可以使用 `.env` 文件将环境变量加载到部署的应用中: ```bash deno deploy env load .env ``` 🦕 你已成功使用 `deno deploy` 命令部署了第一个应用!更多命令和选项,请查阅 [`deno deploy` 文档](/runtime/reference/cli/deploy/)。 欲了解更多 Deno Deploy 相关信息,请参考 [Deno Deploy 文档](/deploy/)。 --- # 使用 OpenTelemetry 和 Deno Deploy 监控您的应用 > 添加自定义 OpenTelemetry 仪表到您的 Deno Deploy 应用的逐步教程。 URL: https://docs.deno.com/examples/deploy_otel_tutorial/ Deno DeployEA 内置了 OpenTelemetry 支持,能够自动捕获 HTTP 请求、数据库查询和其他操作的追踪信息。本教程展示如何为您的应用添加自定义 OpenTelemetry 仪表,以实现更详细的可观测性。 ## 先决条件 1. 一个 [GitHub](https://github.com) 账号 2. 在本地机器上安装 [Deno](https://docs.deno.com/runtime/manual/getting_started/installation) 3. 访问 [Deno Deploy 早期体验计划](https://dash.deno.com/account#early-access) 4. 基本了解 [OpenTelemetry 概念](https://opentelemetry.io/docs/concepts/) ## 创建一个基础 API 应用 首先,让我们创建一个简单的 API 服务器,稍后将使用 OpenTelemetry 对其进行仪表: ```ts title="main.ts" const dataStore: Record = {}; async function handler(req: Request): Promise { const url = new URL(req.url); // 模拟随机延迟 await new Promise((resolve) => setTimeout(resolve, Math.random() * 200)); try { // 处理产品列表 if (url.pathname === "/products" && req.method === "GET") { return new Response(JSON.stringify(Object.values(dataStore)), { headers: { "Content-Type": "application/json" }, }); } // 处理产品创建 if (url.pathname === "/products" && req.method === "POST") { const data = await req.json(); const id = crypto.randomUUID(); dataStore[id] = data; return new Response(JSON.stringify({ id, ...data }), { status: 201, headers: { "Content-Type": "application/json" }, }); } // 按 ID 获取产品 if (url.pathname.startsWith("/products/") && req.method === "GET") { const id = url.pathname.split("/")[2]; const product = dataStore[id]; if (!product) { return new Response("Product not found", { status: 404 }); } return new Response(JSON.stringify(product), { headers: { "Content-Type": "application/json" }, }); } // 处理根路由 if (url.pathname === "/") { return new Response("Product API - Try /products endpoint"); } return new Response("Not Found", { status: 404 }); } catch (error) { console.error("Error handling request:", error); return new Response("Internal Server Error", { status: 500 }); } } console.log("Server running on http://localhost:8000"); Deno.serve(handler, { port: 8000 }); ``` 保存此文件并在本地运行: ```sh deno run --allow-net main.ts ``` 使用 curl 或浏览器测试 API,以确保其正常工作: ```sh # 列出产品(初始为空) curl http://localhost:8000/products # 添加一个产品 curl -X POST http://localhost:8000/products \ -H "Content-Type: application/json" \ -d '{"name": "Test Product", "price": 19.99}' ``` ## 添加 OpenTelemetry 仪表 现在,给我们的应用添加自定义的 OpenTelemetry 仪表。创建一个新文件 `instrumented-main.ts`: ```ts title="instrumented-main.ts" import { trace } from "npm:@opentelemetry/api@1"; // 获取 OpenTelemetry tracer const tracer = trace.getTracer("product-api"); const dataStore: Record = {}; // 模拟数据库操作并创建自定义 span async function queryDatabase( operation: string, data?: unknown, ): Promise { return await tracer.startActiveSpan(`database.${operation}`, async (span) => { try { // 为 span 添加属性以便提供更好的上下文 span.setAttributes({ "db.system": "memory-store", "db.operation": operation, }); // 模拟数据库延迟 const delay = Math.random() * 100; await new Promise((resolve) => setTimeout(resolve, delay)); // 将延迟信息添加到 span 属性 span.setAttributes({ "db.latency_ms": delay }); if (operation === "list") { return Object.values(dataStore); } else if (operation === "get") { return dataStore[data as string]; } else if (operation === "insert") { const id = crypto.randomUUID(); dataStore[id] = data as string; return { id, data }; } return null; } catch (error) { // 记录任何错误到 span span.recordException(error); span.setStatus({ code: trace.SpanStatusCode.ERROR }); throw error; } finally { // 结束 span span.end(); } }); } async function handler(req: Request): Promise { // 为整个请求创建父 span return await tracer.startActiveSpan( `${req.method} ${new URL(req.url).pathname}`, async (parentSpan) => { const url = new URL(req.url); // 为 span 添加请求详情属性 parentSpan.setAttributes({ "http.method": req.method, "http.url": req.url, "http.route": url.pathname, }); try { // 处理产品列表 if (url.pathname === "/products" && req.method === "GET") { const products = await queryDatabase("list"); return new Response(JSON.stringify(products), { headers: { "Content-Type": "application/json" }, }); } // 处理产品创建 if (url.pathname === "/products" && req.method === "POST") { // 创建一个解析请求 JSON 的 span const data = await tracer.startActiveSpan( "parse.request.body", async (span) => { try { const result = await req.json(); return result; } catch (error) { span.recordException(error); span.setStatus({ code: trace.SpanStatusCode.ERROR }); throw error; } finally { span.end(); } }, ); const result = await queryDatabase("insert", data); return new Response(JSON.stringify(result), { status: 201, headers: { "Content-Type": "application/json" }, }); } // 根据 ID 获取产品 if (url.pathname.startsWith("/products/") && req.method === "GET") { const id = url.pathname.split("/")[2]; parentSpan.setAttributes({ "product.id": id }); const product = await queryDatabase("get", id); if (!product) { parentSpan.setAttributes({ "error": true, "error.type": "not_found", }); return new Response("Product not found", { status: 404 }); } return new Response(JSON.stringify(product), { headers: { "Content-Type": "application/json" }, }); } // 处理根路由 if (url.pathname === "/") { return new Response("Product API - Try /products endpoint"); } parentSpan.setAttributes({ "error": true, "error.type": "not_found" }); return new Response("Not Found", { status: 404 }); } catch (error) { console.error("Error handling request:", error); // 在 span 中记录错误 parentSpan.recordException(error); parentSpan.setAttributes({ "error": true, "error.type": error.name, "error.message": error.message, }); parentSpan.setStatus({ code: trace.SpanStatusCode.ERROR }); return new Response("Internal Server Error", { status: 500 }); } finally { // 结束父 span parentSpan.end(); } }, ); } console.log( "Server running with OpenTelemetry instrumentation on http://localhost:8000", ); Deno.serve(handler, { port: 8000 }); ``` 在本地运行带有仪表的版本: ```sh deno run --allow-net instrumented-main.ts ``` 再次使用 curl 测试 API 以生成一些追踪。 ## 创建 GitHub 仓库 1. 访问 [GitHub](https://github.com) 并创建一个新仓库。 2. 初始化本地目录为 Git 仓库: ```sh git init git add . git commit -m "Add OpenTelemetry instrumented API" ``` 3. 添加 GitHub 仓库作为远程仓库并推送代码: ```sh git remote add origin https://github.com/your-username/otel-demo-app.git git branch -M main git push -u origin main ``` ## 部署到 Deno Deploy 早期体验 1. 访问 [console.deno.com](https://console.deno.com) 2. 选择您的组织,或根据需要新建一个 3. 点击 "+ New App" 4. 选择您之前创建的 GitHub 仓库 5. 配置构建设置: - 框架预设:无预设 - 运行时配置:动态 - 入口文件:`instrumented-main.ts` 6. 点击 "Create App" 开始部署过程 ## 生成示例流量 为了生成示例追踪和指标,让我们向部署的应用发送一些流量: 1. 从 Deno Deploy 控制台复制您的部署 URL 2. 发送多个请求到不同的端点: ```sh # 将应用 URL 存入变量 APP_URL=https://your-app-name.your-org-name.deno.net # 访问根路由 curl $APP_URL/ # 列出产品(初始为空) curl $APP_URL/products # 添加一些产品 curl -X POST $APP_URL/products -H "Content-Type: application/json" -d '{"name": "Laptop", "price": 999.99}' curl -X POST $APP_URL/products -H "Content-Type: application/json" -d '{"name": "Headphones", "price": 129.99}' curl -X POST $APP_URL/products -H "Content-Type: application/json" -d '{"name": "Mouse", "price": 59.99}' # 再次列出产品 curl $APP_URL/products # 尝试访问一个不存在的产品(将生成错误 span) curl $APP_URL/products/nonexistent-id ``` ## 探索 OpenTelemetry 追踪和指标 现在,让我们探索 Deno Deploy 收集的可观测性数据: 1. 在应用仪表盘,点击侧边栏的 "Traces" - 您将看到针对每个请求的追踪列表 - 可以使用搜索栏按 HTTP 方法或状态码过滤追踪 2. 选择一个 `/products` 的 POST 追踪以查看详细信息: - 整个请求的父 span - 数据库操作的子 span - 解析请求体的 span ![追踪瀑布图视图](./images/early-access/otel_trace.png) 3. 点击单个 span 查看详情: - 持续时间和时间信息 - 您设置的属性,如 `db.operation` 和 `db.latency_ms` - 任何已记录的异常 4. 点击侧边栏的 "Logs" 查看带有追踪上下文的控制台输出: - 注意追踪期间发出的日志如何被自动关联到追踪 - 点击日志行的 "View trace" 可查看关联的追踪 5. 点击 "Metrics" 查看应用性能指标: - 按端点的 HTTP 请求计数 - 错误率 - 响应时间分布 🦕 Deno DeployEA 的自动仪表结合您的自定义仪表,为您的应用性能和行为提供了全面的可视化。 更多关于 Deno 中 OpenTelemetry 的信息,请参考以下资源: - [Deno 中的 OpenTelemetry 文档](/runtime/fundamentals/open_telemetry/) - [Deno DeployEA 可观测性参考](/deploy/early-access/reference/observability/) - [OpenTelemetry 官方文档](https://opentelemetry.io/docs/) --- # 如何将 Deno 部署到 Digital Ocean > 在 Digital Ocean 上部署 Deno 应用的逐步指南。了解 Docker 容器化、GitHub Actions 自动化、容器注册表以及如何设置持续部署工作流。 URL: https://docs.deno.com/examples/digital_ocean_tutorial/ Digital Ocean 是一个受欢迎的云基础设施提供商,提供多种托管服务,从网络到计算再到存储。 以下是将 Deno 应用程序通过 Docker 和 GitHub Actions 部署到 Digital Ocean 的逐步指南。 此过程的先决条件包括: - [`docker` CLI](https://docs.docker.com/engine/reference/commandline/cli/) - 一个 [GitHub 帐户](https://github.com) - 一个 [Digital Ocean 帐户](https://digitalocean.com) - [`doctl` CLI](https://docs.digitalocean.com/reference/doctl/how-to/install/) ## 创建 Dockerfile 和 docker-compose.yml 为了专注于部署,我们的应用程序仅为一个返回 HTTP 响应字符串的 `main.ts` 文件: ```ts title="main.ts" import { Application } from "jsr:@oak/oak"; const app = new Application(); app.use((ctx) => { ctx.response.body = "Hello from Deno and Digital Ocean!"; }); await app.listen({ port: 8000 }); ``` 接下来,我们将创建两个文件 -- `Dockerfile` 和 `docker-compose.yml` -- 来构建 Docker 镜像。 在我们的 `Dockerfile` 中,添加如下内容: ```Dockerfile title="Dockerfile" FROM denoland/deno EXPOSE 8000 WORKDIR /app ADD . /app RUN deno install --entrypoint main.ts CMD ["run", "--allow-net", "main.ts"] ``` 然后,在我们的 `docker-compose.yml` 中: ```yml version: "3" services: web: build: . container_name: deno-container image: deno-image ports: - "8000:8000" ``` 让我们通过运行 `docker compose -f docker-compose.yml build`,然后 `docker compose up`,并访问 `localhost:8000` 来测试这个应用程序。 ![Hello from localhost](./images/how-to/digital-ocean/hello-world-from-localhost.png) 它工作正常! ## 构建、标记并将 Docker 镜像推送到 Digital Ocean 容器注册表 Digital Ocean 有自己的私有容器注册表,我们可以在其中推送和拉取 Docker 镜像。为了使用该注册表,让我们 [在命令行中安装并认证 `doctl`](https://docs.digitalocean.com/reference/doctl/how-to/install/)。 之后,我们将创建一个名为 `deno-on-digital-ocean` 的新私有注册表: ```shell doctl registry create deno-on-digital-ocean ``` 使用我们的 Dockerfile 和 docker-compose.yml,我们将构建一个新镜像,标记它,并将其推送到注册表。请注意,`docker-compose.yml` 将在本地将构建命名为 `deno-image`。 ```shell docker compose -f docker-compose.yml build ``` 让我们 [标记](https://docs.docker.com/engine/reference/commandline/tag/) 它为 `new`: ```shell docker tag deno-image registry.digitalocean.com/deno-on-digital-ocean/deno-image:new ``` 在推送之前,请对 Docker 客户端进行 Digital Ocean 容器注册表的认证: ```shell doctl registry login ``` 现在我们可以将其推送到注册表。 ```shell docker push registry.digitalocean.com/deno-on-digital-ocean/deno-image:new ``` 你应该在你的 [Digital Ocean 容器注册表](https://cloud.digitalocean.com/registry) 中看到新的带有 `new` 标签的 `deno-image`: ![New deno image on Digital Ocean container registry](./images/how-to/digital-ocean/new-deno-image-on-digital-ocean-container-registry.png) 完美! ## 通过 SSH 部署到 Digital Ocean 一旦我们的 `deno-image` 在注册表中,我们可以使用 `docker run` 在任何地方运行它。在这种情况下,我们将在我们的 [Digital Ocean Droplet](https://www.digitalocean.com/products/droplets) 上运行,那里是他们托管的虚拟机。 在你的 [Droplet 页面](https://cloud.digitalocean.com/droplets) 上,点击你的 Droplet,然后点击 `console` 通过 SSH 进入虚拟机。(或者你可以从你的命令行 [直接 ssh](https://docs.digitalocean.com/products/droplets/how-to/connect-with-ssh/))。 要拉取 `deno-image` 镜像并运行它,我们可以运行: ```shell docker run -d --restart always -it -p 8000:8000 --name deno-image registry.digitalocean.com/deno-on-digital-ocean/deno-image:new ``` 使用我们的浏览器访问 Digital Ocean 地址,我们现在看到: ![Hello from Deno and Digital Ocean](./images/how-to/digital-ocean/hello-from-deno-and-digital-ocean.png) 太棒了! ## 通过 GitHub Actions 自动化部署 让我们通过 GitHub Actions 自动化整个过程。 首先,让我们获取所有需要的环境变量,以便登录到 `doctl` 和 SSH 进入 Droplet: - [DIGITALOCEAN_ACCESS_TOKEN](https://docs.digitalocean.com/reference/api/create-personal-access-token/) - DIGITALOCEAN_HOST(你的 Droplet 的 IP 地址) - DIGITALOCEAN_USERNAME(默认是 `root`) - DIGITALOCEAN_SSHKEY(关于这一点,稍后会详细说明) ### 生成 `DIGITALOCEAN_SSHKEY` `DIGITALOCEAN_SSHKEY` 是一个私钥,其公钥位于虚拟机的 `~/.ssh/authorized_keys` 文件中。 为此,首先在你的本地机器上运行 `ssh-keygen`: ```shell ssh-keygen ``` 当出现提示输入电子邮件时,**确保使用你的 GitHub 电子邮件** 以便 GitHub Action 正确验证。最终输出应该类似于: ```console Output Your identification has been saved in /your_home/.ssh/id_rsa Your public key has been saved in /your_home/.ssh/id_rsa.pub The key fingerprint is: SHA256:/hk7MJ5n5aiqdfTVUZr+2Qt+qCiS7BIm5Iv0dxrc3ks user@host The key's randomart image is: +---[RSA 3072]----+ | .| | + | | + | | . o . | |o S . o | | + o. .oo. .. .o| |o = oooooEo+ ...o| |.. o *o+=.*+o....| | =+=ooB=o.... | +----[SHA256]-----+ ``` 接下来,我们需要将新生成的公钥上传到你的 Droplet。你可以使用 [`ssh-copy-id`](https://www.ssh.com/academy/ssh/copy-id) 或手动复制它,SSH 进入你的 Droplet,并将其粘贴到 `~/.ssh/authorized_keys`。 使用 `ssh-copy-id`: ```shell ssh-copy-id {{ username }}@{{ host }} ``` 这个命令会提示你输入密码。请注意,这将自动从你的本地机器复制 `id_rsa.pub` 密钥并粘贴到你的 Droplet 的 `~/.ssh/authorized_keys` 文件中。如果你将密钥命名为其他名称,可以通过 `-i` 标志将其传递给命令: ```shell ssh-copy-id -i ~/.ssh/mykey {{ username }}@{{ host }} ``` 要测试是否成功执行: ```shell ssh -i ~/.ssh/mykey {{ username }}@{{ host }} ``` 太好了! ### 定义 yml 文件 最后一步是将这一切结合在一起。我们基本上是在手动部署的每一步中,将其添加到一个 GitHub Actions 工作流的 yml 文件中: ```yml name: Deploy to Digital Ocean on: push: branches: - main env: REGISTRY: "registry.digitalocean.com/deno-on-digital-ocean" IMAGE_NAME: "deno-image" jobs: build_and_push: name: Build, Push, and Deploy runs-on: ubuntu-latest steps: - name: Checkout main uses: actions/checkout@v4 - name: Set $TAG from shortened sha run: echo "TAG=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV - name: Build container image run: docker compose -f docker-compose.yml build - name: Tag container image run: docker tag ${{ env.IMAGE_NAME }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }} - name: Install `doctl` uses: digitalocean/action-doctl@v2 with: token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - name: Log in to Digital Ocean Container Registry run: doctl registry login --expiry-seconds 600 - name: Push image to Digital Ocean Container Registry run: docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }} - name: Deploy via SSH uses: appleboy/ssh-action@master with: host: ${{ secrets.DIGITALOCEAN_HOST }} username: ${{ secrets.DIGITALOCEAN_USERNAME }} key: ${{ secrets.DIGITALOCEAN_SSHKEY }} script: | # 登录到 Digital Ocean 容器注册表 docker login -u ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} -p ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} registry.digitalocean.com # 停止并删除正在运行的镜像 docker stop ${{ env.IMAGE_NAME }} docker rm ${{ env.IMAGE_NAME }} # 从新镜像运行一个新容器 docker run -d --restart always -it -p 8000:8000 --name ${{ env.IMAGE_NAME }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }} ``` 当你推送到 GitHub 时,这个 yml 文件会被自动检测,从而触发部署动作。 --- # 使用 Drizzle ORM 和 Deno 构建数据库应用 > Step-by-step guide to building database applications with Drizzle ORM and Deno. Learn about schema management, type-safe queries, PostgreSQL integration, migrations, and how to implement CRUD operations. URL: https://docs.deno.com/examples/drizzle_tutorial/ [Drizzle ORM](https://orm.drizzle.team/) 是一个 TypeScript ORM,提供了一种类型安全的方式与数据库进行交互。在本教程中,我们将设置 Drizzle ORM 与 Deno 和 PostgreSQL,以便创建、读取、更新和删除恐龙数据: - [安装 Drizzle](#install-drizzle) - [配置 Drizzle](#configure-drizzle) - [定义模式](#define-schemas) - [与数据库交互](#interact-with-the-database) - [接下来做什么?](#whats-next) 您可以在[this GitHub repo](https://github.com/denoland/examples/tree/main/with-drizzle)中找到本教程的所有代码。 ## 安装 Drizzle 首先,我们将使用 Deno 的 npm 兼容性安装所需的依赖项。我们将与 [Postgres](https://orm.drizzle.team/docs/get-started-postgresql) 一起使用 Drizzle,但您也可以使用 [MySQL](https://orm.drizzle.team/docs/get-started-mysql) 或 [SQLite](https://orm.drizzle.team/docs/get-started-sqlite) 。(如果您没有 PostgreSQL,可以在 [这里安装](https://www.postgresql.org/download/)。) ```bash deno install npm:drizzle-orm npm:drizzle-kit npm:pg npm:@types/pg ``` 这将安装 Drizzle ORM 及其相关工具——用于模式迁移的 [drizzle-kit](https://orm.drizzle.team/docs/kit-overview),用于 PostgreSQL 连接的 [pg](https://www.npmjs.com/package/pg),以及 PostgreSQL 的 [TypeScript 类型](https://www.npmjs.com/package/@types/pg)。这些包将允许我们以类型安全的方式与数据库交互,同时保持与 Deno 的运行环境的兼容性。 它还将在您的项目根目录中创建一个 `deno.json` 文件以管理 npm 依赖项: ```json { "imports": { "@types/pg": "npm:@types/pg@^8.11.10", "drizzle-kit": "npm:drizzle-kit@^0.27.2", "drizzle-orm": "npm:drizzle-orm@^0.36.0", "pg": "npm:pg@^8.13.1" } } ``` ## 配置 Drizzle 接下来,让我们在项目根目录中创建一个 `drizzle.config.ts` 文件。此文件将配置 Drizzle 以与您的 PostgreSQL 数据库配合使用: ```tsx import { defineConfig } from "drizzle-kit"; export default defineConfig({ out: "./drizzle", schema: "./src/db/schema.ts", dialect: "postgresql", dbCredentials: { url: Deno.env.get("DATABASE_URL")!, }, }); ``` 这些配置设置决定: - 迁移文件的输出位置(`./drizzle`) - 查找模式定义的位置(`./src/db/schema.ts`) - PostgreSQL 作为您的数据库方言, - 如何使用存储在环境变量中的 URL 连接到您的数据库 `drizzle-kit` 将使用此配置管理您的数据库模式并自动生成 SQL 迁移。 我们还需要在项目根目录中添加一个 `.env` 文件,其中包含 `DATABASE_URL` 连接字符串: ```bash DATABASE_URL=postgresql://[user[:password]@][host][:port]/[dbname] ``` 确保将登录凭据替换为您自己的。 接下来,让我们连接到数据库,并使用 Drizzle 填充我们的表。 ## 定义模式 使用 Drizzle 定义表模式有两种方法。如果您已经定义了 Postgres 表,您可以使用 `pull` 推断它们;否则,您可以在代码中定义它们,然后使用 Drizzle 创建新表。我们将在下面探讨这两种方法。 ### 使用 `pull` 推断模式 如果您在添加 Drizzle 之前已经有 Postgres 表,则可以 introspect 您的数据库模式,以使用命令 [`npm:drizzle-kit pull`](https://orm.drizzle.team/docs/drizzle-kit-pull) 自动生成 TypeScript 类型和表定义。这在处理现有数据库时特别有用,或者当您希望确保代码与数据库结构保持同步时。 假设我们当前的数据库已经具有以下表模式: ![Postgres 中表模式的图示](./images/how-to/drizzle/table-diagram.png) 我们将运行以下命令以 introspect 数据库并在 `./drizzle` 目录下填充多个文件:
```bash deno --env -A --node-modules-dir npm:drizzle-kit pull Failed to find Response internal state key No config path provided, using default 'drizzle.config.ts' Reading config file '/private/tmp/deno-drizzle-example/drizzle.config.ts' Pulling from ['public'] list of schemas Using 'pg' driver for database querying [✓] 2 tables fetched [✓] 8 columns fetched [✓] 0 enums fetched [✓] 0 indexes fetched [✓] 1 foreign keys fetched [✓] 0 policies fetched [✓] 0 check constraints fetched [✓] 0 views fetched [i] No SQL generated, you already have migrations in project [✓] Your schema file is ready ➜ drizzle/schema.ts 🚀 [✓] Your relations file is ready ➜ drizzle/relations.ts 🚀 ```
我们使用 --env 标志来读取包含我们数据库 URL 的 .env 文件,以及 --node-modules-dir 标志来创建一个 node_modules 文件夹,使我们能够正确使用 drizzle-kit

上述命令将在 `./drizzle` 目录中创建一些文件,这些文件定义了模式、跟踪更改,并提供了进行数据库迁移所需的信息: - `drizzle/schema.ts`:此文件使用 Drizzle ORM 的模式定义语法定义数据库模式。 - `drizzle/relations.ts`:此文件用于定义使用 Drizzle ORM 的关系 API 的表之间的关系。 - `drizzle/0000_long_veda.sql`:一个 SQL 迁移文件,其中包含创建数据库表的 SQL 代码。该代码被注释掉 — 如果要运行此迁移以在新环境中创建表,可以取消注释该代码。 - `drizzle/meta/0000_snapshot.json`:一个快照文件,表示您数据库模式的当前状态。 - `drizzle/meta/_journal.json`:此文件跟踪已应用于数据库的迁移。它帮助 Drizzle ORM 知道哪些迁移已运行,哪些仍需应用。 ### 首先在 Drizzle 中定义模式 如果您还没有在 Postgres 中定义任何现有表(例如,您正在开始一个全新的项目),则可以在代码中定义表和类型,并让 Drizzle 创建它们。 让我们创建一个新的目录 `./src/db/`,并在其中创建一个 `schema.ts` 文件,填入以下内容:
```ts // schema.ts import { boolean, foreignKey, integer, pgTable, serial, text, timestamp, } from "drizzle-orm/pg-core"; export const dinosaurs = pgTable("dinosaurs", { id: serial().primaryKey().notNull(), name: text(), description: text(), }); export const tasks = pgTable("tasks", { id: serial().primaryKey().notNull(), dinosaurId: integer("dinosaur_id"), description: text(), dateCreated: timestamp("date_created", { mode: "string" }).defaultNow(), isComplete: boolean("is_complete"), }, (table) => { return { tasksDinosaurIdFkey: foreignKey({ columns: [table.dinosaurId], foreignColumns: [dinosaurs.id], name: "tasks_dinosaur_id_fkey", }), }; }); ```
上述代码表示两个表 dinosaurstasks 及其关系。了解有关使用 Drizzle 定义模式及其关系的更多信息

定义完 `./src/db/schema.ts` 后,我们可以通过创建迁移来创建表和指定的关系: ```bash deno -A --node-modules-dir npm:drizzle-kit generate Failed to find Response internal state key No config path provided, using default 'drizzle.config.ts' Reading config file '/private/tmp/drizzle/drizzle.config.ts' 2 tables dinosaurs 3 columns 0 indexes 0 fks tasks 5 columns 0 indexes 1 fks ``` 上述命令将创建一个包含迁移脚本和日志的 `./drizzle/` 文件夹。 ## 与数据库交互 现在我们已经设置了 Drizzle ORM,可以使用它来简化在 Postgres 数据库中管理数据。首先,Drizzle 建议将 `schema.ts` 和 `relations.ts` 复制到 `./src/db` 目录中,以便在应用程序中使用。 让我们创建一个 `./src/db/db.ts` 文件,导出一些助手函数,使我们更容易与数据库交互: ```ts import { drizzle } from "drizzle-orm/node-postgres"; import { dinosaurs as dinosaurSchema, tasks as taskSchema } from "./schema.ts"; import { dinosaursRelations, tasksRelations } from "./relations.ts"; import pg from "pg"; import { integer } from "drizzle-orm/sqlite-core"; import { eq } from "drizzle-orm/expressions"; // 使用 pg 驱动程序。 const { Pool } = pg; // 使用 pg 驱动程序和模式实例化 Drizzle 客户端。 export const db = drizzle({ client: new Pool({ connectionString: Deno.env.get("DATABASE_URL"), }), schema: { dinosaurSchema, taskSchema, dinosaursRelations, tasksRelations }, }); // 插入恐龙。 export async function insertDinosaur(dinosaurObj: typeof dinosaurSchema) { return await db.insert(dinosaurSchema).values(dinosaurObj); } // 插入任务。 export async function insertTask(taskObj: typeof taskSchema) { return await db.insert(taskSchema).values(taskObj); } // 按 id 查找恐龙。 export async function findDinosaurById(dinosaurId: typeof integer) { return await db.select().from(dinosaurSchema).where( eq(dinosaurSchema.id, dinosaurId), ); } // 按名称查找恐龙。 export async function findDinosaurByName(name: string) { return await db.select().from(dinosaurSchema).where( eq(dinosaurSchema.name, name), ); } // 根据恐龙 id 查找任务。 export async function findDinosaurTasksByDinosaurId( dinosaurId: typeof integer, ) { return await db.select().from(taskSchema).where( eq(taskSchema.dinosaurId, dinosaurId), ); } // 更新恐龙。 export async function updateDinosaur(dinosaurObj: typeof dinosaurSchema) { return await db.update(dinosaurSchema).set(dinosaurObj).where( eq(dinosaurSchema.id, dinosaurObj.id), ); } // 更新任务。 export async function updateTask(taskObj: typeof taskSchema) { return await db.update(taskSchema).set(taskObj).where( eq(taskSchema.id, taskObj.id), ); } // 按 id 删除恐龙。 export async function deleteDinosaurById(id: typeof integer) { return await db.delete(dinosaurSchema).where( eq(dinosaurSchema.id, id), ); } // 按 id 删除任务。 export async function deleteTask(id: typeof integer) { return await db.delete(taskSchema).where(eq(taskSchema.id, id)); } ``` 现在我们可以将其中一些助手函数导入到一个脚本中,在其中对我们的数据库执行一些简单的 CRUD 操作。让我们创建一个新文件 `./src/script.ts`: ```ts import { deleteDinosaurById, findDinosaurByName, insertDinosaur, insertTask, updateDinosaur, } from "./db/db.ts"; // 创建一个新的恐龙。 await insertDinosaur({ name: "Denosaur", description: "Dinosaurs should be simple.", }); // 按名称查找该恐龙。 const res = await findDinosaurByName("Denosaur"); // 根据该恐龙的 id 创建一个任务。 await insertTask({ dinosaurId: res.id, description: "Remove unnecessary config.", isComplete: false, }); // 使用新描述更新恐龙。 const newDeno = { id: res.id, name: "Denosaur", description: "The simplest dinosaur.", }; await updateDinosaur(newDeno); // 删除恐龙(及其可能存在的任何任务)。 await deleteDinosaurById(res.id); ``` 我们可以运行它并在数据库上执行所有操作: ```ts deno -A --env ./src/script.ts ``` ## 接下来做什么? Drizzle ORM 是一个流行的数据映射工具,简化了管理和维护数据模型以及与数据库的工作。希望本教程能为您如何在 Deno 项目中使用 Drizzle 提供一个起点。 现在您对如何在 Deno 中使用 Drizzle ORM 有了基本的了解,您可以: 1. 添加更复杂的数据库关系 2. [实现一个 REST API](https://docs.deno.com/examples/) 使用 [Hono](https://jsr.io/@hono/hono) 提供您的恐龙数据 3. 为您的数据库操作添加验证和错误处理 4. 为您的数据库交互编写测试 5. [将您的应用程序部署到云端](https://docs.deno.com/runtime/tutorials/#deploying-deno-projects) 🦕 祝您在 Deno 和 Drizzle ORM 上编码愉快!这种堆栈的类型安全性和简单性使其成为构建现代 Web 应用的绝佳选择。 --- # 如何在 Deno 中使用 Express > Step-by-step guide to using Express.js with Deno. Learn how to set up an Express server, configure routes, handle middleware, and build REST APIs using Deno's Node.js compatibility features. URL: https://docs.deno.com/examples/express_tutorial/ [Express](https://expressjs.com/) 是一个流行的 web 框架,以简单和无特定意见著称,拥有庞大的中间件生态系统。 本指南将展示如何使用 Express 和 Deno 创建一个简单的 API。 [查看源代码。](https://github.com/denoland/tutorial-with-express) ## 初始化一个新的 Deno 项目 在命令行中运行以下命令以创建一个新的启动项目,然后导航到项目目录中: ```sh deno init my-express-project cd my-express-project ``` ## 安装 Express 要安装 Express,我们将使用 `npm:` 模块说明符。这个说明符允许我们从 npm 导入模块: ```sh deno add npm:express ``` 这将把最新的 `express` 包添加到 `deno.json` 文件中的 `imports` 字段。现在你可以在代码中使用 `import express from "express";` 导入 `express`。 ## 更新 `main.ts` 在 `main.ts` 中,让我们创建一个简单的服务器: ```ts import express from "express"; const app = express(); app.get("/", (req, res) => { res.send("欢迎来到恐龙 API!"); }); app.listen(8000); console.log(`服务器正在运行在 http://localhost:8000`); ``` 你可能会注意到编辑器对 `req` 和 `res` 参数发出警告。这是因为 Deno 没有为 `express` 模块提供类型。为了解决这个问题,你可以直接从 npm 导入 Express 类型文件。在 `main.ts` 的顶部添加以下注释: ```ts // @ts-types="npm:@types/express@4.17.15" ``` 这个注释告诉 Deno 使用 `@types/express` 包中的类型。 ## 运行服务器 当你初始化项目时,Deno 设置了一个任务来运行 `main.ts` 文件,你可以在 `deno.json` 文件中看到它。更新 `dev` 任务以包含 [`--allow-net`](/runtime/fundamentals/security/#network-access) 标志: ```jsonc { "scripts": { "dev": "deno run --allow-net main.ts" }, ... } ``` 这将允许项目进行网络请求。你可以 [阅读更多关于权限标志的信息](/runtime/fundamentals/security/)。 现在你可以使用以下命令运行服务器: ```sh deno run dev ``` 如果你在浏览器中访问 `localhost:8000`,你应该看到: **欢迎来到恐龙 API!** ## 添加数据和路由 接下来的步骤是添加一些数据。我们将使用来自 [这篇文章](https://www.thoughtco.com/dinosaurs-a-to-z-1093748) 的恐龙数据。随意 [从这里复制它](https://raw.githubusercontent.com/denoland/tutorial-with-express/refs/heads/main/data.json)。 在项目根目录中创建一个 `data.json` 文件,并粘贴恐龙数据。 接下来,我们将把这些数据导入到 `main.ts` 中: ```ts import data from "./data.json" with { type: "json" }; ``` 我们将创建访问这些数据的路由。 为了简单起见,我们只为 `/api/` 和 `/api/:dinosaur` 定义 `GET` 处理程序。在 `const app = express();` 这一行后添加以下代码: ```ts app.get("/", (req, res) => { res.send("欢迎来到恐龙 API!"); }); app.get("/api", (req, res) => { res.send(data); }); app.get("/api/:dinosaur", (req, res) => { if (req?.params?.dinosaur) { const found = data.find((item) => item.name.toLowerCase() === req.params.dinosaur.toLowerCase() ); if (found) { res.send(found); } else { res.send("未找到恐龙。"); } } }); app.listen(8000); console.log(`服务器正在运行在 http://localhost:8000`); ``` 让我们使用 `deno run dev` 运行服务器,并在浏览器中查看 `localhost:8000/api`。你应该会看到一列恐龙! ```jsonc [ { "name": "Aardonyx", "description": "长颈龙演化早期阶段。" }, { "name": "Abelisaurus", "description": "\"阿贝尔的蜥蜴\" 是由单个头骨重建的。" }, { "name": "Abrictosaurus", "description": "异齿龙的早期亲属。" }, ... ``` 你还可以通过访问 "/api/恐龙名称" 来获取特定恐龙的详细信息,例如 `localhost:8000/api/aardonyx` 将显示: ```json { "name": "Aardonyx", "description": "长颈龙演化早期阶段。" } ``` 🦕 现在你已准备好在 Deno 中使用 Express。你可以考虑把这个示例扩展成一个恐龙网页应用。或者查看 [Deno 内置的 HTTP 服务器](https://docs.deno.com/runtime/fundamentals/http_server/)。 --- # 获取和流数据 > A tutorial on working with network requests in Deno. Learn how to use the fetch API for HTTP requests, handle responses, implement data streaming, and manage file uploads and downloads. URL: https://docs.deno.com/examples/fetch_data_tutorial/ Deno 将几个熟悉的 Web API 引入到服务器环境中。如果您曾经使用过浏览器,您可能会认识 [`fetch()`](/api/web/fetch) 方法和 [`streams`](/api/web/streams) API,它们用于进行网络请求和访问网络上的数据流。Deno 实现了这些 API,使您能够从网络中获取和流式传输数据。 ## 获取数据 在构建 Web 应用程序时,开发人员通常需要从 Web 的其他地方检索资源。我们可以使用 `fetch` API 来实现。我们将看看如何从 URL 获取不同形状的数据,以及如果请求失败时如何处理错误。 创建一个名为 `fetch.js` 的新文件,并添加以下代码: ```ts title="fetch.js" // 输出:JSON 数据 const jsonResponse = await fetch("https://api.github.com/users/denoland"); const jsonData = await jsonResponse.json(); console.log(jsonData, "\n"); // 输出:HTML 数据 const textResponse = await fetch("https://deno.land/"); const textData = await textResponse.text(); console.log(textData, "\n"); // 输出:错误信息 try { await fetch("https://does.not.exist/"); } catch (error) { console.log(error); } ``` 您可以使用 `deno run` 命令运行此代码。因为它正在跨网络获取数据,您需要授予 `--allow-net` 权限: ```sh deno run --allow-net fetch.js ``` 您应该在控制台看到 JSON 数据、作为文本的 HTML 数据以及一条错误信息。 ## 流式传输数据 有时您可能希望通过网络发送或接收大文件。当您不知道文件的大小时,流式传输是处理数据的更有效方法。客户端可以从流中读取数据,直到它说它完成。 Deno 提供了一种使用 `Streams API` 进行数据流式传输的方法。我们将看看如何将文件转换为可读或可写的流,以及如何使用流来发送和接收文件。 创建一个名为 `stream.js` 的新文件。 我们将使用 `fetch` API 来检索一个文件。然后我们将使用 [`Deno.open`](/api/deno/Deno.open) 方法来创建和打开一个可写文件,并使用 Streams API 的 [`pipeTo`](/api/web/~/ReadableStream.pipeTo) 方法将字节流发送到创建的文件。 接下来,我们将使用 `readable` 属性在 `POST` 请求中将文件的字节流发送到服务器。 ```ts title="stream.js" // 接收文件 const fileResponse = await fetch("https://deno.land/logo.svg"); if (fileResponse.body) { const file = await Deno.open("./logo.svg", { write: true, create: true }); await fileResponse.body.pipeTo(file.writable); } // 发送文件 const file = await Deno.open("./logo.svg", { read: true }); await fetch("https://example.com/", { method: "POST", body: file.readable, }); ``` 您可以使用 `deno run` 命令运行此代码。因为它正在从网络获取数据并写入文件,所以您需要授予 `--allow-net`、`--allow-write` 和 `--allow-read` 权限: ```sh deno run --allow-read --allow-write --allow-net stream.js ``` 您应该会看到文件 `logo.svg` 在当前目录中创建并填充,而如果您拥有 example.com,您会看到文件被发送到服务器。 🦕 现在您知道如何在网络上获取和流式传输数据,以及如何将数据流式传输到文件和从文件中流式传输数据!无论您是提供静态文件、处理上传、生成动态内容还是流式传输大型数据集,Deno 的文件处理和流式传输能力都是您开发工具箱中的绝佳工具! --- # 基于文件的路由 > Tutorial on implementing file-based routing in Deno. Learn how to create a dynamic routing system similar to Next.js, handle HTTP methods, manage nested routes, and build a flexible server architecture. URL: https://docs.deno.com/examples/file_based_routing_tutorial/ 如果您使用过像 [Next.js](https://nextjs.org/) 这样的框架,您可能对基于文件的路由已不陌生 - 您在特定目录中添加一个文件,它会自动成为一个路由。本教程演示如何创建一个使用基于文件的路由的简单 HTTP 服务器。 ## 路由请求 创建一个名为 `server.ts` 的新文件。这个文件将用于路由请求。 设置一个名为 `handler` 的异步函数,接受一个请求对象作为参数: ```ts title="server.ts" async function handler(req: Request): Promise { const url = new URL(req.url); const path = url.pathname; const method = req.method; let module; try { module = await import(`.${path}.ts`); } catch (_error) { return new Response("未找到", { status: 404 }); } if (module[method]) { return module[method](req); } return new Response("未实现的方法", { status: 501 }); } Deno.serve(handler); ``` `handler` 函数设置了一个路径变量,其中包含从请求 URL 中提取的路径,以及一个方法变量,包含请求方法。 接下来尝试根据路径导入一个模块。如果未找到模块,则返回404响应。 如果找到了模块,它会检查该模块是否有请求方法的处理程序。如果找到了方法处理程序,它将使用请求对象调用该方法处理程序。如果未找到方法处理程序,则返回501响应。 最后,它使用 `Deno.serve` 提供处理程序函数。 > 路径可以是任何有效的 URL 路径,例如 `/users`、`/posts` 等。对于像 `/users` 这样的路径,将导入 `./users.ts` 文件。然而,像 `/org/users` 这样的更深路径将需要 `./org/users.ts` 文件。您可以通过创建嵌套目录和文件来创建嵌套路由。 ## 处理请求 在与 `server.ts` 相同的目录中创建一个名为 `users.ts` 的新文件。这个文件将用于处理对 `/users` 路径的请求。我们将使用 `GET` 请求作为示例。您可以添加更多的 HTTP 方法,如 `POST`、`PUT`、`DELETE` 等。 在 `users.ts` 中,设置一个名为 `GET` 的异步函数,接受一个请求对象作为参数: ```ts title="users.ts" export function GET(_req: Request): Response { return new Response("来自 user.ts 的问候", { status: 200 }); } ``` ## 启动服务器 要启动服务器,请运行以下命令: ```sh deno run --allow-net --allow-read server.ts ``` 这将在 `localhost:8080` 上启动服务器。您现在可以向 `localhost:8000/users` 发出 `GET` 请求,您应该会看到响应 `来自 user.ts 的问候`。 此命令需要 `--allow-net` 和 `--allow-read` [权限标志](/runtime/fundamentals/security/),以允许访问网络以启动服务器并从文件系统中读取 `users.ts` 文件。 🦕 现在您可以基于文件结构在您的应用程序中设置路由。您可以根据需要扩展此示例以添加更多路由和方法。 感谢 [@naishe](https://github.com/naishe) 贡献此教程。 --- # 编写一个文件服务器 > Tutorial on building a file server with Deno. Learn how to handle HTTP requests, serve static files, implement streaming responses, and use the standard library's file server module for production deployments. URL: https://docs.deno.com/examples/file_server_tutorial/ 一个文件服务器监听传入的HTTP请求,并从本地文件系统提供文件。这个教程演示了如何使用Deno内置的 [文件系统API](/api/deno/file-system) 创建一个简单的文件服务器。 ## 编写一个简单的文件服务器 首先,创建一个新的文件,命名为 `file-server.ts`。 我们将使用Deno内置的 [HTTP服务器](/api/deno/~/Deno.serve) 来监听传入的请求。在你的新 `file-server.ts` 文件中,添加以下代码: ```ts title="file-server.ts" Deno.serve( { hostname: "localhost", port: 8080 }, async (request) => { const url = new URL(request.url); const filepath = decodeURIComponent(url.pathname); }, ); ``` > 如果你不熟悉 `URL` 对象,可以在 [URL API](https://developer.mozilla.org/en-US/docs/Web/API/URL) 文档中了解更多。 > [decodeURIComponent函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent) 用于解码URL编码的路径,以防字符被百分号编码。 ### 打开文件并流式传输其内容 当接收到请求时,我们将尝试使用 [`Deno.open`](/api/deno/~/Deno.open) 打开请求URL中指定的文件。 如果请求的文件存在,我们将其转换为可读的数据流,使用 [ReadableStream API](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream),并将其内容流式传输到响应中。我们不知道请求的文件有多大,因此流式传输可以在服务大型文件或同时处理多个请求时防止内存问题。 如果文件不存在,我们将返回 "404 Not Found" 响应。 在请求处理程序的主体中,在这两个变量的下面,添加以下代码: ```ts try { const file = await Deno.open("." + filepath, { read: true }); return new Response(file.readable); } catch { return new Response("404 Not Found", { status: 404 }); } ``` ### 运行文件服务器 使用 `deno run` 命令运行你的新文件服务器,允许读取访问和网络访问: ```shell deno run --allow-read=. --allow-net file-server.ts ``` ## 使用Deno标准库提供的文件服务器 从头编写文件服务器是理解Deno的HTTP服务器如何工作的一个很好的练习。然而,从零开始编写生产级文件服务器可能会很复杂且容易出错。使用经过测试和可靠的解决方案更好。 Deno标准库为你提供了一个 [文件服务器](https://jsr.io/@std/http/doc/file-server/~),这样你就不必自己编写。 要使用它,首先将远程脚本安装到本地文件系统: ```shell # Deno 1.x deno install --allow-net --allow-read jsr:@std/http/file-server # Deno 2.x deno install --global --allow-net --allow-read jsr:@std/http/file-server ``` > 这将把脚本安装到Deno安装根目录中,例如 `/home/user/.deno/bin/file-server`。 你现在可以使用简化的脚本名称运行该脚本: ```shell $ file-server . Listening on: - Local: http://0.0.0.0:8000 ``` 要查看文件服务器可用的完整选项列表,请运行 `file-server --help`。 如果你在网页浏览器中访问 [http://0.0.0.0:8000/](http://0.0.0.0:8000/),你将看到本地目录的内容。 ### 在Deno项目中使用 @std/http 文件服务器 要在 [Deno项目](/runtime/getting_started/first_project) 中使用文件服务器,你可以在 `deno.json` 文件中添加它: ```sh deno add jsr:@std/http ``` 然后在你的项目中导入它: ```ts title="file-server.ts" import { serveDir } from "@std/http/file-server"; Deno.serve((req) => { const pathname = new URL(req.url).pathname; if (pathname.startsWith("/static")) { return serveDir(req, { fsRoot: "path/to/static/files/dir", }); } return new Response(); }); ``` 这段代码将使用 `Deno.serve` 设置一个HTTP服务器。当请求到来时,它会检查请求的路径是否以 “/static” 开头。如果是,则从指定目录服务文件。否则,它会返回一个空响应。 🦕 现在你知道如何编写自己的简单文件服务器,以及如何使用Deno标准库提供的文件服务器工具。你可以处理各种任务 - 无论是服务静态文件、处理上传、转换数据还是管理访问控制 - 你都准备好使用Deno服务文件了。 --- # 文件系统事件 > Tutorial on monitoring file system changes with Deno. Learn how to watch directories for file modifications, handle change events, and understand platform-specific behaviors across Linux, macOS, and Windows. URL: https://docs.deno.com/examples/file_system_events_tutorial/ ## 概念 - 使用 [Deno.watchFs](https://docs.deno.com/api/deno/~/Deno.watchFs) 来监视文件系统事件。 - 结果可能会因操作系统而异。 ## 示例 要在当前目录中轮询文件系统事件: ```ts title="watcher.ts" const watcher = Deno.watchFs("."); for await (const event of watcher) { console.log(">>>> event", event); // 示例事件: { kind: "create", paths: [ "/home/alice/deno/foo.txt" ] } } ``` 运行命令: ```shell deno run --allow-read watcher.ts ``` 现在尝试在与 `watcher.ts` 相同的目录中添加、删除和修改文件。 请注意,事件的具体顺序可能会因操作系统而异。此功能根据平台使用不同的系统调用: - Linux: [inotify](https://man7.org/linux/man-pages/man7/inotify.7.html) - macOS: [FSEvents](https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/FSEvents_ProgGuide/Introduction/Introduction.html) - Windows: [ReadDirectoryChangesW](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw) --- # 构建一个 Fresh 应用 > 使用 Fresh 和 Deno 构建全栈应用的完整指南。学习如何设置项目、使用岛屿架构实现服务器端渲染、添加 API 路由,以及部署你的 TypeScript 应用。 URL: https://docs.deno.com/examples/fresh_tutorial/ [Fresh](https://fresh.deno.dev/) 是一个面向 Deno 的全栈 Web 框架,强调使用岛屿交互的服务器端渲染。它默认不向客户端发送任何 JavaScript,使其运行极其快速且高效。Fresh 采用基于文件的路由系统,并利用 Deno 现代运行时的能力。 在本教程中,我们将构建一个简单的恐龙目录应用,演示 Fresh 的关键特性。该应用将展示恐龙列表,允许你查看单个恐龙的详细信息,并使用 Fresh 的岛屿架构添加交互组件。 你可以查看 [GitHub 上的完整应用代码仓库](https://github.com/denoland/tutorial-with-fresh) 和 [Deno Deploy 上的应用演示](https://tutorial-with-fresh.deno.deno.net/)。 :::info 部署你自己的应用 想跳过教程,立即部署完成的应用吗?点击下面的按钮,即可将完整的 Fresh 恐龙应用即时部署到 Deno Deploy。你将获得一个可实时运行的应用,可以在学习过程中自定义和修改! [![Deploy on Deno](https://deno.com/button)](https://console.deno.com/new?clone=https://github.com/denoland/tutorial-with-fresh) ::: ## 创建 Fresh 项目 Fresh 提供了便捷的脚手架工具来创建新项目。在你的终端中运行以下命令: ```sh deno run -Ar jsr:@fresh/init ``` 此命令将会: - 下载最新的 Fresh 脚手架脚本 - 创建一个名为 `my-fresh-app` 的新目录 - 设置基础的 Fresh 项目结构 - 安装所有必需的依赖 进入新项目目录: ```sh cd my-fresh-app ``` 启动开发服务器: ```sh deno task dev ``` 打开浏览器,访问 `http://localhost:5173`,即可看到你的 Fresh 应用运行起来了! ## 了解项目结构 项目包含以下关键目录和文件: ```text my-fresh-app/ ├── assets/ # 静态资源(图片、CSS 等) ├── components/ # 可重用的 UI 组件 ├── islands/ # 交互式组件(岛屿) ├── routes/ # 基于文件的路由 │ └── api/ # API 路由 ├── static/ # 静态资源(图片、CSS 等) ├── main.ts # 应用入口文件 ├── deno.json # Deno 配置文件 └── README.md # 项目文档 ``` ## 添加恐龙数据 为了给应用添加恐龙数据,我们将创建一个简单的数据文件,里面包含一些恐龙的 JSON 信息。在真实应用中,这些数据可能来自数据库或外部 API,但为了简单起见,我们使用静态文件。 在 `routes/api` 目录下创建一个新文件 `data.json`,并复制这里的内容: [链接](https://github.com/denoland/tutorial-with-fresh/blob/main/routes/api/data.json)。 ## 显示恐龙列表 主页将显示一个可点击的恐龙列表,用户点击后可查看详细信息。我们更新 `routes/index.tsx` 文件来获取并展示恐龙数据。 首先将文件头部的 `` 改为 "Dinosaur Encyclopedia"。然后添加一些基本的 HTML 来介绍应用。 ```tsx title="index.tsx" <main> <h1>🦕 Welcome to the Dinosaur Encyclopedia</h1> <p>Click on a dinosaur below to learn more.</p> <div class="dinosaur-list"> {/* Dinosaur list will go here */} </div> </main>; ``` 我们将创建一个新组件,用于展示列表中的每个恐龙。 ## 创建组件 在 `components/LinkButton.tsx` 文件中创建以下代码: ```tsx title="LinkButton.tsx" import type { ComponentChildren } from "preact"; export interface LinkButtonProps { href?: string; class?: string; children?: ComponentChildren; } export function LinkButton(props: LinkButtonProps) { return ( <a {...props} class={"btn " + (props.class ?? "")} /> ); } ``` 该组件渲染一个看起来像按钮的样式化链接,接收 `href`、`class` 和 `children` 属性。 最后,更新 `routes/index.tsx`,导入并使用新建的 `LinkButton` 组件来显示恐龙列表。 ```tsx title="index.tsx" import { Head } from "fresh/runtime"; import { define } from "../utils.ts"; import data from "./api/data.json" with { type: "json" }; import { LinkButton } from "../components/LinkButton.tsx"; export default define.page(function Home() { return ( <> <Head> <title>Dinosaur Encyclopedia

🦕 Welcome to the Dinosaur Encyclopedia

Click on a dinosaur below to learn more.

{data.map((dinosaur: { name: string; description: string }) => ( {dinosaur.name} ))}
); }); ``` ## 创建动态路由 Fresh 允许我们通过基于文件的路由创建动态路由。我们将创建一个新路由来显示单个恐龙的详细信息。 在 `routes/dinosaurs/[name].tsx` 文件中,根据参数名获取恐龙数据并展示。 ```tsx title="[dinosaur].tsx" import { PageProps } from "$fresh/server.ts"; import data from "../api/data.json" with { type: "json" }; import { LinkButton } from "../../components/LinkButton.tsx"; export default function DinosaurPage(props: PageProps) { const name = props.params.dinosaur; const dinosaur = data.find((d: { name: string }) => d.name.toLowerCase() === name.toLowerCase() ); if (!dinosaur) { return (

Dinosaur not found

); } return (

{dinosaur.name}

{dinosaur.description}

← Back to list
); } ``` ## 使用岛屿添加交互 Fresh 的岛屿架构允许我们给特定组件添加交互,而不向客户端发送多余的 JavaScript。我们来创建一个简单的交互组件,允许用户“收藏”某只恐龙。 在 `islands/FavoriteButton.tsx` 文件中添加以下代码: ```tsx title="FavoriteButton.tsx" import { useState } from "preact/hooks"; export default function FavoriteButton() { const [favorited, setFavorited] = useState(false); return ( ); } ``` 它是一个简单按钮,点击时切换收藏状态。你也可以扩展它,将收藏状态存储到数据库或本地存储,实现更完整的功能。 接着,在 `routes/dinosaurs/[dinosaur].tsx` 顶部导入该 `FavoriteButton` 岛屿: ```tsx title="[dinosaur].tsx" import FavoriteButton from "../../islands/FavoriteButton.tsx"; ``` 然后在 JSX 中添加 `` 组件,比如放在返回列表按钮前面: ```tsx title="[dinosaur].tsx" ; ``` ## 应用样式 我们已经为应用准备了一些基础样式,但你也可以在 `assets/styles.css` 文件中添加自定义 CSS。在 `routes/_app.tsx` 的 `` 中添加链接引用我们的样式表: ```tsx title="_app.tsx" ; ``` ## 运行应用 确认你的开发服务器正在运行: ```sh deno task dev ``` 打开浏览器访问 `http://localhost:5173`,查看你的恐龙目录应用!你应该可以查看恐龙列表,点击任意一项查看细节,并能通过“收藏”按钮切换收藏状态。 ## 构建与部署 默认的 Fresh 应用附带了一个使用 Vite 构建应用的 `build` 任务。你可以通过以下命令来构建生产版本: ```sh deno run build ``` 该命令会将优化后的文件输出到 `_fresh` 目录。 要运行已构建的应用,可以使用 `start` 任务,它会自动加载 `_fresh` 中的优化资源: ```sh deno task start ``` 打开浏览器,访问 `http://localhost:8000`,查看生产环境的应用。 你可以将此应用部署到你喜欢的云服务商。我们推荐使用 [Deno Deploy](https://deno.com/deploy) 进行简单快速的部署。你只需将代码推送到 GitHub,然后与 Deno Deploy 连接即可。 ### 创建 GitHub 仓库 [创建一个新的 GitHub 仓库](https://github.com/new),然后初始化并推送你的应用代码: ```sh git init -b main git remote add origin https://github.com//.git git add . git commit -am 'my fresh app' git push -u origin main ``` ### 部署到 Deno Deploy 代码在 GitHub 后,可以 [部署到 Deno DeployEA](https://console.deno.com/)。 如果想要部署教程,可以参考 [Deno Deploy 教程](/examples/deno_deploy_tutorial/)。 🦕 现在你拥有了一个基础的 Fresh 应用!这里有一些扩展恐龙目录的建议: - 添加数据库(尝试 [Deno KV](https://docs.deno.com/runtime/fundamentals/kv/) 或连接到 [PostgreSQL](https://docs.deno.com/runtime/tutorials/connecting_to_databases/)) - 实现用户身份验证 - 增加更多交互功能如收藏或评分 - 连接外部 API 获取更多恐龙数据 Fresh 架构让你轻松构建快速、可扩展的 Web 应用,同时保持良好的开发体验。默认的服务器端渲染结合可选的客户端交互,为你提供了两者的最佳结合。 --- # 如何部署到 Google Cloud Run > Step-by-step guide to deploying Deno applications on Google Cloud Run. Learn about Docker containerization, Artifact Registry configuration, GitHub Actions automation, and how to set up continuous deployment to Google Cloud. URL: https://docs.deno.com/examples/google_cloud_run_tutorial/ [Google Cloud Run](https://cloud.google.com/run) 是一个托管计算平台,允许您在 Google 可扩展的基础设施上运行容器。 本如何做指南将向您展示如何使用 Docker 将您的 Deno 应用程序部署到 Google Cloud Run。 首先,我们将向您展示如何手动部署,然后我们将展示如何使用 GitHub Actions 自动化部署。 先决条件: - [Google Cloud Platform 账户](https://cloud.google.com/gcp) - 已安装 [`docker` CLI](https://docs.docker.com/engine/reference/commandline/cli/) - 已安装 [`gcloud`](https://cloud.google.com/sdk/gcloud) ## 手动部署 ### 创建 `Dockerfile` 和 `docker-compose.yml` 为了集中关注部署,我们的应用程序将简单地为一个返回字符串的 `main.ts` 文件作为 HTTP 响应: ```ts title="main.ts" import { Application } from "jsr:@oak/oak"; const app = new Application(); app.use((ctx) => { ctx.response.body = "Hello from Deno and Google Cloud Run!"; }); await app.listen({ port: 8000 }); ``` 然后,我们将创建两个文件——`Dockerfile` 和 `docker-compose.yml`——用于构建 Docker 镜像。 在我们的 `Dockerfile` 中,添加: ```Dockerfile FROM denoland/deno EXPOSE 8000 WORKDIR /app ADD . /app RUN deno install --entrypoint main.ts CMD ["run", "--allow-net", "main.ts"] ``` 然后,在我们的 `docker-compose.yml` 中: ```yml version: "3" services: web: build: . container_name: deno-container image: deno-image ports: - "8000:8000" ``` 我们通过运行 `docker compose -f docker-compose.yml build` 接着 `docker compose up`,并访问 `localhost:8000` 来进行本地测试。 ![Hello from localhost](./images/how-to/google-cloud-run/hello-world-from-localhost.png) 它成功了! ### 设置 Artifact Registry Artifact Registry 是 GCP 的 Docker 镜像私有注册中心。 在我们可以使用它之前,请访问 GCP 的 [Artifact Registry](https://console.cloud.google.com/artifacts) 并点击 "创建存储库"。您将被要求输入一个名称(`deno-repository`)和区域(`us-central1`)。然后点击 "创建"。 ![New repository in Google Artifact Repository](./images/how-to/google-cloud-run/new-repository-in-google-artifact-repository.png) ### 构建、标记并推送到 Artifact Registry 一旦我们创建了一个存储库,我们就可以开始向其推送镜像。 首先,让我们将注册表地址添加到 `gcloud`: ```shell gcloud auth configure-docker us-central1-docker.pkg.dev ``` 然后,让我们构建您的 Docker 镜像。(请注意,镜像名称在我们的 `docker-compose.yml` 文件中定义。) ```shell docker compose -f docker-compose.yml build ``` 然后,用新的 Google Artifact Registry 地址、存储库和名称标记它。镜像名称应遵循以下结构: `{{ location }}-docker.pkg.dev/{{ google_cloudrun_project_name }}/{{ repository }}/{{ image }}`。 ```shell docker tag deno-image us-central1-docker.pkg.dev/deno-app-368305/deno-repository/deno-cloudrun-image ``` 如果不指定标签,它将默认使用 `:latest`。 接下来,推送镜像: ```shell docker push us-central1-docker.pkg.dev/deno-app-368305/deno-repository/deno-cloudrun-image ``` _[有关如何推送和拉取镜像到 Google Artifact Registry 的更多信息](https://cloud.google.com/artifact-registry/docs/docker/pushing-and-pulling)。_ 您的镜像现在应该出现在您的 Google Artifact Registry 中! ![Image in Google Artifact Registry](./images/how-to/google-cloud-run/image-in-google-artifact-registry.png) ### 创建 Google Cloud Run 服务 我们需要一个实例来构建这些镜像,因此让我们访问 [Google Cloud Run](https://console.cloud.google.com/run) 并点击 "创建服务"。 让我们将其命名为 "hello-from-deno"。 选择 "从现有容器镜像部署一个修订版本"。使用下拉菜单选择来自 `deno-repository` Artifact Registry 的镜像。 选择 "允许未经身份验证的请求",然后点击 "创建服务"。确保端口为 `8000`。 完成后,您的应用程序现在应该是在线的: ![Hello from Google Cloud Run](./images/how-to/google-cloud-run/hello-from-google-cloud-run.png) 太棒了! ### 使用 `gcloud` 部署 现在它已经创建,我们将能够从 `gcloud` CLI 部署到此服务。命令的结构如下: `gcloud run deploy {{ service_name }} --image={{ image }} --region={{ region }} --allow-unauthenticated`。 请注意,`image` 名称遵循上面的结构。 在本示例中,命令为: ```shell gcloud run deploy hello-from-deno --image=us-central1-docker.pkg.dev/deno-app-368305/deno-repository/deno-cloudrun-image --region=us-central1 --allow-unauthenticated ``` ![Hello from Google Cloud Run](./images/how-to/google-cloud-run/hello-from-google-cloud-run.png) 成功! ## 使用 GitHub Actions 自动化部署 为了使自动化工作,我们首先需要确保这两个已经创建: - Google Artifact Registry - Google Cloud Run 服务实例 (如果您还没有做到这一点,请参见之前的部分。) 现在我们完成了,可以通过 GitHub 工作流自动化部署。以下是 yaml 文件: ```yml name: Build and Deploy to Cloud Run on: push: branches: - main env: PROJECT_ID: { { PROJECT_ID } } GAR_LOCATION: { { GAR_LOCATION } } REPOSITORY: { { GAR_REPOSITORY } } SERVICE: { { SERVICE } } REGION: { { REGION } } jobs: deploy: name: Deploy permissions: contents: "read" id-token: "write" runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Google Auth id: auth uses: "google-github-actions/auth@v0" with: credentials_json: "${{ secrets.GCP_CREDENTIALS }}" - name: Login to GAR uses: docker/login-action@v2.1.0 with: registry: ${{ env.GAR_LOCATION }}-docker.pkg.dev username: _json_key password: ${{ secrets.GCP_CREDENTIALS }} - name: Build and Push Container run: |- docker build -t "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}" ./ docker push "${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}" - name: Deploy to Cloud Run id: deploy uses: google-github-actions/deploy-cloudrun@v0 with: service: ${{ env.SERVICE }} region: ${{ env.REGION }} image: ${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }} - name: Show Output run: echo ${{ steps.deploy.outputs.url }} ``` 我们需要设置的环境变量是(括号中的示例是本存储库的): - `PROJECT_ID`: 您的项目 ID(`deno-app-368305`) - `GAR_LOCATION`: 您的 Google Artifact Registry 的位置(`us-central1`) - `GAR_REPOSITORY`: 您为 Google Artifact Registry 指定的名称(`deno-repository`) - `SERVICE`: Google Cloud Run 服务的名称(`hello-from-deno`) - `REGION`: 您的 Google Cloud Run 服务的区域(`us-central1`) 我们需要设置的秘密变量是: - `GCP_CREDENTIALS`: 这是 [服务账户](https://cloud.google.com/iam/docs/service-accounts) 的 json 密钥。创建服务账户时,请确保 [包括必要的角色和权限](https://cloud.google.com/iam/docs/granting-changing-revoking-access#granting_access_to_a_user_for_a_service_account) 以便用于 Artifact Registry 和 Google Cloud Run。 [查看有关从 GitHub Actions 部署到 Cloud Run 的更多详细信息和示例。](https://github.com/google-github-actions/deploy-cloudrun) 供参考: https://github.com/google-github-actions/example-workflows/blob/main/workflows/deploy-cloudrun/cloudrun-docker.yml --- # How to export telemetry data to Grafana > Complete guide to exporting telemetry data with OpenTelemetry and Grafana. Learn how to configure collectors, visualize traces, and monitor application performance. URL: https://docs.deno.com/examples/grafana_tutorial/ [OpenTelemetry](https://opentelemetry.io/) (often abbreviated as OTel) is an open-source observability framework that provides a standardized way to collect and export telemetry data such as traces, metrics and logs. Deno has built-in support for OpenTelemetry, making it easy to instrument your applications without adding external dependencies. This integration works out of the box with observability platforms like [Grafana](https://grafana.com/). Grafana is an open-source observability platform that lets DevOps teams visualize, query, and alert on metrics, logs, and traces from diverse data sources in real time. It’s widely used for building dashboards to monitor infrastructure, applications, and systems health. Grafana also offers a hosted version called [Grafana Cloud](https://grafana.com/products/cloud/). This tutorial will help you configure your project to export OTel data to Grafana Cloud. In this tutorial, we'll build a simple application and export its telemetry data to Grafana Cloud. We'll cover: - [Set up your chat app](#set-up-your-chat-app) - [Set up a Docker collector](#set-up-a-docker-collector) - [Generating telemetry data](#generating-telemetry-data) - [Viewing telemetry data](#viewing-telemetry-data) You can find the complete source code for this tutorial [on GitHub](https://github.com/denoland/examples/tree/main/with-grafana). ## Set up your chat app For this tutorial, we'll use a simple chat application to demonstrate how to export telemetry data. You can find the [code for the app on GitHub](https://github.com/denoland/examples/tree/main/with-grafana). Either take a copy of that repository or create a [main.ts](https://github.com/denoland/examples/blob/main/with-grafana/main.ts) file and a [.env](https://github.com/denoland/examples/blob/main/with-grafana/.env.example) file. In order to run the app you will need an OpenAI API key. You can get one by signing up for an account at [OpenAI](https://platform.openai.com/signup) and creating a new secret key. You can find your API key in the [API keys section](https://platform.openai.com/account/api-keys) of your OpenAI account. Once you have an API key, set up an `OPENAI_API-KEY` environment variable in your `.env` file: ```env title=".env" OPENAI_API_KEY=your_openai_api_key ``` ## Set up a Docker collector Next, we'll set up a Docker container to run the OpenTelemetry collector. The collector is responsible for receiving telemetry data from your application and exporting it to Grafana Cloud. In the same directory as your `main.ts` file, create a `Dockerfile` and an `otel-collector.yml` file. The `Dockerfile` will be used to build a Docker image: ```dockerfile title="Dockerfile" FROM otel/opentelemetry-collector-contrib:latest COPY otel-collector.yml /otel-config.yml CMD ["--config", "/otel-config.yml"] ``` [`FROM otel/opentelemetry-collector-contrib:latest`](https://hub.docker.com/r/otel/opentelemetry-collector-contrib/) - This line specifies the base image for the container. It uses the official OpenTelemetry Collector Contributor image, which contains all receivers, exporters, processors, connectors, and other optional components, and pulls the latest version. `COPY otel-collector.yml /otel-config.yml` - This instruction copies our configuration file named `otel-collector.yml` from the local build context into the container. The file is renamed to `/otel-config.yml` inside the container. `CMD ["--config", "/otel-config.yml"]` - This sets the default command that will run when the container starts. It tells the OpenTelemetry Collector to use the configuration file we copied in the previous step. Next, let's setup a Grafana Cloud account and grab some info. If you have not already, [create a free Grafana Cloud account](https://grafana.com/auth/sign-up/create-user). Once created, you will receive a Grafana Cloud stack. Click "Details". ![Click details on your Grafana Cloud stack](./images/how-to/grafana/grafana-1.png) Next, find "OpenTelemetry" and click "Configure". ![Find and configure OpenTelemetry](./images/how-to/grafana/grafana-2.png) This page will provide you with all the details you'll need to configure your OpenTelemetry collector. Make note of your **OTLP Endpoint**, **Instance ID**, and **Password / API Token** (you will have to generate one). ![Configuring OTel in Grafana Cloud](./images/how-to/grafana/grafana-3.png) Next, add the following to your `otel-collector.yml` file to define how how telemetry data should be collected and exported to Grafana Cloud: ```yml title="otel-collector.yml" receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 exporters: otlphttp/grafana_cloud: endpoint: $_YOUR_GRAFANA_OTLP_ENDPOINT auth: authenticator: basicauth/grafana_cloud extensions: basicauth/grafana_cloud: client_auth: username: $_YOUR_INSTANCE_ID password: $_YOUR_API_TOKEN processors: batch: service: extensions: [basicauth/grafana_cloud] pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlphttp/grafana_cloud] metrics: receivers: [otlp] processors: [batch] exporters: [otlphttp/grafana_cloud] logs: receivers: [otlp] processors: [batch] exporters: [otlphttp/grafana_cloud] ``` The `receivers` section configures how the collector receives data. It sets up an OTLP (OpenTelemetry Protocol) receiver that listens on two protocols, `gRPC` and `HTTP`, the `0.0.0.0` address means it will accept data from any source. The `exporters` section defines where the collected data should be sent. Be sure to include **the OTLP endpoint** provided by your Grafana Cloud instance. The `extensions` section defines the authentication for OTel to export data to Grafana Cloud. Be sure to include your Grafana Cloud **Instance ID**, as well as your generated **Password / API Token**. The `processors` section defines how the data should be processed before export. It uses batch processing with a timeout of 5 seconds and a maximum batch size of 5000 items. The `service` section ties everything together by defining three pipelines. Each pipeline is responsible for a different type of telemetry data. The logs pipeline collects application logs. The traces pipeline is for distributed tracing data. The metric pipeline is for performance metrics. Build and run the docker instance to start collecting your telemetry data with the following command: ```sh docker build -t otel-collector . && docker run -p 4317:4317 -p 4318:4318 otel-collector ``` ## Generating telemetry data Now that we have the app and the docker container set up, we can start generating telemetry data. Run your application with these environment variables to send data to the collector: ```sh OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \ OTEL_SERVICE_NAME=chat-app \ OTEL_DENO=true \ deno run --allow-net --allow-env --env-file --allow-read main.ts ``` This command: - Points the OpenTelemetry exporter to your local collector (`localhost:4318`) - Names your service "chat-app" in Grafana Cloud - Enables Deno's OpenTelemetry integration - Runs your application with the necessary permissions To generate some telemetry data, make a few requests to your running application in your browser at [`http://localhost:8000`](http://localhost:8000). Each request will: 1. Generate traces as it flows through your application 2. Send logs from your application's console output 3. Create metrics about the request performance 4. Forward all this data through the collector to Grafana Cloud ## Viewing telemetry data After making some requests to your application, you'll see three types of data in your Grafana Cloud dashboard: 1. **Traces** - End-to-end request flows through your system 2. **Logs** - Console output and structured log data 3. **Metrics** - Performance and resource utilization data ![Viewing logs in Grafana](./images/how-to/grafana/grafana-logs.png) You can drill down into individual spans to debug performance issues: ![Viewing traces in Grafana](./images/how-to/grafana/grafana-traces.png) 🦕 Now that you have telemetry export working, you could: 1. Add custom spans and attributes to better understand your application 2. Set up alerts based on latency or error conditions 3. Deploy your application and collector to production using platforms like: - [Fly.io](https://docs.deno.com/examples/deploying_deno_with_docker/) - [Digital Ocean](https://docs.deno.com/examples/digital_ocean_tutorial/) - [AWS Lightsail](https://docs.deno.com/examples/aws_lightsail_tutorial/) For more details on OpenTelemetry configuration, check out the [Grafana Cloud documentation](https://grafana.com/docs/grafana-cloud/monitor-applications/application-observability/collector/). --- # 可执行脚本 > Guide to creating executable scripts with Deno. Learn about hashbangs, file permissions, cross-platform compatibility, and how to create command-line tools that can run directly from the terminal. URL: https://docs.deno.com/examples/hashbang_tutorial/ 使 Deno 脚本可执行在创建小工具或用于文件操作、数据处理或从命令行运行的重复任务等任务时非常方便。可执行脚本允许您创建即席解决方案,而无需设置整个项目。 ## 创建示例脚本 要使脚本可执行,请以哈希bang(有时称为 shebang)开头。它是一串字符(#!),告诉您的操作系统如何执行脚本。后面跟着应用于运行脚本的解释器的路径。 :::note 要在 Windows 上使用哈希bang,您需要安装 Windows 子系统 Linux(WSL)或使用类似 Unix 的 shell,如 [Git Bash](https://git-scm.com/downloads)。 ::: 我们将创建一个简单的脚本,使用 [Deno.env](/api/deno/~/Deno.env) API 打印 Deno 安装路径。 创建一个名为 `hashbang.ts` 的文件,内容如下: ```ts title="hashbang.ts" #!/usr/bin/env -S deno run --allow-env const path = Deno.env.get("DENO_INSTALL"); console.log("Deno 安装路径:", path); ``` 该脚本告诉系统使用 deno 运行时来运行脚本。-S 标志将命令分割成参数,并指示应将后续参数(`deno run --allow-env`)传递给 env 命令。 然后,脚本使用 `Deno.env.get()` 检索与环境变量 `DENO_INSTALL` 关联的值,并将其分配给名为 `path` 的变量。最后,它使用 `console.log()` 将路径打印到控制台。 ### 执行脚本 为了执行脚本,您可能需要给予脚本执行权限,您可以使用 `chmod` 命令和 `+x` 标志(用于执行)来实现: ```sh chmod +x hashbang.ts ``` 您可以直接在命令行中执行脚本: ```sh ./hashbang.ts ``` ## 在没有扩展名的文件中使用哈希bang 为了简洁,您可能希望省略脚本文件名的扩展名。在这种情况下,在脚本本身中使用 `--ext` 标志提供一个,然后您可以仅使用文件名运行脚本: ```shell title="my_script" $ cat my_script #!/usr/bin/env -S deno run --allow-env --ext=js console.log("你好!"); $ ./my_script 你好! ``` 🦕 现在您可以直接从命令行执行 Deno 脚本!记得为您的脚本文件设置执行权限(`chmod +x`),您就可以开始构建从简单的实用工具到复杂工具的任何东西。查看 [Deno 示例](/examples/) 获取您可以编写的脚本灵感。 --- # How to export telemetry data to Honeycomb > Complete guide to exporting telemetry data with OpenTelemetry and Honeycomb.io. Learn how to configure collectors, visualize traces, and monitor application performance. URL: https://docs.deno.com/examples/honeycomb_tutorial/ [OpenTelemetry](https://opentelemetry.io/) (often abbreviated as OTel) is an open-source observability framework that provides a standardized way to collect and export telemetry data such as traces, metrics and logs. Deno has built-in support for OpenTelemetry, making it easy to instrument your applications without adding external dependencies. This integration works out of the box with observability platforms like [Honeycomb](https://honeycomb.io). Honeycomb is an observability platform designed for debugging and understanding complex, modern distributed systems. In this tutorial, we'll build a simple application and export its telemetry data to Honeycomb. We'll cover: - [Set up your chat app](#set-up-your-chat-app) - [Set up a Docker collector](#set-up-a-docker-collector) - [Generating telemetry data](#generating-telemetry-data) - [Viewing telemetry data](#viewing-telemetry-data) You can find the complete source code for this tutorial [on GitHub](https://github.com/denoland/examples/tree/main/with-honeycomb). ## Set up your chat app For this tutorial, we'll use a simple chat application to demonstrate how to export telemetry data. You can find the [code for the app on GitHub](https://github.com/denoland/examples/tree/main/with-honeycomb). Either take a copy of that repository or create a [main.ts](https://github.com/denoland/examples/blob/main/with-honeycomb/main.ts) file and a [.env](https://github.com/denoland/examples/blob/main/with-honeycomb/.env.example) file. In order to run the app you will need an OpenAI API key. You can get one by signing up for an account at [OpenAI](https://platform.openai.com/signup) and creating a new secret key. You can find your API key in the [API keys section](https://platform.openai.com/account/api-keys) of your OpenAI account. Once you have an API key, set up an `OPENAI_API-KEY` environment variable in your `.env` file: ```env title=".env" OPENAI_API_KEY=your_openai_api_key ``` ## Set up a Docker collector Next, we'll set up a Docker container to run the OpenTelemetry collector. The collector is responsible for receiving telemetry data from your application and exporting it to Honeycomb. If you have not already, create a free Honeycomb account and set up an [ingest API key](https://docs.honeycomb.io/configure/environments/manage-api-keys/). In the same directory as your `main.ts` file, create a `Dockerfile` and an `otel-collector.yml` file. The `Dockerfile` will be used to build a Docker image: ```dockerfile title="Dockerfile" FROM otel/opentelemetry-collector:latest COPY otel-collector.yml /otel-config.yml CMD ["--config", "/otel-config.yml"] ``` `FROM otel/opentelemetry-collector:latest` - This line specifies the base image for the container. It uses the official OpenTelemetry Collector image and pulls the latest version. `COPY otel-collector.yml /otel-config.yml` - This instruction copies our configuration file named `otel-collector.yml` from the local build context into the container. The file is renamed to `/otel-config.yml` inside the container. `CMD ["--config", "/otel-config.yml"]` - This sets the default command that will run when the container starts. It tells the OpenTelemetry Collector to use the configuration file we copied in the previous step. Next, add the following to your `otel-collector.yml` file to define how how telemetry data should be collected and exported to Honeycomb: ```yml title="otel-collector.yml" receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 exporters: otlp: endpoint: "api.honeycomb.io:443" headers: x-honeycomb-team: $_HONEYCOMB_API_KEY processors: batch: timeout: 5s send_batch_size: 5000 service: pipelines: logs: receivers: [otlp] processors: [batch] exporters: [otlp] traces: receivers: [otlp] processors: [batch] exporters: [otlp] metrics: receivers: [otlp] processors: [batch] exporters: [otlp] ``` The `receivers` section configures how the collector receives data. It sets up an OTLP (OpenTelemetry Protocol) receiver that listens on two protocols, `gRPC` and `HTTP`, the `0.0.0.0` address means it will accept data from any source. The `exporters` section defines where the collected data should be sent. It's configured to send data to Honeycomb's API endpoint at `api.honeycomb.io:443`. The configuration requires an API key for authentication, swap `$_HONEYCOMB_API_KEY` for your actual Honeycomb API key. The `processors` section defines how the data should be processed before export. It uses batch processing with a timeout of 5 seconds and a maximum batch size of 5000 items. The `service` section ties everything together by defining three pipelines. Each pipeline is responsible for a different type of telemetry data. The logs pipeline collects application logs. The traces pipeline is for distributed tracing data. The metric pipeline is for performance metrics. Build and run the docker instance to start collecting your telemetry data with the following command: ```sh docker build -t otel-collector . && docker run -p 4317:4317 -p 4318:4318 otel-collector ``` ## Generating telemetry data Now that we have the app and the docker container set up, we can start generating telemetry data. Run your application with these environment variables to send data to the collector: ```sh OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \ OTEL_SERVICE_NAME=chat-app \ OTEL_DENO=true \ deno run --allow-net --allow-env --env-file --allow-read main.ts ``` This command: - Points the OpenTelemetry exporter to your local collector (`localhost:4318`) - Names your service "chat-app" in Honeycomb - Enables Deno's OpenTelemetry integration - Runs your application with the necessary permissions To generate some telemetry data, make a few requests to your running application in your browser at [`http://localhost:8000`](http://localhost:8000). Each request will: 1. Generate traces as it flows through your application 2. Send logs from your application's console output 3. Create metrics about the request performance 4. Forward all this data through the collector to Honeycomb ## Viewing telemetry data After making some requests to your application, you'll see three types of data in your Honeycomb.io dashboard: 1. **Traces** - End-to-end request flows through your system 2. **Logs** - Console output and structured log data 3. **Metrics** - Performance and resource utilization data ![Viewing traces in Honeycomb](./images/how-to/honeycomb/honeycomb-3.webp) You can drill down into individual spans to debug performance issues: ![Viewing expanded traces in Honeycomb](./images/how-to/honeycomb/honeycomb-4.webp) 🦕 Now that you have telemetry export working, you could: 1. Add custom spans and attributes to better understand your application 2. Set up alerts based on latency or error conditions 3. Deploy your application and collector to production using platforms like: - [Fly.io](https://docs.deno.com/examples/deploying_deno_with_docker/) - [Digital Ocean](https://docs.deno.com/examples/digital_ocean_tutorial/) - [AWS Lightsail](https://docs.deno.com/examples/aws_lightsail_tutorial/) For more details on OpenTelemetry configuration, check out the [Honeycomb documentation](https://docs.honeycomb.io/send-data/opentelemetry/collector/). --- # How to export telemetry data to HyperDX > Complete guide to exporting telemetry data with OpenTelemetry and HyperDX. Learn how to configure collectors, visualize traces, logs, metrics, and debug distributed applications effectively. URL: https://docs.deno.com/examples/hyperdx_tutorial/ [HyperDX](https://hyperdx.io) is an open source observability platform that unifies logs, traces, metrics, exceptions, and session replays into a single interface. It helps developers debug applications faster by providing a complete view of your system's behavior and performance. [OpenTelemetry](https://opentelemetry.io/) (often abbreviated as OTel) provides a standardized way to collect and export telemetry data. Deno includes built-in OpenTelemetry support, allowing you to instrument your applications without additional dependencies. This integration works seamlessly with platforms like HyperDX to collect and visualize telemetry data. In this tutorial, we'll build a simple application and export its telemetry data to HyperDX: - [Set up your chat app](#set-up-your-chat-app) - [Set up a Docker collector](#set-up-a-docker-collector) - [Generating telemetry data](#generating-telemetry-data) - [Viewing telemetry data](#viewing-telemetry-data) You can find the complete source code for this tutorial [on GitHub](https://github.com/denoland/examples/tree/main/with-hyperdx). ## Set up the app For this tutorial, we'll use a simple chat application to demonstrate how to export telemetry data. You can find the [code for the app on GitHub](https://github.com/denoland/examples/tree/main/with-hyperdx). Either take a copy of that repository or create a [main.ts](https://github.com/denoland/examples/blob/main/with-hyperdx/main.ts) file and a [.env](https://github.com/denoland/examples/blob/main/with-hyperdx/.env.example) file. In order to run the app you will need an OpenAI API key. You can get one by signing up for an account at [OpenAI](https://platform.openai.com/signup) and creating a new secret key. You can find your API key in the [API keys section](https://platform.openai.com/account/api-keys) of your OpenAI account. Once you have an API key, set up an `OPENAI_API-KEY` environment variable in your `.env` file: ```env title=".env" OPENAI_API_KEY=your_openai_api_key ``` ## Set up the collector First, create a free HyperDX account to get your API key. Then, we'll set up two files to configure the OpenTelemetry collector: 1. Create a `Dockerfile`: ```dockerfile title="Dockerfile" FROM otel/opentelemetry-collector:latest COPY otel-collector.yml /otel-config.yml CMD ["--config", "/otel-config.yml"] ``` This Dockerfile: - Uses the official OpenTelemetry Collector as the base image - Copies your configuration into the container - Sets up the collector to use your config when it starts 2. Create a file called `otel-collector.yml`: ```yml title="otel-collector.yml" receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 exporters: otlphttp/hdx: endpoint: "https://in-otel.hyperdx.io" headers: authorization: $_HYPERDX_API_KEY compression: gzip processors: batch: service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlphttp/hdx] metrics: receivers: [otlp] processors: [batch] exporters: [otlphttp/hdx] logs: receivers: [otlp] processors: [batch] exporters: [otlphttp/hdx] ``` This configuration file sets up the OpenTelemetry collector to receive telemetry data from your application and export it to HyperDX. It includes: - The receivers section accepts data via gRPC (4317) and HTTP (4318) - The Exporters section sends data to HyperDX with compression and authentication - The processors section batches telemetry data for efficient transmission - The pipelines section defines separate flows for logs, traces, and metrics Build and run the docker instance to start collecting your telemetry data with the following command: ```sh docker build -t otel-collector . && docker run -p 4317:4317 -p 4318:4318 otel-collector ``` ## Generating telemetry data Now that we have the app and the docker container set up, we can start generating telemetry data. Run your application with these environment variables to send data to the collector: ```sh OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \ OTEL_SERVICE_NAME=chat-app \ OTEL_DENO=true \ deno run --allow-net --allow-env --env-file --allow-read main.ts ``` This command: - Points the OpenTelemetry exporter to your local collector (`localhost:4318`) - Names your service "chat-app" in HyperDX - Enables Deno's OpenTelemetry integration - Runs your application with the necessary permissions To generate some telemetry data, make a few requests to your running application in your browser at [`http://localhost:8000`](http://localhost:8000). Each request will: 1. Generate traces as it flows through your application 2. Send logs from your application's console output 3. Create metrics about the request performance 4. Forward all this data through the collector to HyperDX ## Viewing telemetry data In your HyperDX dashboard, you'll see different views of your telemetry data: ### Logs View ![Viewing logs in HyperDX](./images/how-to/hyperdx/hyperdx-1.webp) Click any log to see details: ![Viewing a single log in HyperDX](./images/how-to/hyperdx/hyperdx-2.webp) ### Request Traces See all logs within a single request: ![Viewing all logs in a request in HyperDX](./images/how-to/hyperdx/hyperdx-3.webp) ### Metrics Dashboard Monitor system performance: ![Viewing metrics in HyperDX](./images/how-to/hyperdx/hyperdx-4.webp) 🦕 Now that you have telemetry export working, you could: 1. Add custom spans and attributes to better understand your application 2. Set up alerts based on latency or error conditions 3. Deploy your application and collector to production using platforms like: - [Fly.io](https://docs.deno.com/examples/deploying_deno_with_docker/) - [Digital Ocean](https://docs.deno.com/examples/digital_ocean_tutorial/) - [AWS Lightsail](https://docs.deno.com/examples/aws_lightsail_tutorial/) 🦕 For more details on OpenTelemetry configuration with HyperDX, see their [documentation](https://www.hyperdx.io/docs/install/opentelemetry). --- # 初始化一个项目 > Guide to creating and structuring new Deno projects. Learn about starting a new project, task configuration, dependency management, and best practices for growing applications. URL: https://docs.deno.com/examples/initialize_project_tutorial/ 虽然可以直接使用 `deno run` 运行脚本,但对于较大的项目,建议创建一个合理的目录结构。这样你可以更轻松地组织代码、管理依赖、编写脚本任务和运行测试。 通过运行以下命令初始化一个新项目: ```sh deno init my_project ``` 其中 `my_project` 是你的项目名称。你可以 [阅读更多关于项目结构的信息](/runtime/getting_started/first_project/)。 ### 运行你的项目 导航到项目目录: ```sh cd my_project ``` 然后你可以直接使用 `deno task` 命令运行项目: ```sh deno run dev ``` 查看你新项目中的 `deno.json` 文件。你应该在 "tasks" 字段看到一个 `dev` 任务。 ```json title="deno.json" "tasks": { "dev": "deno run --watch main.ts" }, ``` `dev` 任务是一个常见任务,用于在开发模式下运行项目。正如你所看到的,它使用 `--watch` 标志运行 `main.ts` 文件,当有更改时会自动重新加载脚本。如果你打开 `main.ts` 文件并进行更改,就可以看到这一点的实际效果。 ### 运行测试 在项目目录中运行: ```sh deno test ``` 这将执行项目中的所有测试。你可以阅读更多关于 [在 Deno 中测试的信息](/runtime/fundamentals/testing/),我们将在稍后的教程中更深入地讨论测试。此时你有一个测试文件 `main_test.ts`,它测试 `main.ts` 中的 `add` 函数。 ### 向你的项目添加内容 `main.ts` 文件作为应用程序的入口点。这里是你编写主要程序逻辑的地方。在开发项目时,你将从删除默认的加法程序开始,并将其替换为自己的代码。例如,如果你正在构建一个网络服务器,这里是你设置路由和处理请求的地方。 除了初始文件外,你可能还会创建其他模块(文件)来组织代码。考虑将相关功能分组成单独的文件。请记住,Deno [支持 ES 模块](/runtime/fundamentals/modules/),因此你可以使用导入和导出语句来组织代码。 Deno 项目的示例文件夹结构: ```sh my_project/ ├── deno.json ├── main.ts ├── main_test.ts ├── routes/ │ ├── home.ts │ ├── about.ts ├── services/ │ ├── user.ts │ ├── post.ts └── utils/ ├── logger.ts ├── logger_test.ts ├── validator_test.ts └── validator.ts ``` 这种结构可以保持你的项目整洁,并更容易找到和管理文件。 🦕 恭喜你!现在你知道如何用 `deno init` 创建一个全新的项目。请记住,Deno 鼓励简洁,避免复杂的构建工具。保持你的项目模块化、可测试且有条理。随着项目的发展,调整结构以适应你的需求。最重要的是,享受探索 Deno 功能的乐趣! --- # 如何在 Kinsta 上部署 Deno > Step-by-step guide to deploying Deno applications on Kinsta. Learn how to configure package.json, handle environment variables, set up Git deployments, and use Kinsta's application hosting platform. URL: https://docs.deno.com/examples/kinsta_tutorial/ [Kinsta 应用托管](https://kinsta.com/application-hosting)是一项服务,让您可以直接从 Git 仓库构建和部署您的 Web 应用。 ## 准备您的应用 在 **Kinsta**,我们建议使用 [`deno-bin`](https://www.npmjs.com/package/deno-bin) 包来运行 Deno 应用。 为此,您的 `package.json` 应该如下所示: ```json title="package.json" { "name": "deno app", "scripts": { "start": "deno run --allow-net index.js --port=${PORT}" }, "devDependencies": { "deno-bin": "^1.28.2" } } ``` ## 示例应用 ```js import { parseArgs } from "jsr:@std/cli"; const { args } = Deno; const port = parseArgs(args).port ? Number(parseArgs(args).port) : 8000; Deno.serve({ port }, (_req) => new Response("Hello, world")); ``` 应用本身不言自明。重要的是不要硬编码 `PORT`,而是使用 **Kinsta** 提供的环境变量。 还有一个 [仓库](https://github.com/kinsta/hello-world-deno) 可以帮助您入门。 ## 部署 1. 在 [Kinsta 应用托管](https://kinsta.com/signup/?product_type=app-db) 注册,或直接登录 [My Kinsta](https://my.kinsta.com/) 管理面板。 2. 转到应用程序选项卡。 3. 连接您的 GitHub 仓库。 4. 按下 **添加服务 > 应用程序按钮**。 5. 按照向导步骤操作。 --- # Build a Real-time LLM Chat App with Deno > Learn how to integrate Large Language Models (LLM) with Deno to create an interactive roleplay chat application with AI characters using OpenAI or Anthropic APIs. URL: https://docs.deno.com/examples/llm_tutorial/ Large Language Models (LLMs) like OpenAI's GPT and Anthropic's Claude are powerful tools for creating intelligent, conversational applications. In this tutorial, we'll build a real-time chat application where AI characters powered by LLMs interact with users in a roleplay game setting. You can see the code for the [finished app on GitHub](https://github.com/denoland/tutorial-with-llm). :::info Deploy your own Want to skip the tutorial and deploy the finished app right now? Click the button below to instantly deploy your own copy of the complete LLM chat application to Deno Deploy. You'll get a live, working application that you can customize and modify as you learn! [![Deploy on Deno](https://deno.com/button)](https://console.deno.com/new?clone=https://github.com/denoland/tutorial-with-llm&mode=dynamic&entrypoint=main.ts&install=deno+install) Once you have deployed, add your `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` in the project "Settings". ::: ## Initialize a new project First, create a new directory for your project and initialize it: ```bash mkdir deno-llm-chat cd deno-llm-chat deno init ``` ## Project structure We'll create a modular structure that separates concerns between LLM integration, game logic, and server management: ```sh ├── main.ts # Main server entry point ├── main_test.ts # Test file ├── deno.json # Deno configuration ├── .env # Environment variables (API keys) ├── src/ │ ├── config/ │ │ ├── characters.ts # Character configurations and presets │ │ └── scenarios.ts # Pre-defined scenario templates │ ├── game/ │ │ ├── GameManager.ts # Core game logic and state management │ │ └── Character.ts # AI character implementation │ ├── llm/ │ │ └── LLMProvider.ts # LLM integration layer (OpenAI/Anthropic) │ └── server/ │ └── WebSocketHandler.ts # Real-time communication └── static/ ├── index.html # Web interface ├── app.js # Frontend JavaScript └── styles.css # Application styling ``` ## Set up dependencies Add the required dependencies to your `deno.json`: ```json title="deno.json" { "tasks": { "dev": "deno run -A --env-file --watch main.ts", "start": "deno run --allow-net --allow-env --allow-read main.ts", "test": "deno test --allow-net --allow-env" }, "imports": { "@std/assert": "jsr:@std/assert@1", "@std/http": "jsr:@std/http@1", "@std/uuid": "jsr:@std/uuid@1", "@std/json": "jsr:@std/json@1" }, "compilerOptions": { "lib": [ "dom", "dom.asynciterable", "deno.ns" ] } } ``` ## Configure environment variables Create a `.env` file for your API keys. The application supports both OpenAI and Anthropic. Comment out the config that you won't be using with a `#`. ```bash title=".env" # Choose one of the following LLM providers: # OpenAI Configuration OPENAI_API_KEY=your-openai-api-key-here # OR Anthropic Configuration # ANTHROPIC_API_KEY=your-anthropic-api-key-here # Server Configuration (optional) PORT=8000 ``` You can get API keys from: - [OpenAI Platform](https://platform.openai.com/api-keys) - [Anthropic Console](https://console.anthropic.com/) ## Build the LLM Provider The core of our application is the LLM provider that handles communication with AI services. Create `src/llm/LLMProvider.ts`: ```typescript title="src/llm/LLMProvider.ts" export interface LLMConfig { provider: "openai" | "anthropic" | "mock"; apiKey?: string; model?: string; maxTokens?: number; temperature?: number; } export class LLMProvider { private config: LLMConfig; private rateLimitedUntil: number = 0; private retryCount: number = 0; private maxRetries: number = 3; constructor(config?: Partial) { const apiKey = config?.apiKey || Deno.env.get("OPENAI_API_KEY") || Deno.env.get("ANTHROPIC_API_KEY"); // Auto-detect provider based on available API keys let provider = config?.provider; if (!provider && apiKey) { if (Deno.env.get("OPENAI_API_KEY")) { provider = "openai"; } else if (Deno.env.get("ANTHROPIC_API_KEY")) { provider = "anthropic"; } } this.config = { provider: provider || "mock", model: provider === "anthropic" ? "claude-3-haiku-20240307" : "gpt-3.5-turbo", maxTokens: 150, temperature: 0.8, ...config, apiKey, }; console.log(`LLM Provider initialized: ${this.config.provider}`); } async generateResponse(prompt: string): Promise { // Check rate limiting if (this.rateLimitedUntil > Date.now()) { console.warn("Rate limited, using mock response"); return this.mockResponse(prompt); } try { switch (this.config.provider) { case "openai": return await this.callOpenAI(prompt); case "anthropic": return await this.callAnthropic(prompt); case "mock": default: return this.mockResponse(prompt); } } catch (error) { console.error("LLM API error:", error); if (this.shouldRetry(error)) { this.retryCount++; if (this.retryCount <= this.maxRetries) { console.log(`Retrying... (${this.retryCount}/${this.maxRetries})`); await this.delay(1000 * this.retryCount); return this.generateResponse(prompt); } } return this.mockResponse(prompt); } } private async callOpenAI(prompt: string): Promise { const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${this.config.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: this.config.model, messages: [{ role: "user", content: prompt }], max_tokens: this.config.maxTokens, temperature: this.config.temperature, }), }); if (!response.ok) { throw new Error(`OpenAI API error: ${response.status}`); } const data = await response.json(); this.retryCount = 0; // Reset on success return data.choices[0].message.content.trim(); } private async callAnthropic(prompt: string): Promise { const response = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "x-api-key": this.config.apiKey!, "Content-Type": "application/json", "anthropic-version": "2023-06-01", }, body: JSON.stringify({ model: this.config.model, max_tokens: this.config.maxTokens, messages: [{ role: "user", content: prompt }], temperature: this.config.temperature, }), }); if (!response.ok) { throw new Error(`Anthropic API error: ${response.status}`); } const data = await response.json(); this.retryCount = 0; // Reset on success return data.content[0].text.trim(); } private mockResponse(prompt: string): string { const responses = [ "I understand! Let me think about this...", "That's an interesting approach to the situation.", "I see what you're getting at. Here's what I think...", "Fascinating! I would approach it this way...", "Good point! That gives me an idea...", ]; return responses[Math.floor(Math.random() * responses.length)]; } private shouldRetry(error: any): boolean { // Retry on rate limits and temporary server errors const errorMessage = error.message?.toLowerCase() || ""; return errorMessage.includes("rate limit") || errorMessage.includes("429") || errorMessage.includes("500") || errorMessage.includes("502") || errorMessage.includes("503"); } private delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } } ``` In this file we set an LLM provider, this allows us to easily switch between different LLM APIs or mock responses for testing. We also add a retry mechanism for handling API errors. ## Create AI Characters Characters are the heart of our roleplay application. Create `src/game/Character.ts`: ```typescript title="src/game/Character.ts" import { LLMProvider } from "../llm/LLMProvider.ts"; export class Character { public name: string; public class: string; public personality: string; public conversationHistory: string[] = []; private llmProvider: LLMProvider; constructor( name: string, characterClass: string, personality: string, llmProvider: LLMProvider, ) { this.name = name; this.class = characterClass; this.personality = personality; this.llmProvider = llmProvider; } async generateResponse( context: string, userMessage: string, ): Promise { // Build the character's prompt with personality and context const characterPrompt = ` You are ${this.name}, a ${this.class} with this personality: ${this.personality} Context: ${context} Recent conversation: ${this.conversationHistory.slice(-3).join("\n")} User message: ${userMessage} Respond as ${this.name} in character. Keep responses under 150 words and maintain your personality traits. Be engaging and helpful to advance the roleplay scenario. `.trim(); try { const response = await this.llmProvider.generateResponse(characterPrompt); // Add to conversation history this.conversationHistory.push(`User: ${userMessage}`); this.conversationHistory.push(`${this.name}: ${response}`); // Keep history manageable if (this.conversationHistory.length > 20) { this.conversationHistory = this.conversationHistory.slice(-10); } return response; } catch (error) { console.error(`Error generating response for ${this.name}:`, error); return `*${this.name} seems lost in thought and doesn't respond*`; } } getCharacterInfo() { return { name: this.name, class: this.class, personality: this.personality, }; } clearHistory() { this.conversationHistory = []; } } ``` Here we define the `Character` class, which represents each player character in the game. This class will handle generating responses based on the character's personality and the current game context. ## Set up character configurations Create predefined character templates in `src/config/characters.ts`: ```typescript title="src/config/characters.ts" export interface CharacterConfig { name: string; class: string; personality: string; emoji?: string; backstory?: string; } export const defaultCharacters: CharacterConfig[] = [ { name: "Tharin", emoji: "⚔️", class: "Fighter", personality: "Brave and loyal team leader, always ready to protect allies. Takes charge in dangerous situations but listens to party input.", backstory: "A former city guard seeking adventure and justice.", }, { name: "Lyra", emoji: "🔮", class: "Wizard", personality: "Curious and analytical strategist, loves solving puzzles. Uses magic creatively to support the party.", backstory: "A scholar of ancient magic seeking forgotten spells.", }, { name: "Finn", emoji: "🗡️", class: "Rogue", personality: "Witty and sneaky scout, prefers clever solutions. Acts quickly and adapts to what allies need.", backstory: "A former street thief now using skills for good.", }, ]; ``` These templates are what the `Character` class will use to instantiate each character with their unique traits. The LLM will use these traits to generate responses that are consistent with each character's personality and backstory. ## Build the Game Manager The Game Manager coordinates characters and maintains game state. Create `src/game/GameManager.ts`: ```typescript title="src/game/GameManager.ts" import { Character } from "./Character.ts"; import { LLMProvider } from "../llm/LLMProvider.ts"; export interface GameState { id: string; gmPrompt: string; characters: Character[]; messages: GameMessage[]; currentTurn: number; isActive: boolean; createdAt: Date; } export interface GameMessage { id: string; speaker: string; message: string; timestamp: Date; type: "gm" | "character" | "system"; } export interface StartGameRequest { gmPrompt: string; characters: Array<{ name: string; class: string; personality: string; }>; } export class GameManager { private games: Map = new Map(); private llmProvider: LLMProvider; constructor() { this.llmProvider = new LLMProvider(); } async startNewGame( gmPrompt: string, characterConfigs: StartGameRequest["characters"], ): Promise { const gameId = crypto.randomUUID(); // Create characters with their LLM personalities const characters = characterConfigs.map((config) => new Character( config.name, config.class, config.personality, this.llmProvider, ) ); const gameState: GameState = { id: gameId, gmPrompt, characters, messages: [], currentTurn: 0, isActive: true, createdAt: new Date(), }; this.games.set(gameId, gameState); // Add initial system message this.addMessage(gameId, { speaker: "System", message: `Game started! Players: ${ characters.map((c) => c.name).join(", ") }`, type: "system", }); console.log(`New game started: ${gameId}`); return gameId; } async handlePlayerMessage( gameId: string, message: string, ): Promise { const game = this.games.get(gameId); if (!game || !game.isActive) { throw new Error("Game not found or inactive"); } // Add player message this.addMessage(gameId, { speaker: "Player", message, type: "gm", }); // Generate responses from each character const responses: GameMessage[] = []; for (const character of game.characters) { try { const context = this.buildContext(game); const response = await character.generateResponse(context, message); const characterMessage = this.addMessage(gameId, { speaker: character.name, message: response, type: "character", }); responses.push(characterMessage); // Small delay between character responses for realism await new Promise((resolve) => setTimeout(resolve, 500)); } catch (error) { console.error(`Error getting response from ${character.name}:`, error); } } game.currentTurn++; return responses; } private buildContext(game: GameState): string { const recentMessages = game.messages.slice(-5); const context = [ `Scenario: ${game.gmPrompt}`, `Current turn: ${game.currentTurn}`, "Recent events:", ...recentMessages.map((m) => `${m.speaker}: ${m.message}`), ].join("\n"); return context; } private addMessage( gameId: string, messageData: Omit, ): GameMessage { const game = this.games.get(gameId); if (!game) throw new Error("Game not found"); const message: GameMessage = { id: crypto.randomUUID(), timestamp: new Date(), ...messageData, }; game.messages.push(message); return message; } getGame(gameId: string): GameState | undefined { return this.games.get(gameId); } getActiveGames(): string[] { return Array.from(this.games.entries()) .filter(([_, game]) => game.isActive) .map(([id, _]) => id); } endGame(gameId: string): boolean { const game = this.games.get(gameId); if (game) { game.isActive = false; console.log(`Game ended: ${gameId}`); return true; } return false; } } ``` The game manager will handle all game-related logic, including starting new games, processing player messages, and managing game state. When a player sends a message, the game manager will route it to the appropriate character for response generation. ## Add WebSocket Support Real-time communication makes the roleplay experience more engaging. Create `src/server/WebSocketHandler.ts`: ```typescript title="src/server/WebSocketHandler.ts" import { GameManager } from "../game/GameManager.ts"; export interface WebSocketMessage { type: "start_game" | "send_message" | "join_game" | "get_game_state"; gameId?: string; data?: any; } export class WebSocketHandler { private gameManager: GameManager; private connections: Map = new Map(); constructor(gameManager: GameManager) { this.gameManager = gameManager; } handleConnection(request: Request): Response { const { socket, response } = Deno.upgradeWebSocket(request); const connectionId = crypto.randomUUID(); this.connections.set(connectionId, socket); socket.onopen = () => { console.log(`WebSocket connection opened: ${connectionId}`); this.sendMessage(socket, { type: "connection", data: { connectionId, message: "Connected to LLM Chat server" }, }); }; socket.onmessage = async (event) => { try { const message: WebSocketMessage = JSON.parse(event.data); await this.handleMessage(socket, message); } catch (error) { console.error("Error handling WebSocket message:", error); this.sendError(socket, "Invalid message format"); } }; socket.onclose = () => { console.log(`WebSocket connection closed: ${connectionId}`); this.connections.delete(connectionId); }; socket.onerror = (error) => { console.error(`WebSocket error for ${connectionId}:`, error); }; return response; } private async handleMessage(socket: WebSocket, message: WebSocketMessage) { switch (message.type) { case "start_game": await this.handleStartGame(socket, message.data); break; case "send_message": await this.handleSendMessage(socket, message); break; case "get_game_state": await this.handleGetGameState(socket, message.gameId!); break; default: this.sendError(socket, `Unknown message type: ${message.type}`); } } private async handleStartGame(socket: WebSocket, data: any) { try { const { gmPrompt, characters } = data; const gameId = await this.gameManager.startNewGame(gmPrompt, characters); this.sendMessage(socket, { type: "game_started", data: { gameId, message: "Game started successfully! You can now send messages to interact with your characters.", }, }); } catch (error) { this.sendError(socket, `Failed to start game: ${error.message}`); } } private async handleSendMessage( socket: WebSocket, message: WebSocketMessage, ) { try { const { gameId, data } = message; if (!gameId) { this.sendError(socket, "Game ID required"); return; } const responses = await this.gameManager.handlePlayerMessage( gameId, data.message, ); this.sendMessage(socket, { type: "character_responses", data: { gameId, responses }, }); } catch (error) { this.sendError(socket, `Failed to process message: ${error.message}`); } } private async handleGetGameState(socket: WebSocket, gameId: string) { try { const game = this.gameManager.getGame(gameId); if (!game) { this.sendError(socket, "Game not found"); return; } this.sendMessage(socket, { type: "game_state", data: { gameId, characters: game.characters.map((c) => c.getCharacterInfo()), messages: game.messages.slice(-10), // Last 10 messages isActive: game.isActive, }, }); } catch (error) { this.sendError(socket, `Failed to get game state: ${error.message}`); } } private sendMessage(socket: WebSocket, message: any) { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify(message)); } } private sendError(socket: WebSocket, error: string) { this.sendMessage(socket, { type: "error", data: { error }, }); } } ``` Here we set up the WebSocket server to handle connections and messages. Websockets allow for real-time communication between the client and server, making them ideal for interactive applications like a chat app, or game. We send messages back and forth between the client and server to keep the game state in sync. ## Create the main server Now let's tie everything together in `main.ts`: ```typescript title="main.ts" import { GameManager } from "./src/game/GameManager.ts"; import { WebSocketHandler } from "./src/server/WebSocketHandler.ts"; import { defaultCharacters } from "./src/config/characters.ts"; const gameManager = new GameManager(); const wsHandler = new WebSocketHandler(gameManager); async function handler(req: Request): Promise { const url = new URL(req.url); // Handle WebSocket connections if (req.headers.get("upgrade") === "websocket") { return wsHandler.handleConnection(req); } // Serve static files and API endpoints switch (url.pathname) { case "/": return new Response(await getIndexHTML(), { headers: { "content-type": "text/html" }, }); case "/api/characters": return new Response(JSON.stringify(defaultCharacters), { headers: { "content-type": "application/json" }, }); case "/api/game/start": if (req.method === "POST") { try { const body = await req.json(); const gameId = await gameManager.startNewGame( body.gmPrompt, body.characters, ); return new Response(JSON.stringify({ gameId }), { headers: { "content-type": "application/json" }, }); } catch (error) { return new Response( JSON.stringify({ error: error.message }), { status: 400, headers: { "content-type": "application/json" }, }, ); } } break; case "/api/game/message": if (req.method === "POST") { try { const body = await req.json(); const responses = await gameManager.handlePlayerMessage( body.gameId, body.message, ); return new Response(JSON.stringify({ responses }), { headers: { "content-type": "application/json" }, }); } catch (error) { return new Response( JSON.stringify({ error: error.message }), { status: 400, headers: { "content-type": "application/json" }, }, ); } } break; default: return new Response("Not Found", { status: 404 }); } return new Response("Method Not Allowed", { status: 405 }); } async function getIndexHTML(): Promise { try { return await Deno.readTextFile("./static/index.html"); } catch { // Return a basic HTML template if file doesn't exist return ` LLM Roleplay Chat

Oops! Something went wrong.

`; } } const port = parseInt(Deno.env.get("PORT") || "8000"); console.log(`🎭 LLM Chat server starting on http://localhost:${port}`); Deno.serve({ port }, handler); ``` In the `main.ts` file we set up an HTTP server and a WebSocket server to handle real-time communication. We use the HTTP server to serve static files and provide API endpoints, while the WebSocket server manages real-time interactions between clients. ## Add a frontend The frontend of our app will live in the `static` directory. Create an `index.html`, `app.js` and a `style.css` file in the `static` directory. ### `index.html` We'll create a very basic layout with a textarea to collect the user's scenario input and a section to show the response messages with a text input to send messages. Copy the content from [this html file](https://github.com/denoland/tutorial-with-llm/blob/main/static/index.html) into your `index.html`. ### `app.js` In `app.js`, we'll add the JavaScript to handle user input and display responses. Copy the content from [this js file](https://github.com/denoland/tutorial-with-llm/blob/main/static/app.js) into your `app.js`. ### `style.css` We'll add some basic styles to make our app look nicer. Copy the content from [this css file](https://github.com/denoland/tutorial-with-llm/blob/main/static/style.css) into your `style.css`. ## Run your application Start your development server: ```bash deno task dev ``` Your LLM chat application will be available at `http://localhost:8000`. The application will: 1. **Auto-detect your LLM provider** based on available API keys 2. **Fall back to mock responses** if no API keys are configured 3. **Handle rate limiting** gracefully with retries and fallbacks 4. **Provide real-time interaction** through WebSockets ## Deploy your application to the cloud Now that you have your working LLM chat application, you can deploy it to the cloud with Deno Deploy. For the best experience, you can deploy your app directly from GitHub, which will set up automated deployments. Create a GitHub repository and push your app there. [Create a new GitHub repository](https://github.com/new), then initialize and push your app to GitHub: ```sh git init -b main git remote add origin https://github.com//.git git add . git commit -am 'initial commit' git push -u origin main ``` Once your app is on GitHub, you can [deploy it to Deno Deploy](https://console.deno.com/). Don't forget to add your `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` environment variables in the project "Settings". For a walkthrough of deploying your app, check out the [Deno Deploy tutorial](/examples/deno_deploy_tutorial/). ## Testing We've provided tests to verify your setup, copy the [`main.test.ts`](https://github.com/denoland/tutorial-with-llm/blob/main/tests/main.test.ts) file to your project directory and run the included tests to verify your setup: ```bash deno task test ``` 🦕 You now have a working LLM chat application, with realtime interaction, rate limiting and error handling. Next you can customise it to your own play style! Consider giving the LLM instructions on how to behave in different scenarios, or how to respond to specific user inputs. You can add these into the character configuration files. You could also consider adding a database to store the conversation history for long-term character and story development. --- # 将自定义域从 Deploy Classic 迁移到 Deno Deploy > 了解如何将您的自定义域从 Deploy Classic 迁移到 Deno Deploy URL: https://docs.deno.com/examples/migrate_custom_domain_tutorial/ 如果您之前在 Deploy Classic 上设置了自定义域,并且想将其迁移到 Deno Deploy,请按照以下步骤操作: ## 将您的域添加到 Deno Deploy 1. 访问 [Deno Deploy 控制台](https://dash.deno.com),并导航到您想关联自定义域的项目。 2. 点击 **“Settings”** 标签。 3. 在“Production Domains”下,点击 **“+ Add Domain”**。 4. 输入您的自定义域(例如,`test.mywebsite.com`),选择您是只想添加基础 URL 还是基础 URL 和通配符,然后点击 **“Save”**。 这将启动 DNS 记录配置,可能需要几分钟时间。 系统将显示需要添加到您的 DNS 提供商的 DNS 记录。 ## 申请 TLS 证书 在您的 DNS 提供商设置中,更新您域的 DNS 记录,包含提供的 `_acme-challenge` CNAME 记录。这是 Deno Deploy 验证您的域并申请 TLS 证书所必需的。 ![DNS Records modal](/deploy/images/dns_config.png) 一旦 DNS 记录生效,点击 **“Verify DNS and provision certificate”** 按钮申请新的 TLS 证书。 ## 更新 DNS 记录 在您的 DNS 提供商设置中,删除域名的任何现有 CNAME/A/AAAA 记录,并用 Deno Deploy 提供的 CNAME 或 ANAME 记录替换。 由于 DNS 传播延迟,此过程可能需要一些时间。请允许最多 48 小时以使更改生效,之后再从 Deploy Classic 中移除该域。 --- # 使用 Mock 进行隔离测试 > 掌握单元测试中的 Mock 技巧。了解如何使用 Spy、Stub、模拟时间及 Deno 其他工具来提升代码质量与测试可信度 URL: https://docs.deno.com/examples/mocking_tutorial/ 本指南基于 [Deno 测试基础](/examples/testing_tutorial/),重点介绍可帮助你在测试中隔离代码的 Mock 技术。 为了实现高效的单元测试,你经常需要“模拟”(mock)代码所交互的数据。Mock 是一种测试技术,通过用可控的模拟数据替代真实数据来测试代码。当测试与外部服务(比如 API 或数据库)交互的组件时,这尤为有用。 Deno 标准库提供了[便捷的 Mock 工具](https://jsr.io/@std/testing/doc/mock),让你的测试更轻松编写、更可靠且执行更快。 ### 监听 (Spying) 在 Deno 中,你可以使用 [`spy`](https://jsr.io/@std/testing/doc/mock#spying) 监听函数调用情况。Spy 不会改变函数行为,但会记录如函数被调用次数及传入参数等重要信息。 通过使用 Spy,你可以检验代码是否与其依赖正确交互,而无需搭建复杂的基础设施。 下面示例测试了一个名为 `saveUser()` 的函数,它接受一个用户对象和一个数据库对象,然后调用数据库的 `save` 方法: ```ts import { assertEquals } from "jsr:@std/assert"; import { assertSpyCalls, spy } from "jsr:@std/testing/mock"; // 定义类型以提升代码质量 interface User { name: string; } interface Database { save: (user: User) => Promise; } // 待测试函数 function saveUser( user: User, database: Database, ): Promise { return database.save(user); } // 使用 mock 测试 Deno.test("saveUser 调用了 database.save", async () => { // 创建一个带有 save 方法的 mock 数据库,save 方法被 spy 包裹 const mockDatabase = { save: spy((user: User) => Promise.resolve({ id: 1, ...user })), }; const user: User = { name: "Test User" }; const result = await saveUser(user, mockDatabase); // 验证 mock 调用情况 assertSpyCalls(mockDatabase.save, 1); assertEquals(mockDatabase.save.calls[0].args[0], user); assertEquals(result, { id: 1, name: "Test User" }); }); ``` 我们从 Deno 标准库导入必要的函数,用于断言相等和创建及校验 spy 函数。 这个模拟数据库是一个替代真实数据库对象的站位符,带有一个被 `spy` 包裹的 `save` 方法。这个 spy 函数会跟踪该方法的调用次数、记录传入的参数,并执行其底层实现(这里返回了包含 `user` 及 `id` 的 Promise)。 测试中调用了带有模拟数据的 `saveUser()`,我们通过断言验证了: 1. `save` 方法被调用了且仅调用了一次 2. 调用的第一个参数是我们传入的 `user` 对象 3. 返回结果包含原有的用户数据和新增的 ID 我们能够在无需搭建或清理复杂数据库状态的情况下,测试了 `saveUser` 功能。 ### 清除 Spy 当多个测试都使用 spy 时,重要的是在测试之间重置或清除 spy,以避免相互干扰。Deno 测试库提供了使用 `restore()` 方法轻松恢复所有 spy 到原始状态。 下面示例演示如何在完成使用 spy 后清理它: ```ts import { assertEquals } from "jsr:@std/assert"; import { assertSpyCalls, spy } from "jsr:@std/testing/mock"; Deno.test("spy 清理示例", () => { // 创建一个监听函数的 spy const myFunction = spy((x: number) => x * 2); // 使用 spy const result = myFunction(5); assertEquals(result, 10); assertSpyCalls(myFunction, 1); // 测试完成后,恢复 spy try { // 使用 spy 的测试代码 // ... } finally { // 始终清理 spy myFunction.restore(); } }); ``` 方法的 spy 是可销毁的,可以用 `using` 关键字让它们自动恢复。这种做法避免了你必须用 `try` 代码块包裹断言,确保测试结束前方法能被恢复。 ```ts import { assertEquals } from "jsr:@std/assert"; import { assertSpyCalls, spy } from "jsr:@std/testing/mock"; Deno.test("使用可自动恢复的 spies", () => { const calculator = { add: (a: number, b: number) => a + b, multiply: (a: number, b: number) => a * b, }; // spy 会在超出作用域时自动恢复 using addSpy = spy(calculator, "add"); // 使用 spy const sum = calculator.add(3, 4); assertEquals(sum, 7); assertSpyCalls(addSpy, 1); assertEquals(addSpy.calls[0].args, [3, 4]); // 不需要 try/finally 块,spy 会自动恢复 }); Deno.test("同时使用多个可自动恢复的 spies", () => { const calculator = { add: (a: number, b: number) => a + b, multiply: (a: number, b: number) => a * b, }; // 两个 spy 都会自动恢复 using addSpy = spy(calculator, "add"); using multiplySpy = spy(calculator, "multiply"); calculator.add(5, 3); calculator.multiply(4, 2); assertSpyCalls(addSpy, 1); assertSpyCalls(multiplySpy, 1); // 不需要清理代码 }); ``` 如果你有多个不支持 `using` 关键字的 spy,可以将它们保存在数组里,一次性调用恢复: ```ts Deno.test("多个 spies 清理", () => { const spies = []; // 创建 spy const functionA = spy((x: number) => x + 1); spies.push(functionA); const objectB = { method: (x: number) => x * 2, }; const spyB = spy(objectB, "method"); spies.push(spyB); // 测试中使用 spies // ... // 测试结束时清理所有 spies try { // 使用 spies 的测试代码 } finally { // 恢复所有 spies spies.forEach((spyFn) => spyFn.restore()); } }); ``` 正确清理 spies 能确保每个测试从干净的状态开始,避免测试间侧漏副作用。 ### Stub(存根) 虽然 spy 用于记录方法调用而不改变行为,但 stub 会完全替换原有实现。 [Stub](https://jsr.io/@std/testing/doc/mock#stubbing) 是 mock 的一种形式,用于临时替换函数或方法实现,可用于模拟特定情况或预设返回值,也常用于重写依赖环境的功能。 在 Deno 中,你可以通过标准测试库的 `stub` 函数创建 stub: ```ts import { assertEquals } from "jsr:@std/assert"; import { Stub, stub } from "jsr:@std/testing/mock"; // 定义类型以提升代码质量 interface User { name: string; role: string; } // 原始函数 function getCurrentUser(userId: string): User { // 可能涉及数据库调用的实现 return { name: "Real User", role: "admin" }; } // 待测试函数 function hasAdminAccess(userId: string): boolean { const user = getCurrentUser(userId); return user.role === "admin"; } Deno.test("hasAdminAccess 使用 stub 用户", () => { // 创建替代 getCurrentUser 的 stub const getUserStub: Stub = stub( globalThis, "getCurrentUser", // 返回非管理员的测试用户 () => ({ name: "Test User", role: "guest" }), ); try { // 使用 stub 函数测试 const result = hasAdminAccess("user123"); assertEquals(result, false); // 测试中也能改变 stub 行为 getUserStub.restore(); // 移除第一个 stub const adminStub = stub( globalThis, "getCurrentUser", () => ({ name: "Admin User", role: "admin" }), ); try { const adminResult = hasAdminAccess("admin456"); assertEquals(adminResult, true); } finally { adminStub.restore(); } } finally { // 始终还原原始函数,避免影响其他测试 getUserStub.restore(); } }); ``` 这里导入了必要函数,设置了一个可能调用数据库的原始 `getCurrentUser` 函数。 我们定义了待测试的 `hasAdminAccess()`,用来判断用户是否为管理员。 接着创建了 `hasAdminAccess with a stubbed user` 测试,用 stub 替换真实的 `getCurrentUser`,模拟返回一个非管理员用户。 测试调用这个 stub,会发现返回 `false`,符合预期。 然后将 stub 修改为返回管理员用户,断言结果为 `true`。 最后在 `finally` 中保证还原函数,避免对其他测试造成影响。 ### 环境变量的 Stub 要实现确定性的测试,经常需要控制环境变量。Deno 标准库提供了相关工具: ```ts import { assertEquals } from "jsr:@std/assert"; import { stub } from "jsr:@std/testing/mock"; // 依赖环境变量和时间的函数 function generateReport() { const environment = Deno.env.get("ENVIRONMENT") || "development"; const timestamp = new Date().toISOString(); return { environment, generatedAt: timestamp, data: {/* 报告数据 */}, }; } Deno.test("在受控环境下生成报告", () => { // Stub 环境变量 const originalEnv = Deno.env.get; const envStub = stub(Deno.env, "get", (key: string) => { if (key === "ENVIRONMENT") return "production"; return originalEnv.call(Deno.env, key); }); // Stub 时间 const dateStub = stub( Date.prototype, "toISOString", () => "2023-06-15T12:00:00Z", ); try { const report = generateReport(); // 验证使用受控值生成的结果 assertEquals(report.environment, "production"); assertEquals(report.generatedAt, "2023-06-15T12:00:00Z"); } finally { // 始终还原 stub,避免影响其他测试 envStub.restore(); dateStub.restore(); } }); ``` ### 模拟时间 (Faking time) 与时间相关的代码难以测试,因为测试结果可能随执行时间变化。Deno 提供了一个 [`FakeTime`](https://jsr.io/@std/testing/doc/time) 工具,可在测试中模拟时间流动,控制日期相关函数。 以下示例演示如何测试依赖时间的函数: `isWeekend()`(判断当天是否周六或周日返回 true),以及 `delayedGreeting()`(1 秒延迟后回调): ```ts import { assertEquals } from "jsr:@std/assert"; import { FakeTime } from "jsr:@std/testing/time"; // 基于当前时间的函数 function isWeekend(): boolean { const date = new Date(); const day = date.getDay(); return day === 0 || day === 6; // 0 是星期日,6 是星期六 } // 使用定时器的函数 function delayedGreeting(callback: (message: string) => void): void { setTimeout(() => { callback("Hello after delay"); }, 1000); // 1 秒延迟 } Deno.test("时间相关测试", () => { using fakeTime = new FakeTime(); // 创建从特定日期(星期一)开始的假时间 const mockedTime: FakeTime = fakeTime(new Date("2023-05-01T12:00:00Z")); try { // 测试周一 assertEquals(isWeekend(), false); // 向前推进时间到周六 mockedTime.tick(5 * 24 * 60 * 60 * 1000); // 前进 5 天 assertEquals(isWeekend(), true); // 测试带定时器的异步操作 let greeting = ""; delayedGreeting((message) => { greeting = message; }); // 立即推进 1 秒以触发定时器 mockedTime.tick(1000); assertEquals(greeting, "Hello after delay"); } finally { // 始终还原真实时间 mockedTime.restore(); } }); ``` 这里使用 `fakeTime` 创建受控时间环境,初始时间为 2023 年 5 月 1 日(星期一),返回的 `FakeTime` 对象可控制时间流逝。 我们在模拟周一时测试 `isWeekend()` 返回 `false`,推进到周六后为 `true`。 `fakeTime` 替换了 JS 的时间函数 (`Date`、`setTimeout`、`setInterval` 等),让你无论实际测试时间何时,都可测试指定时间条件。此技术可避免依赖系统时钟导致的测试不稳定,并可通过快速推进时间来加速测试。 模拟时间适用于测试: - 日历或日期相关功能,如日程、预约、过期时间 - 包含超时或定时器的代码,如轮询、延迟操作、去抖 - 动画或过渡效果的定时测试 和 Stub 类似,测试结束后记得调用 `restore()` 还原真实时间函数,避免影响其他测试。 ## 高级 Mock 模式 ### 部分 Mock 有时你只想 mock 某些对象方法,保留其他方法真实实现: ```ts import { assertEquals } from "jsr:@std/assert"; import { stub } from "jsr:@std/testing/mock"; class UserService { async getUser(id: string) { // 复杂数据库查询 return { id, name: "Database User" }; } async formatUser(user: { id: string; name: string }) { return { ...user, displayName: user.name.toUpperCase(), }; } async getUserFormatted(id: string) { const user = await this.getUser(id); return this.formatUser(user); } } Deno.test("使用 stub 进行部分 Mock", async () => { const service = new UserService(); // 仅 mock getUser 方法 const getUserMock = stub( service, "getUser", () => Promise.resolve({ id: "test-id", name: "Mocked User" }), ); try { // formatUser 仍使用真实实现 const result = await service.getUserFormatted("test-id"); assertEquals(result, { id: "test-id", name: "Mocked User", displayName: "MOCKED USER", }); // 验证 getUser 被正确调用 assertEquals(getUserMock.calls.length, 1); assertEquals(getUserMock.calls[0].args[0], "test-id"); } finally { getUserMock.restore(); } }); ``` ### Mock fetch 请求 测试涉及 HTTP 请求的代码通常需要模拟 `fetch` API: ```ts import { assertEquals } from "jsr:@std/assert"; import { stub } from "jsr:@std/testing/mock"; // 使用 fetch 的函数 async function fetchUserData(userId: string) { const response = await fetch(`https://api.example.com/users/${userId}`); if (!response.ok) { throw new Error(`Failed to fetch user: ${response.status}`); } return await response.json(); } Deno.test("Mock fetch API", async () => { const originalFetch = globalThis.fetch; // 创建 mock fetch 返回的响应 const mockResponse = new Response( JSON.stringify({ id: "123", name: "John Doe" }), { status: 200, headers: { "Content-Type": "application/json" } }, ); // 用 stub 替换 fetch globalThis.fetch = stub( globalThis, "fetch", (_input: string | URL | Request, _init?: RequestInit) => Promise.resolve(mockResponse), ); try { const result = await fetchUserData("123"); assertEquals(result, { id: "123", name: "John Doe" }); } finally { // 还原原始 fetch globalThis.fetch = originalFetch; } }); ``` ## 真实案例 现在我们将所有技巧合并,测试一个用户认证服务,该服务: 1. 验证用户凭证 2. 调用 API 进行认证 3. 存储带有过期时间的 token 下面示例创建了完整的 `AuthService` 类,用于登录、token 管理和鉴权。测试中使用了多种 Mock 技术:Stub fetch 请求、Spy 方法、模拟时间来测试 token 过期,并使用结构化测试步骤组织。 Deno 的测试 API 提供了 `t.step()` 方法,将测试逻辑分割为步骤或子测试,使复杂测试更易读,更便于定位问题。每步可单独断言,测试结果中分别报告。 ```ts import { assertEquals, assertRejects } from "jsr:@std/assert"; import { spy, stub } from "jsr:@std/testing/mock"; import { FakeTime } from "jsr:@std/testing/time"; // 目标服务 class AuthService { private token: string | null = null; private expiresAt: Date | null = null; async login(username: string, password: string): Promise { // 校验输入 if (!username || !password) { throw new Error("Username and password are required"); } // 调用认证 API const response = await fetch("https://api.example.com/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); if (!response.ok) { throw new Error(`Authentication failed: ${response.status}`); } const data = await response.json(); // 存储带 1 小时过期时间的 token this.token = data.token; this.expiresAt = new Date(Date.now() + 60 * 60 * 1000); return this.token; } getToken(): string { if (!this.token || !this.expiresAt) { throw new Error("Not authenticated"); } if (new Date() > this.expiresAt) { this.token = null; this.expiresAt = null; throw new Error("Token expired"); } return this.token; } logout(): void { this.token = null; this.expiresAt = null; } } Deno.test("AuthService 综合测试", async (t) => { await t.step("登录应该验证凭证", async () => { const authService = new AuthService(); await assertRejects( () => authService.login("", "password"), Error, "Username and password are required", ); }); await t.step("登录应正确处理 API 调用", async () => { const authService = new AuthService(); // mock 成功响应 const mockResponse = new Response( JSON.stringify({ token: "fake-jwt-token" }), { status: 200, headers: { "Content-Type": "application/json" } }, ); const fetchStub = stub( globalThis, "fetch", (_url: string | URL | Request, options?: RequestInit) => { // 验证请求数据正确 const body = options?.body as string; const parsedBody = JSON.parse(body); assertEquals(parsedBody.username, "testuser"); assertEquals(parsedBody.password, "password123"); return Promise.resolve(mockResponse); }, ); try { const token = await authService.login("testuser", "password123"); assertEquals(token, "fake-jwt-token"); } finally { fetchStub.restore(); } }); await t.step("token 过期应正常工作", () => { using fakeTime = new FakeTime(); const authService = new AuthService(); const time = fakeTime(new Date("2023-01-01T12:00:00Z")); try { // mock 登录过程直接设置 token authService.login = spy( authService, "login", async () => { (authService as any).token = "fake-token"; (authService as any).expiresAt = new Date( Date.now() + 60 * 60 * 1000, ); return "fake-token"; }, ); // 登录并验证 token authService.login("user", "pass").then(() => { const token = authService.getToken(); assertEquals(token, "fake-token"); // 将时间推进到过期后 time.tick(61 * 60 * 1000); // token 应该已过期 assertRejects( () => { authService.getToken(); }, Error, "Token expired", ); }); } finally { time.restore(); (authService.login as any).restore(); } }); }); ``` 该代码定义了 `AuthService` 类,包含三大功能: - 登录:校验凭证,调用 API,保存带过期时间的 token - 获取 Token:返回有效且未过期的 token - 登出:清除 token 和过期时间 测试通过一个主测试,分为三个逻辑**步骤**,分别检验凭证验证、API 调用处理和 token 过期。 🦕 高效的 Mock 技术对于编写可靠、可维护的单元测试至关重要。Deno 提供多种强大工具帮助你在测试中隔离代码。掌握这些技巧后,你能编写更可靠、更快速且不依赖外部服务的测试。 更多测试资源请参考: - [Deno 测试 API 文档](/api/deno/testing) - [Deno 标准库测试模块](https://jsr.io/@std/testing) - [Deno 测试基础教程](/examples/testing_tutorial/) --- # 模块元数据 > A guide to working with module metadata in Deno. Learn about import.meta properties, main module detection, file paths, URL resolution, and how to access module context information in your applications. URL: https://docs.deno.com/examples/module_metadata_tutorial/ ## 概念 - [import.meta](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import.meta) 可以提供模块上下文的信息。 - 布尔值 [import.meta.main](https://docs.deno.com/api/web/~/ImportMeta#property_main) 会告诉你当前模块是否为程序入口点。 - 字符串 [import.meta.url](https://docs.deno.com/api/web/~/ImportMeta#property_url) 将给你当前模块的 URL。 - 字符串 [import.meta.filename](https://docs.deno.com/api/web/~/ImportMeta#property_filename) 将给你当前模块的完全解析路径。_仅适用于本地模块_。 - 字符串 [import.meta.dirname](https://docs.deno.com/api/web/~/ImportMeta#property_dirname) 将给你包含当前模块的目录的完全解析路径。_仅适用于本地模块_。 - [import.meta.resolve](https://docs.deno.com/api/web/~/ImportMeta#property_resolve) 允许你解析相对于当前模块的说明符。此函数会考虑启动时提供的导入地图(如果有)。 - 字符串 [Deno.mainModule](https://docs.deno.com/api/deno/~/Deno.mainModule) 将给你主模块入口点的 URL,即由 deno 运行时调用的模块。 ## 示例 下面的示例使用两个模块来展示 `import.meta.url`、`import.meta.main` 和 `Deno.mainModule` 之间的差异。在这个示例中,`module_a.ts` 是主模块入口点: ```ts title="module_b.ts" export function outputB() { console.log("模块 B 的 import.meta.url", import.meta.url); console.log("模块 B 的 mainModule url", Deno.mainModule); console.log( "模块 B 通过 import.meta.main 判断是否为主模块?", import.meta.main, ); } ``` ```ts title="module_a.ts" import { outputB } from "./module_b.ts"; function outputA() { console.log("模块 A 的 import.meta.url", import.meta.url); console.log("模块 A 的 mainModule url", Deno.mainModule); console.log( "模块 A 通过 import.meta.main 判断是否为主模块?", import.meta.main, ); console.log( "解析 ./module_b.ts 的说明符", import.meta.resolve("./module_b.ts"), ); } outputA(); console.log(""); outputB(); ``` 如果 `module_a.ts` 位于 `/home/alice/deno`,则运行 `deno run --allow-read module_a.ts` 的输出为: ```console 模块 A 的 import.meta.url file:///home/alice/deno/module_a.ts 模块 A 的 mainModule url file:///home/alice/deno/module_a.ts 模块 A 通过 import.meta.main 判断是否为主模块? true 解析 ./module_b.ts 的说明符 file:///home/alice/deno/module_b.ts 模块 B 的 import.meta.url file:///home/alice/deno/module_b.ts 模块 B 的 mainModule url file:///home/alice/deno/module_a.ts 模块 B 通过 import.meta.main 判断是否为主模块? false ``` --- # 如何在 Deno 中使用 Mongoose > 使用 Deno 搭配 Mongoose 的分步指南。学习如何设置 MongoDB 连接,创建模式,实施数据模型,并使用 Mongoose 的基于模式建模执行 CRUD 操作。 URL: https://docs.deno.com/examples/mongoose_tutorial/ [Mongoose](https://mongoosejs.com/) 是一个流行的基于模式的库,用于建模 [MongoDB](https://www.mongodb.com/) 的数据。它简化了编写 MongoDB 验证、类型转换和其他相关业务逻辑的过程。 本教程将向您展示如何在 Deno 项目中设置 Mongoose 和 MongoDB。 [查看源代码](https://github.com/denoland/examples/tree/main/with-mongoose) 或 [查看视频指南](https://youtu.be/dmZ9Ih0CR9g)。 ## 创建一个 Mongoose 模型 让我们创建一个简单的应用程序,连接到 MongoDB,创建一个 `Dinosaur` 模型,并向数据库添加和更新一个恐龙。 首先,我们将创建必要的文件和目录: ```console touch main.ts && mkdir model && touch model/Dinosaur.ts ``` 在 `/model/Dinosaur.ts` 中,我们将导入 `npm:mongoose`,定义 [模式],并导出它: ```ts title="model/Dinosaur.ts" import mongoose, { type HydratedDocument, type Model, model, models, Schema, } from "npm:mongoose@latest"; interface Dinosaur { name: string; description: string; createdAt?: Date; updatedAt?: Date; } interface DinosaurMethods { updateDescription( this: HydratedDocument, description: string, ): Promise< HydratedDocument >; } type DinosaurModel = Model; const dinosaurSchema = new Schema( { name: { type: String, unique: true, required: true }, description: { type: String, required: true }, }, { timestamps: true }, ); dinosaurSchema.methods.updateDescription = async function ( this: HydratedDocument, description: string, ) { this.description = description; return await this.save(); }; export default (models.Dinosaur as DinosaurModel) || model("Dinosaur", dinosaurSchema); ``` ## 连接到 MongoDB 现在,在我们的 `main.ts` 文件中,我们将导入 mongoose 和 `Dinosaur` 模式,并连接到 MongoDB: ```ts import mongoose from "npm:mongoose@latest"; import Dinosaur from "./model/Dinosaur.ts"; const MONGODB_URI = Deno.env.get("MONGODB_URI") ?? "mongodb://localhost:27017/deno_mongoose_tutorial"; await mongoose.connect(MONGODB_URI); console.log(mongoose.connection.readyState); ``` 因为 Deno 支持顶级 `await`,我们能够简单地 `await mongoose.connect()`。 运行代码,我们应该期待得到一个日志 `1`: ```shell deno run --allow-env --allow-net main.ts ``` ## 操作数据 让我们在 `/model/Dinosaur.ts` 中为我们的 `Dinosaur` 模式添加一个实例 [方法](https://mongoosejs.com/docs/guide.html#methods): ```ts title="model/Dinosaur.ts" dinosaurSchema.methods.updateDescription = async function ( this: HydratedDocument, description: string, ) { this.description = description; return await this.save(); }; // ... ``` 这个实例方法 `updateDescription` 将允许您更新记录的描述。 回到 `main.ts`,让我们开始在 MongoDB 中添加和操作数据。 ```ts title="main.ts" const deno = new Dinosaur({ name: "Deno", description: "有史以来最快的恐龙。", }); await deno.save(); const denoFromMongoDb = await Dinosaur.findOne({ name: "Deno" }).exec(); if (!denoFromMongoDb) throw new Error("未找到 Deno"); console.log( `在 MongoDB 中查找 Deno -- \n ${denoFromMongoDb.name}: ${denoFromMongoDb.description}`, ); await denoFromMongoDb.updateDescription( "有史以来最快和最安全的恐龙。", ); const newDenoFromMongoDb = await Dinosaur.findOne({ name: "Deno" }).exec(); if (!newDenoFromMongoDb) throw new Error("更新后未找到 Deno"); console.log( `再次查找 Deno -- \n ${newDenoFromMongoDb.name}: ${newDenoFromMongoDb.description}`, ); ``` 运行代码,我们得到: ```console 在 MongoDB 中查找 Deno -- Deno: 有史以来最快的恐龙。 再次查找 Deno -- Deno: 有史以来最快和最安全的恐龙。 ``` 太棒了!🦕 现在你已经拥有了一个完整的 Deno 应用程序,使用 Mongoose 与 MongoDB 交互! 有关使用 Mongoose 的更多信息,请参考 [他们的文档](https://mongoosejs.com/docs/guide.html)。 --- # 如何在 Deno 中使用 MySQL2 > Step-by-step guide to using MySQL2 with Deno. Learn how to set up database connections, execute queries, handle transactions, and build data-driven applications using MySQL's Node.js driver. URL: https://docs.deno.com/examples/mysql2_tutorial/ [MySQL](https://www.mysql.com/) 是在 [2022年 Stack Overflow 开发者调查](https://survey.stackoverflow.co/2022/#most-popular-technologies-database) 中最受欢迎的数据库,并且有 Facebook、Twitter、YouTube 和 Netflix 等用户。 [在这里查看源代码。](https://github.com/denoland/examples/tree/main/with-mysql2) 你可以使用 `mysql2` node 包通过 `npm:mysql2` 在 Deno 中操作和查询 MySQL 数据库。这使我们能够使用其 Promise 包装器,并利用顶层 await。 ```tsx import mysql from "npm:mysql2@^2.3.3/promise"; ``` ## 连接到 MySQL 我们可以使用 `createConnection()` 方法连接到我们的 MySQL 服务器。你需要指定主机(在测试时为 `localhost`,或在生产中更可能是云数据库端点)以及用户和密码: ```tsx const connection = await mysql.createConnection({ host: "localhost", user: "root", password: "password", }); ``` 你还可以在创建连接时可选指定一个数据库。在这里,我们将使用 `mysql2` 动态创建数据库。 ## 创建和填充数据库 现在你已经建立了连接,可以使用 `connection.query()` 和 SQL 命令来创建数据库和表,以及插入初始数据。 首先,我们想要生成并选择要使用的数据库: ```tsx await connection.query("CREATE DATABASE denos"); await connection.query("use denos"); ``` 然后我们想要创建表: ```tsx await connection.query( "CREATE TABLE `dinosaurs` ( `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY, `name` varchar(255) NOT NULL, `description` varchar(255) )", ); ``` 表创建后,我们可以填充数据: ```tsx await connection.query( "INSERT INTO `dinosaurs` (id, name, description) VALUES (1, 'Aardonyx', 'An early stage in the evolution of sauropods.'), (2, 'Abelisaurus', 'Abel's lizard has been reconstructed from a single skull.'), (3, 'Deno', 'The fastest dinosaur that ever lived.')", ); ``` 现在我们有了所有数据,可以开始查询。 ## 查询 MySQL 我们可以使用相同的 connection.query() 方法来编写我们的查询。首先,我们尝试获取 `dinosaurs` 表中的所有数据: ```tsx const results = await connection.query("SELECT * FROM `dinosaurs`"); console.log(results); ``` 此查询的结果是我们数据库中的所有数据: ```tsx [ [ { id: 1, name: "Aardonyx", description: "An early stage in the evolution of sauropods." }, { id: 2, name: "Abelisaurus", description: `Abel's lizard has been reconstructed from a single skull.` }, { id: 3, name: "Deno", description: "The fastest dinosaur that ever lived." } ], ``` 如果我们只想从数据库中获取单个元素,可以更改我们的查询: ```tsx const [results, fields] = await connection.query( "SELECT * FROM `dinosaurs` WHERE `name` = 'Deno'", ); console.log(results); ``` 这将给我们一个单行结果: ```tsx [{ id: 3, name: "Deno", description: "The fastest dinosaur that ever lived." }]; ``` 最后,我们可以关闭连接: ```tsx await connection.end(); ``` 想要了解更多关于 `mysql2` 的信息,请查看他们的文档 [这里](https://github.com/sidorares/node-mysql2)。 --- # 构建一个 Next.js 应用 > 使用 Deno 构建 Next.js 应用的分步教程。学习如何设置项目,创建 API 路由,实现服务器端渲染,并构建一个全栈 TypeScript 应用。 URL: https://docs.deno.com/examples/next_tutorial/ [Next.js](https://nextjs.org/) 是一个流行的用于构建服务器端渲染应用的框架。它基于 React 构建,并开箱即用提供了很多功能。 在本教程中,我们将构建一个 [简单的 Next.js 应用](https://tutorial-with-next.deno.deno.net/) 并使用 Deno 运行它。该应用会显示一个恐龙列表。当你点击其中一个时,会跳转到对应恐龙的详情页面。 你可以查看 [GitHub 上的完整应用代码](https://github.com/denoland/tutorial-with-next/tree/main)。 :::info 部署你自己的应用 想跳过教程,立即部署完成的应用?点击下面按钮,立刻将完整的 SvelteKit 恐龙应用副本部署到 Deno Deploy。你将获得一个可运行、可自定义、可修改的实时代码! [![Deploy on Deno](https://deno.com/button)](https://console.deno.com/new?clone=https://github.com/denoland/tutorial-with-next) ::: ## 使用 Deno 创建一个 Next.js 应用 Next 提供了一个 CLI 工具,可以快速创建新的 Next.js 应用。在终端运行以下命令,使用 Deno 创建新的 Next.js 应用: ```sh deno run -A npm:create-next-app@latest ``` 当提示时,选择默认选项以创建带有 TypeScript 的新 Next.js 应用。 Next.js 有些依赖仍然依赖于 `Object.prototype.__proto__`,并且需要 CommonJS 模块支持。为让 Deno 兼容 Next.js,更新你的 `deno.json` 文件,使用以下配置: ```json deno.json { "unstable": [ "bare-node-builtins", "detect-cjs", "node-globals", "unsafe-proto", "sloppy-imports" ] } ``` 现在你可以运行你的新的 Next.js 应用: ```sh deno task dev ``` 这将使用 Deno 启动 Next.js 开发服务器。`deno task dev` 命令会带有必要标志启动支持 CommonJS 的 Next.js 开发服务器。 访问 [http://localhost:3000](http://localhost:3000) 在浏览器查看应用。 ## 添加后台服务 下一步是添加后台 API。我们将创建一个非常简单的 API,返回关于恐龙的信息。 我们会使用 Next.js 内置的 [API 路由处理](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) 来设置恐龙 API。Next.js 使用基于文件系统的路由,文件夹结构直接定义路由。 我们将定义三个路由,第一个 `/api` 路由返回字符串 `Welcome to the dinosaur API`,然后 `/api/dinosaurs` 返回所有恐龙的数据,最后 `/api/dinosaurs/[dinosaur]` 根据 URL 中的名称返回特定恐龙。 ### /api/ 在新项目的 `src/app` 文件夹中创建一个 `api` 文件夹,在该文件夹中创建 `route.ts` 文件,用于处理 `/api` 路由。 将以下代码复制粘贴到 `api/route.ts` 文件中: ```ts title="route.ts" export function GET() { return Response.json("welcome to the dinosaur API"); } ``` 此代码定义了一个简单的路由处理器,返回包含字符串 `欢迎来到恐龙 API` 的 JSON 响应。 ### /api/data.json 在 `api` 文件夹中,创建一个 `data.json` 文件,内含硬编码的恐龙数据。将 [这个 JSON 文件](https://raw.githubusercontent.com/denoland/deno-vue-example/main/api/data.json) 复制粘贴到 `data.json` 文件中。 ### /api/dinosaurs 在 `api` 文件夹中,创建一个名为 `dinosaurs` 的文件夹,在其中创建一个 `route.ts` 文件来处理 `/api/dinosaurs` 请求。该路由将读取 `data.json` 文件,并返回所有恐龙的 JSON 数据: ```ts title="route.ts" import data from "./data.json" with { type: "json" }; export function GET() { return Response.json(data); } ``` ### /api/dinosaurs/[dinosaur] 对于最后一个路由 `/api/dinosaurs/[dinosaur]`,在 `dinosaurs` 目录中创建一个 `[dinosaur]` 文件夹。在其中创建 `route.ts` 文件。该文件将读取 `data.json`,根据 URL 中的名称查找对应恐龙并以 JSON 返回: ```ts title="route.ts" import data from "../../data.json" with { type: "json" }; type RouteParams = { params: Promise<{ dinosaur: string }> }; export const GET = async (_request: Request, { params }: RouteParams) => { const { dinosaur } = await params; if (!dinosaur) { return Response.json("未提供恐龙名称。"); } const dinosaurData = data.find((item) => item.name.toLowerCase() === dinosaur.toLowerCase() ); return Response.json(dinosaurData ? dinosaurData : "未找到该恐龙。"); }; ``` 现在,如果你运行应用并访问 `http://localhost:3000/api/dinosaurs/brachiosaurus`,应该能看到关于腕龙的详细信息。 ## 构建前端 现在我们已经设置了后台 API,接着构建前端页面来展示恐龙数据。 ### 定义恐龙类型 首先,我们添加一个类型定义,描述恐龙数据结构。在 `app` 目录中创建 `types.ts` 文件,添加以下代码: ```ts title="types.ts" export type Dino = { name: string; description: string }; ``` ### 更新首页 修改 `app` 目录中的 `page.tsx` 文件,从我们 API 获取恐龙数据,并以链接列表形式显示。 Next.js 中如果有客户端代码,需要在文件顶部添加 `"use client"` 指令。然后导入该页面需要的模块,并导出渲染页面的默认函数: ```tsx title="page.tsx" "use client"; import { useEffect, useState } from "react"; import { Dino } from "./types"; import Link from "next/link"; export default function Home() { } ``` 在 `Home` 函数体内,定义一个状态变量存储恐龙数据,并使用 `useEffect` 钩子在组件挂载时从 API 拉取数据: ```tsx title="page.tsx" const [dinosaurs, setDinosaurs] = useState([]); useEffect(() => { (async () => { const response = await fetch(`/api/dinosaurs`); const allDinosaurs = await response.json() as Dino[]; setDinosaurs(allDinosaurs); })(); }, []); ``` 接着,在 `Home` 函数体内返回一个链接列表,每个链接指向对应恐龙页面: ```tsx title="page.tsx" return (

Welcome to the Dinosaur app

Click on a dinosaur below to learn more.

    {dinosaurs.map((dinosaur: Dino) => { return (
  • {dinosaur.name}
  • ); })}
); ``` ### 创建恐龙详情页面 在 `app` 目录下创建名为 `[dinosaur]` 的文件夹,里面创建 `page.tsx` 文件。该文件从 API 获取特定恐龙详情并渲染。 和首页类似,我们添加客户端代码导入,并导出默认函数,参数入参类型化: ```tsx title="[dinosaur]/page.tsx" "use client"; import { useEffect, useState } from "react"; import { Dino } from "../types"; import Link from "next/link"; type RouteParams = { params: Promise<{ dinosaur: string }> }; export default function Dinosaur({ params }: RouteParams) { } ``` 在 `Dinosaur` 函数中,获取 URL 中选定恐龙名称,定义状态变量存储恐龙信息,创建 `useEffect` 钩子挂载时从 API 获取数据: ```tsx title="[dinosaur]/page.tsx" const selectedDinosaur = params.then((params) => params.dinosaur); const [dinosaur, setDino] = useState({ name: "", description: "" }); useEffect(() => { (async () => { const resp = await fetch(`/api/dinosaurs/${await selectedDinosaur}`); const dino = await resp.json() as Dino; setDino(dino); })(); }, []); ``` 最后,在组件中返回显示恐龙名称及描述的描述元素: ```tsx title="[dinosaur]/page.tsx" return (

{dinosaur.name}

{dinosaur.description}

🠠 返回所有恐龙
); ``` ### 添加样式 给应用添加基础样式以使界面更美观。更新 `app/globals.css` 文件,使用 [此文件中的样式](https://raw.githubusercontent.com/denoland/tutorial-with-next/refs/heads/main/src/app/globals.css)。 ## 运行应用 现在,你可以用 `deno run dev` 启动应用,然后在浏览器访问 `http://localhost:3000` 查看恐龙列表。点击恐龙可以看到更详细的信息! ## 部署应用 既然你的 Next.js 应用已运行,可以使用 Deno DeployEA 部署到线上。 最佳体验是直接从 GitHub 部署,自动设置持续部署。先创建一个 GitHub 仓库并上传应用。 [创建新的 GitHub 仓库](https://github.com/new),然后初始化并推送应用: ```sh git init -b main git remote add origin https://github.com//.git git add . git commit -am 'my next app' git push -u origin main ``` 一旦你的应用上线到 GitHub,你就可以 [部署到 Deno DeployEA](https://console.deno.com/)。 想了解部署详情,请查看 [Deno Deploy 教程](/examples/deno_deploy_tutorial/)。 🦕 现在你可以用 Deno 构建并运行一个 Next.js 应用了!想进一步扩展,可以考虑 [添加数据库](/runtime/tutorials/connecting_to_databases/) 替代 `data.json` 文件,或尝试 [编写测试](/runtime/fundamentals/testing/) 以确保应用稳定,准备好生产环境。 --- # 使用 Deno 构建 Nuxt 应用 > 逐步指南,教你如何使用 Deno 构建 Nuxt 应用。学习如何创建完整的 Vue.js 全栈应用,实现服务器端渲染,添加 Tailwind 样式,并部署你的应用。 URL: https://docs.deno.com/examples/nuxt_tutorial/ [Nuxt](https://nuxt.com/) 是一个基于 [Vue](https://vuejs.org/) 的直观框架, 提供了文件路由、多种渲染选项和开箱即用的自动代码拆分。凭借其模块化架构,Nuxt 通过提供结构化的开发方式简化了 Vue 应用的构建流程。 在本教程中,我们将使用 Deno 构建一个简单的 Nuxt 应用,显示恐龙列表,并允许你点击名字查看更多恐龙信息。 你可以在 [GitHub 上查看完整示例](https://github.com/denoland/examples/tree/main/with-nuxt)。 也可以体验 [Deno Deploy 上的在线示例](https://example-with-nuxt.deno.deno.net/)。 :::info 部署你的应用 想跳过教程,立即部署完整的 Nuxt 恐龙应用?点击下面按钮,瞬间将应用部署到 Deno Deploy。 你将获得一个可用的实时应用,可在学习时自由定制和修改! [![Deploy on Deno](https://deno.com/button)](https://console.deno.com/new?clone=https://github.com/denoland/examples&path=with-nuxt) ::: ## 使用 Deno 脚手架 Nuxt 应用 通过 Deno 创建新的 Nuxt 项目: ```bash deno -A npm:nuxi@latest init ``` 选择创建项目的目录,并选择 `deno` 作为依赖管理方式。你也可以选择初始化 git 仓库,也可以之后再做。 然后进入新项目目录,运行 `deno task` 查看 Nuxt 可用任务: ```bash cd nuxt-app deno task ``` 这会显示可用任务,如 `dev`、`build` 和 `preview`。`dev` 用于启动开发服务器。 ## 启动开发服务器 启动开发服务器: ```bash deno task dev ``` 这会启动 Nuxt 开发服务器,在浏览器访问 [http://localhost:3000](http://localhost:3000) 查看默认 Nuxt 欢迎页面。 ## 构建应用架构 基础 Nuxt 应用搭建完成后,开始建立应用架构。我们创建几个目录以组织代码,并为后续功能做准备。项目内创建如下目录: ```bash NUXT-APP/ ├── pages/ # Vue 页面 │ └── dinosaurs/ # 恐龙页面 ├── public/ # 静态文件 ├── server/ # 服务器端代码 │ └── api/ # API 路由 ``` ## 添加恐龙数据 在 `api` 目录下创建 `data.json` 文件,用于存储硬编码的恐龙数据。 复制粘贴 [此 JSON 文件](https://raw.githubusercontent.com/denoland/tutorial-with-nuxt/refs/heads/main/src/data/data.json) 到 `data.json` 文件中。(实际应用中,通常从数据库或外部 API 获取数据。) ## 设置 API 路由 应用将包含两个 API 路由,分别提供: - 供索引页面使用的完整恐龙列表 - 单个恐龙页面的详细恐龙信息 路由均为 `*.get.ts` 文件,Nuxt 会自动根据文件生成响应 `GET` 请求的 API 端点。 [文件命名决定 HTTP 方法及路由路径](https://nuxt.com/docs/guide/directory-structure/server#matching-http-method)。 初始的 `dinosaurs.get.ts` 十分简单,使用 [`defineCachedEventHandler`](https://nitro.build/guide/cache) 创建缓存端点提升性能。该函数直接返回完整恐龙数据数组,无任何过滤: ```tsx title="server/api/dinosaurs.get.ts" import data from "./data.json" with { type: "json" }; export default defineCachedEventHandler(() => { return data; }); ``` 单个恐龙的 `GET` 路由逻辑较多。它从事件上下文中取出名称参数,以不区分大小写方式匹配请求恐龙,缺少或错误时返回相应错误。我们创建 `dinosaurs` 文件夹,并新建 `[name].get.ts`: ```tsx title="server/api/dinosaurs/[name].get.ts" import data from "../data.json"; export default defineCachedEventHandler((event) => { const name = getRouterParam(event, "name"); if (!name) { throw createError({ statusCode: 400, message: "未提供恐龙名称", }); } const dinosaur = data.find( (dino) => dino.name.toLowerCase() === name.toLowerCase(), ); if (!dinosaur) { throw createError({ statusCode: 404, message: "未找到该恐龙", }); } return dinosaur; }); ``` 启动服务器 `deno task dev`,在浏览器访问 [http://localhost:3000/api/dinosaurs](http://localhost:3000/api/dinosaurs),你应能看到包含所有恐龙的原始 JSON 响应! ![设置 API](./images/how-to/nuxt/nuxt-1.webp) 访问某个恐龙的特定 URL,如: [http://localhost:3000/api/dinosaurs/aardonyx](http://localhost:3000/api/dinosaurs/aardonyx),查看单个恐龙数据。 ![设置 API](./images/how-to/nuxt/nuxt-2.webp) 接下来,设置 Vue 前端以显示索引页面和单个恐龙页面。 ## 设置 Vue 前端 我们需要两个页面: - 一个索引页,列出全部恐龙 - 一个单独页,展示指定恐龙详情 首先,创建索引页面。Nuxt 使用 [文件系统路由](https://nuxt.com/docs/getting-started/routing),我们在根目录创建 `pages` 文件夹,并新建 `index.vue` 作为索引页。 利用 `useFetch` 组合函数请求先前创建的 API 端点: ```tsx title="pages/index.vue" ``` 然后,为显示单个恐龙信息,创建动态页面 `[name].vue`。该页面使用 Nuxt 的 [动态路由参数](https://nuxt.com/docs/getting-started/routing#route-parameters),文件名中的 `[name]` 在 JavaScript 中通过 `route.params.name` 访问。我们用 `useRoute` 访问参数,并用 `useFetch` 根据名称获取指定恐龙数据: ```tsx title="pages/[name].vue" ``` 接下来,将这些 Vue 组件串联起来,使访问根目录时能正确渲染。更新根目录的 `app.vue`,提供应用根组件。使用 [`NuxtLayout`](https://nuxt.com/docs/api/components/nuxt-layout) 保持一致结构,`NuxtPage` 用于动态页面渲染: ```tsx title="app.vue" ; ``` 运行 `deno task dev`,在 [http://localhost:3000](http://localhost:3000) 查看效果: 效果很棒! ```bash deno install -D npm:tailwindcss npm:@tailwindcss/vite ``` 随后,更新 `nuxt.config.ts`。导入 Tailwind 依赖并配置 Nuxt 应用以支持 Deno,启用开发工具,设置 Tailwind CSS: ```tsx title="nuxt.config.ts" import tailwindcss from "@tailwindcss/vite"; export default defineNuxtConfig({ compatibilityDate: "2025-05-15", devtools: { enabled: true }, nitro: { preset: "deno", }, app: { head: { title: "恐龙百科全书", }, }, css: ["~/assets/css/main.css"], vite: { plugins: [ tailwindcss(), ], }, }); ``` 然后,创建新的 CSS 文件 `assets/css/main.css`,添加导入语句引入 tailwind 及其实用类: ```tsx title="assets/css/main.css" @import "tailwindcss"; @tailwind base; @tailwind components; @tailwind utilities; ``` ## 运行应用 最后,通过以下命令运行应用: ```bash deno task dev ``` 应用将在 localhost:3000 启动:
完成了! 🦕 Nuxt 应用的下一步可以是使用 [Nuxt Auth](https://auth.nuxtjs.org/) 模块添加认证,集成 [Pinia](https://pinia.vuejs.org/) 状态管理,添加服务器端数据持久化(例如 [Prisma](https://docs.deno.com/examples/prisma_tutorial/) 或 [MongoDB](https://docs.deno.com/examples/mongoose_tutorial/)),以及搭建 Vitest 自动化测试,这些都将使应用更适合生产环境和大型项目。 --- # 处理操作系统信号 > Tutorial on handling operating system signals in Deno. Learn how to capture SIGINT and SIGBREAK events, manage signal listeners, and implement graceful shutdown handlers in your applications. URL: https://docs.deno.com/examples/os_signals_tutorial/ > ⚠️ 从 Deno v1.23 开始,Windows 仅支持监听 SIGINT 和 SIGBREAK。 ## 概念 - [Deno.addSignalListener()](https://docs.deno.com/api/deno/~/Deno.addSignalListener) 可用于捕获和监视操作系统信号。 - [Deno.removeSignalListener()](https://docs.deno.com/api/deno/~/Deno.removeSignalListener) 可用于停止监听信号。 ## 设置操作系统信号监听器 处理操作系统信号的 API 是基于已经熟悉的 [`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) 和 [`removeEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) API 模型化的。 > ⚠️ 请注意,监听操作系统信号并不会阻止事件循环完成,即如果没有其他挂起的异步操作,进程将退出。 你可以使用 `Deno.addSignalListener()` 函数来处理操作系统信号: ```ts title="add_signal_listener.ts" console.log("按 Ctrl-C 触发 SIGINT 信号"); Deno.addSignalListener("SIGINT", () => { console.log("被中断!"); Deno.exit(); }); // 添加一个超时以防止进程立即退出。 setTimeout(() => {}, 5000); ``` 运行命令: ```shell deno run add_signal_listener.ts ``` 你可以使用 `Deno.removeSignalListener()` 函数注销之前添加的信号处理程序。 ```ts title="signal_listeners.ts" console.log("按 Ctrl-C 触发 SIGINT 信号"); const sigIntHandler = () => { console.log("被中断!"); Deno.exit(); }; Deno.addSignalListener("SIGINT", sigIntHandler); // 添加一个超时以防止进程立即退出。 setTimeout(() => {}, 5000); // 在 1 秒后停止监听信号。 setTimeout(() => { Deno.removeSignalListener("SIGINT", sigIntHandler); }, 1000); ``` 运行命令: ```shell deno run signal_listeners.ts ``` --- # 在 Deno 中使用上下文传播实现分布式追踪 > 在 Deno 应用中实现端到端的分布式追踪及自动上下文传播。本教程涵盖创建追踪服务、追踪上下文的自动传播,以及分布式追踪的可视化。 URL: https://docs.deno.com/examples/otel_span_propagation_tutorial/ 现代应用通常构建为多个服务相互通信的分布式系统。在调试问题或优化性能时,能够追踪请求在不同服务间的流动至关重要。这正是分布式追踪的作用所在。 自 Deno 2.3 起,运行时自动在服务边界间保留追踪上下文,使得分布式系统中的端到端追踪变得更加简单和强大。这意味着当一个服务向另一个服务发起请求时,追踪上下文会自动传播,让你能够将整个请求流程视为一个完整的追踪。 ## 搭建分布式系统 我们的示例系统由两部分组成: 1. 一个提供 API 端点的服务器 2. 一个向服务器发起请求的客户端 ### 服务器 我们搭建一个简单的 HTTP 服务器,用于响应 GET 请求并返回 JSON 消息: ```ts title="server.ts" import { trace } from "npm:@opentelemetry/api@1"; const tracer = trace.getTracer("api-server", "1.0.0"); // 使用 Deno.serve 创建一个简单的 API 服务器 Deno.serve({ port: 8000 }, (req) => { return tracer.startActiveSpan("process-api-request", async (span) => { // 向 span 添加属性以提供更多上下文 span.setAttribute("http.route", "/"); span.updateName("GET /"); // 添加一个 span 事件以便在追踪中查看 span.addEvent("processing_request", { request_id: crypto.randomUUID(), timestamp: Date.now(), }); // 模拟处理时间 await new Promise((resolve) => setTimeout(resolve, 50)); console.log("Server: Processing request in trace context"); // 操作完成后结束 span span.end(); return new Response(JSON.stringify({ message: "Hello from server!" }), { headers: { "Content-Type": "application/json" }, }); }); }); ``` ### 客户端 现在,让我们创建一个客户端向服务器发起请求: ```ts title="client.ts" import { SpanStatusCode, trace } from "npm:@opentelemetry/api@1"; const tracer = trace.getTracer("api-client", "1.0.0"); // 为客户端操作创建一个父 span await tracer.startActiveSpan("call-api", async (parentSpan) => { try { console.log("Client: Starting API call"); // 该 span 内的 fetch 调用将自动: // 1. 为 fetch 操作创建子 span // 2. 将追踪上下文注入到外发请求头中 const response = await fetch("http://localhost:8000/"); const data = await response.json(); console.log(`Client: Received response: ${JSON.stringify(data)}`); parentSpan.addEvent("received_response", { status: response.status, timestamp: Date.now(), }); } catch (error) { console.error("Error calling API:", error); if (error instanceof Error) { parentSpan.recordException(error); } parentSpan.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error), }); } finally { parentSpan.end(); } }); ``` ## 使用 OpenTelemetry 进行追踪 客户端和服务器代码中已包含基础的 OpenTelemetry 仪表化: 1. 创建 tracer —— 两个文件均使用 `trace.getTracer()` 创建跟踪器,包含名称和版本信息。 2. 创建 span —— 我们使用 `startActiveSpan()` 创建表示操作的 span。 3. 添加上下文 —— 通过向 span 添加属性和事件提供更多上下文信息。 4. 结束 span —— 在操作完成时确保结束 span。 ## 自动上下文传播 关键步骤发生在客户端向服务器发起请求时。客户端代码中的 fetch 调用: ```ts const response = await fetch("http://localhost:8000/"); ``` 由于此 fetch 调用位于活动 span 内,Deno 会自动为 fetch 操作创建子 span,并将追踪上下文注入到外发请求头中。 服务器接收请求后,会从请求头中提取追踪上下文,并将服务器的 span 设为客户端 span 的子 span。 ## 运行示例 运行此示例,首先启动服务器,为你的 otel 服务命名: ```sh OTEL_DENO=true OTEL_SERVICE_NAME=server deno run --allow-net server.ts ``` 然后,在另一个终端运行客户端,为客户端服务指定不同的名称,以便更清晰地观察传播: ```sh OTEL_DENO=true OTEL_SERVICE_NAME=client deno run --allow-net client.ts ``` 你应当看到: 1. 客户端打印日志 "Client: Starting API call" 2. 服务器打印日志 "Server: Processing request in trace context" 3. 客户端打印接收到的响应内容 ## 查看追踪 要查看追踪,需要配置 OpenTelemetry 收集器和可视化工具,例如 [Grafana Tempo](/runtime/fundamentals/open_telemetry/#quick-start)。 可视化追踪时,你会看到: 1. 客户端的父 span 2. 连接到 HTTP 请求的子 span 3. 连接到服务器端的 span 4. 全部组成一个完整的追踪! 例如,在 Grafana 中的追踪可视化可能长这样: ![Viewing expanded traces in Grafana](./images/how-to/grafana/propagation.png) 🦕 现在你已经了解了 Deno 中的分布式追踪,接下来可以将这应用扩展到包含更多服务和异步操作的复杂系统中。 借助 Deno 的自动上下文传播,实现分布式追踪比以往任何时候都更简单! --- # 如何使用 Planetscale 与 Deno > Step-by-step guide to using Planetscale with Deno. Learn how to set up serverless MySQL databases, manage connections, execute queries, and build scalable applications with Planetscale's developer-friendly platform. URL: https://docs.deno.com/examples/planetscale_tutorial/ Planetscale 是一个与 MySQL 兼容的无服务器数据库,旨在为开发者提供工作流程,使开发者可以通过命令行创建、分支和部署数据库。 [在此处查看源码。](https://github.com/denoland/examples/tree/main/with-planetscale) 我们将使用 Planetscale 无服务器驱动程序 `@planetscale/database` 来与 Deno 配合使用。首先我们想要创建 `main.ts` 并从该包中导入连接方法: ```tsx import { connect } from "npm:@planetscale/database@^1.4"; ``` ## 配置我们的连接 连接需要三种凭据:主机、用户名和密码。这些都是特定于数据库的,因此我们首先需要在 Planetscale 中创建一个数据库。您可以按照最初的说明[在这里](https://planetscale.com/docs/tutorials/planetscale-quick-start-guide)进行操作。别担心添加模式—我们可以通过 `@planetscale/database` 来完成这一点。 一旦您创建了数据库,前往概览,点击“连接”,选择“使用 `@planetscale/database` 连接”以获取主机和用户名。然后点击“密码”创建一个新的数据库密码。一旦您拥有这三项,您可以直接输入它们,或者更好的是,将它们存储为环境变量: ```bash export HOST= export USERNAME= export PASSWORD= ``` 然后使用 `Deno.env` 调用它们: ```tsx const config = { host: Deno.env.get("HOST"), username: Deno.env.get("USERNAME"), password: Deno.env.get("PASSWORD"), }; const conn = connect(config); ``` 如果您在仪表板中设置了环境变量,这在 Deno Deploy 上也可以工作。运行命令: ```shell deno run --allow-net --allow-env main.ts ``` 现在 `conn` 对象是一个与我们的 Planetscale 数据库的开放连接。 ## 创建和填充我们的数据库表 现在您已成功建立连接,可以使用 SQL 命令通过 `conn.execute()` 创建表并插入初始数据: ```tsx await conn.execute( "CREATE TABLE dinosaurs (id int NOT NULL AUTO_INCREMENT PRIMARY KEY, name varchar(255) NOT NULL, description varchar(255) NOT NULL);", ); await conn.execute( "INSERT INTO `dinosaurs` (id, name, description) VALUES (1, 'Aardonyx', 'An early stage in the evolution of sauropods.'), (2, 'Abelisaurus', 'Abels lizard has been reconstructed from a single skull.'), (3, 'Deno', 'The fastest dinosaur that ever lived.')", ); ``` ## 查询 Planetscale 我们也可以使用同样的 `conn.execute()` 来编写查询。让我们获取所有恐龙的列表: ```tsx const results = await conn.execute("SELECT * FROM `dinosaurs`"); console.log(results.rows); ``` 结果为: ```tsx [ { id: 1, name: "Aardonyx", description: "An early stage in the evolution of sauropods.", }, { id: 2, name: "Abelisaurus", description: "Abels lizard has been reconstructed from a single skull.", }, { id: 3, name: "Deno", description: "The fastest dinosaur that ever lived." }, ]; ``` 我们也可以通过指定恐龙名称来仅获取数据库中的一行: ```tsx const result = await conn.execute( "SELECT * FROM `dinosaurs` WHERE `name` = 'Deno'", ); console.log(result.rows); ``` 这将给我们一个单行结果: ```tsx [{ id: 3, name: "Deno", description: "The fastest dinosaur that ever lived." }]; ``` 您可以在他们的[文档](https://planetscale.com/docs)中了解更多关于使用 Planetscale 的信息。 --- # 如何使用 Prisma 和 Oak 创建 RESTful API > 使用 Prisma 和 Oak 以及 Deno 构建 RESTful API 的指南。学习如何设置数据库模式、生成客户端、实现 CRUD 操作,并以合适的类型安全性部署您的 API。 URL: https://docs.deno.com/examples/prisma_tutorial/ [Prisma](https://prisma.io) 一直以来都是我们在 Deno 中最受欢迎的模块之一。鉴于 Prisma 的开发者体验非常出色,并且与众多持久数据存储技术兼容,需求是可以理解的。 我们很高兴向您展示如何在 Deno 中使用 Prisma。 在本教程中,我们将使用 Oak 和 Prisma 在 Deno 中设置一个简单的 RESTful API。 让我们开始吧。 [查看源代码](https://github.com/denoland/examples/tree/main/with-prisma) 或者 [查看视频教程](https://youtu.be/P8VzA_XSF8w)。 ## 设置应用程序 首先创建文件夹 `rest-api-with-prisma-oak` 并导航到该文件夹: ```shell mkdir rest-api-with-prisma-oak cd rest-api-with-prisma-oak ``` 然后,使用 Deno 运行 `prisma init`: ```shell npx prisma@latest init --generator-provider prisma-client --output ./generated ``` 让我们了解关键参数: - `--generator-provider prisma-client`: 定义生成器提供者为 `"prisma-client"`,而非默认的 `"prisma-client-js"`。该 `"prisma-client"` 提供者针对 Deno 进行了优化,会生成兼容 Deno 运行时的 TypeScript 代码。 - `--output`: 定义 Prisma 将保存生成客户端文件的目录,包括类型定义和数据库访问工具。 这将生成 [`prisma/schema.prisma`](https://www.prisma.io/docs/orm/prisma-schema)。让我们用以下内容更新它: :::tip 不要忘记在你的 schema.prisma 文件中的生成器块中添加 `runtime = "deno"`。这是 Prisma 正确与 Deno 一起工作所必需的。 ::: ```ts generator client { provider = "prisma-client" output = "./generated" runtime = "deno" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model Dinosaur { id Int @id @default(autoincrement()) name String @unique description String } ``` Prisma 还会生成一个 `.env` 文件,其中包含 `DATABASE_URL` 环境变量。让我们将 `DATABASE_URL` 设置为 PostgreSQL 连接字符串。在本例中,我们将使用免费的 [Supabase PostgreSQL 数据库](https://supabase.com/database)。 接下来,创建数据库模式: ```shell deno run -A npm:prisma@latest db push ``` 完成后,我们需要生成 Prisma Client: ```shell deno run -A npm:prisma@latest generate ``` ## 在 Prisma 数据平台中设置 Accelerate > 注意:此步骤为可选。Prisma Accelerate 并非基础功能所必需。 开始使用 Prisma 数据平台: 1. 注册免费 [Prisma 数据平台账户](https://console.prisma.io)。 2. 创建一个项目。 3. 进入您创建的项目。 4. 通过提供数据库的连接字符串启用 Accelerate。 5. 生成 Accelerate 连接字符串并复制到剪贴板。 将以 `prisma://` 开头的 Accelerate 连接字符串赋值给 `.env` 文件中的 `DATABASE_URL`,替换现有连接字符串。 接着,让我们创建一个种子脚本来初始化数据库。 ## 为数据库添加种子数据 创建 `./prisma/seed.ts` 文件: ```shell touch prisma/seed.ts ``` 在 `./prisma/seed.ts` 中: ```ts import { Prisma, PrismaClient } from "./generated/client.ts"; const prisma = new PrismaClient({ datasourceUrl: process.env.DATABASE_URL, }); const dinosaurData: Prisma.DinosaurCreateInput[] = [ { name: "Aardonyx", description: "在蜥脚类动物演化的早期阶段。", }, { name: "Abelisaurus", description: "阿贝尔的蜥蜴是根据一个单独的头骨重建的。", }, { name: "Acanthopholis", description: "不,这不是希腊的一个城市。", }, ]; /** * 给数据库加种子。 */ for (const u of dinosaurData) { const dinosaur = await prisma.dinosaur.create({ data: u, }); console.log(`创建了一个 ID 为 ${dinosaur.id} 的恐龙`); } console.log(`种子填充完成。`); await prisma.$disconnect(); ``` 现在我们可以运行 `seed.ts`: ```shell deno run -A --env prisma/seed.ts ``` :::tip `--env` 标志用于告诉 Deno 从 `.env` 文件加载环境变量。 ::: 完成后,您应该能够通过运行以下命令在 Prisma Studio 中看到您的数据: ```bash deno run -A npm:prisma studio ``` 您应该会看到与以下屏幕截图相似的内容: ![新恐龙出现在 Prisma 控制台](./images/how-to/prisma/1-dinosaurs-in-prisma.png) ## 创建您的 API 路由 我们将使用 [`oak`](https://jsr.io/@oak/oak) 来创建 API 路由。现在让我们保持简单。 首先创建一个 `main.ts` 文件: ```shell touch main.ts ``` 然后在您的 `main.ts` 文件中: ```ts import { PrismaClient } from "./prisma/generated/client.ts"; import { Application, Router } from "jsr:@oak/oak"; /** * 初始化。 */ const prisma = new PrismaClient({ datasources: { db: { url: process.env.DATABASE_URL, }, }, }); const app = new Application(); const router = new Router(); /** * 设置路由。 */ router .get("/", (context) => { context.response.body = "欢迎来到恐龙 API!"; }) .get("/dinosaur", async (context) => { // 获取所有恐龙。 const dinosaurs = await prisma.dinosaur.findMany(); context.response.body = dinosaurs; }) .get("/dinosaur/:id", async (context) => { // 根据 ID 获取一只恐龙。 const { id } = context.params; const dinosaur = await prisma.dinosaur.findUnique({ where: { id: Number(id), }, }); context.response.body = dinosaur; }) .post("/dinosaur", async (context) => { // 创建一只新的恐龙。 const { name, description } = await context.request.body.json(); const result = await prisma.dinosaur.create({ data: { name, description, }, }); context.response.body = result; }) .delete("/dinosaur/:id", async (context) => { // 根据 ID 删除一只恐龙。 const { id } = context.params; const dinosaur = await prisma.dinosaur.delete({ where: { id: Number(id), }, }); context.response.body = dinosaur; }); /** * 设置中间件。 */ app.use(router.routes()); app.use(router.allowedMethods()); /** * 启动服务器。 */ await app.listen({ port: 8000 }); ``` 现在,我们来运行它: ```shell deno run -A --env main.ts ``` 现在我们访问 `localhost:8000/dinosaur`: ![REST API 提供的所有恐龙的列表](./images/how-to/prisma/2-dinosaurs-from-api.png) 接下来,让我们用这个 `curl` 命令 `POST` 新用户: ```shell curl -X POST http://localhost:8000/dinosaur -H "Content-Type: application/json" -d '{"name": "Deno", "description":"有史以来走在地球上最快、最安全、最易用的恐龙。" }' ``` 您现在应该能在 Prisma Studio 中看到一行新数据: ![新恐龙 Deno 在 Prisma 中](./images/how-to/prisma/3-new-dinosaur-in-prisma.png) 很好! ## 接下来是什么? 使用 Deno 和 Prisma 构建您的下一个应用程序将更加高效和有趣,因为这两种技术都提供直观的开发者体验,包括数据建模、类型安全和强大的 IDE 支持。 如果您有兴趣将 Prisma 连接到 Deno Deploy,请 [查看这个很棒的指南](https://www.prisma.io/docs/guides/deployment/deployment-guides/deploying-to-deno-deploy)。 --- # Build Qwik with Deno > Step-by-step guide to building Qwik applications with Deno. Learn about resumability, server-side rendering, route handling, and how to create fast, modern web applications with zero client-side JavaScript by default. URL: https://docs.deno.com/examples/qwik_tutorial/ [Qwik](https://qwik.dev/) 是一个 JavaScript 框架,通过利用可恢复性而不是水合,提供即时加载的 Web 应用程序。在本教程中,我们将构建一个简单的 Qwik 应用程序,并使用 Deno 运行它。该应用将显示恐龙列表。当你点击其中一只恐龙时,会将你带到一个包含更多细节的恐龙页面。 我们将讨论如何使用 Deno 构建一个简单的 Qwik 应用: - [搭建一个 Qwik 应用](#scaffold-a-qwik-app) - [设置数据和类型定义](#setup-data-and-type-definitions) - [构建前端](#build-the-frontend) - [后续步骤](#next-steps) 可以直接跳转到 [源码](https://github.com/denoland/examples/tree/main/with-qwik),或者继续往下阅读! ## 搭建一个 Qwik 应用 我们可以通过 Deno 创建一个新的 Qwik 项目,如下所示: ```bash deno init --npm qwik@latest ``` 这将指导你完成 Qwik 和 Qwik City 的设置过程。在这里,我们选择了最简单的“空应用”部署,具有 npm 依赖项。 完成后,你将拥有如下的项目结构: ``` . ├── node_modules/ ├── public/ └── src/ ├── components/ │ └── router-head/ │ └── router-head.tsx └── routes/ ├── index.tsx ├── layout.tsx ├── service-worker.ts ├── entry.dev.tsx ├── entry.preview.tsx ├── entry.ssr.tsx ├── global.css └── root.tsx ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── package-lock.json ├── package.json ├── qwik.env.d.ts ├── README.md ├── tsconfig.json └── vite.config.ts ``` 大部分都是我们不会触碰的样板配置。了解 Qwik 如何工作的几个重要文件包括: - `src/components/router-head/router-head.tsx`:管理你的 Qwik 应用中不同路由的 HTML 头元素(例如标题、元标签等)。 - `src/routes/index.tsx`:应用的主要入口点和用户访问根 URL 时看到的主页。 - `src/routes/layout.tsx`:定义包裹页面的常见布局结构,使你能够维护一致的 UI 元素,如页眉和页脚。 - `src/routes/service-worker.ts`:处理渐进式 Web 应用(PWA)功能、离线缓存和应用的后台任务。 - `src/routes/entry.ssr.tsx`:控制你的应用如何进行服务器端渲染,管理初始 HTML 生成和水合过程。 - `src/routes/root.tsx`:作为应用程序的外壳的根组件,包含全局提供者和主要路由结构。 现在我们可以在应用程序中构建自己的路由和文件。 ## 设置数据和类型定义 我们将首先将我们的 [恐龙数据](https://github.com/denoland/examples/blob/main/with-qwik/src/data/dinosaurs.json) 添加到新的 `./src/data` 目录中,命名为 `dinosaurs.json`: ```jsonc // ./src/data/dinosaurs.json { "dinosaurs": [ { "name": "霸王龙", "description": "一种巨大的肉食性恐龙,拥有强大的下颚和小巧的手臂。" }, { "name": "腕龙", "description": "一种巨大的草食性恐龙,拥有非常长的脖子。" }, { "name": "迅猛龙", "description": "一种小型但凶猛的掠食者,通常以群体狩猎。" } // ... ] } ``` 这就是我们的数据将从中提取的地方。在完整的应用中,这些数据将来自数据库。 > ⚠️️ 在本教程中,我们硬编码了数据。但是你可以连接到 > [多种数据库](https://docs.deno.com/runtime/tutorials/connecting_to_databases/) 和 > [甚至使用 ORM,如 Prisma](https://docs.deno.com/runtime/tutorials/how_to_with_npm/prisma/)与 > Deno。 接下来,让我们为我们的恐龙数据添加类型定义。我们将其放在 `./src/types.ts` 中: ```tsx // ./src/types.ts export type Dino = { name: string; description: string; }; ``` 接下来,让我们添加 API 路由来提供这些数据。 ## 添加 API 路由 首先,让我们创建一个路由,以加载主页的所有恐龙。此 API 端点使用 Qwik City 的 [`RequestHandler`](https://qwik.dev/docs/advanced/request-handling/) 创建一个 `GET` 端点,加载并返回我们的恐龙数据,并使用 json 帮助器进行适当的响应格式化。我们将在 `./src/routes/api/dinosaurs/index.ts` 中添加以下内容: ```tsx // ./src/routes/api/dinosaurs/index.ts import { RequestHandler } from "@builder.io/qwik-city"; import data from "~/data/dinosaurs.json" with { type: "json" }; export const onGet: RequestHandler = async ({ json }) => { const dinosaurs = data; json(200, dinosaurs); }; ``` 接下来,让我们创建一个 API 路由,以获取单个恐龙的信息。这个路由将从 URL 获取参数,并用它来在我们的恐龙数据中进行搜索。我们将以下代码添加到 `./src/routes/api/dinosaurs/[name]/index.ts`: ```tsx // ./src/routes/api/dinosaurs/[name]/index.ts import { RequestHandler } from "@builder.io/qwik-city"; import data from "~/data/dinosaurs.json" with { type: "json" }; export const onGet: RequestHandler = async ({ params, json }) => { const { name } = params; const dinosaurs = data; if (!name) { json(400, { error: "未提供恐龙名称。" }); return; } const dinosaur = dinosaurs.find( (dino) => dino.name.toLowerCase() === name.toLowerCase(), ); if (!dinosaur) { json(404, { error: "未找到恐龙。" }); return; } json(200, dinosaur); }; ``` 现在 API 路由已连接并提供数据,让我们创建两个前端页面:主页和单个恐龙详情页。 ## 构建前端 我们将通过更新 `./src/routes/index.tsx` 文件来创建我们的主页,使用 Qwik 的 [`routeLoader$`](https://qwik.dev/docs/route-loader/) 进行服务器端数据获取。这个 `component$` 在 SSR 期间通过 `useDinosaurs()` 加载并渲染恐龙数据: ```tsx // ./src/routes/index.tsx import { component$ } from "@builder.io/qwik"; import { Link, routeLoader$ } from "@builder.io/qwik-city"; import type { Dino } from "~/types"; import data from "~/data/dinosaurs.json" with { type: "json" }; export const useDinosaurs = routeLoader$(() => { return data; }); export default component$(() => { const dinosaursSignal = useDinosaurs(); return (

欢迎来到恐龙应用

点击下面的恐龙以了解更多信息。

    {dinosaursSignal.value.dinosaurs.map((dinosaur: Dino) => (
  • {dinosaur.name}
  • ))}
); }); ``` 现在我们有了主要的主页,让我们为单个恐龙信息添加一个页面。我们将使用 Qwik 的 [动态路由](https://qwik.dev/docs/routing/),以 `[name]` 作为每个恐龙的关键字。此页面利用 `routeLoader$` 根据 URL 参数获取单个恐龙的详细信息,并内置错误处理以防无法找到恐龙。 该组件与我们的主页使用相同的 SSR 模式,但使用基于参数的数据加载,并为单个恐龙详情显示一个更简单的布局: ```tsx // ./src/routes/[name]/index.tsx import { component$ } from "@builder.io/qwik"; import { Link, routeLoader$ } from "@builder.io/qwik-city"; import type { Dino } from "~/types"; import data from "~/data/dinosaurs.json" with { type: "json" }; export const useDinosaurDetails = routeLoader$(({ params }): Dino => { const { dinosaurs } = data; const dinosaur = dinosaurs.find( (dino: Dino) => dino.name.toLowerCase() === params.name.toLowerCase(), ); if (!dinosaur) { throw new Error("未找到恐龙"); } return dinosaur; }); export default component$(() => { const dinosaurSignal = useDinosaurDetails(); return (

{dinosaurSignal.value.name}

{dinosaurSignal.value.description}

返回所有恐龙
); }); ``` 现在我们已经构建了我们的路由和前端组件,我们可以运行我们的应用: ```bash deno task dev ``` 这将启动应用程序,地址为 `localhost:5173`:
完成! ## 后续步骤 🦕 现在你可以使用 Deno 构建和运行 Qwik 应用!以下是一些可以增强你恐龙应用的方法: 下一步可能是使用 Qwik 的延迟加载功能来加载恐龙图像和其他组件,或为复杂功能添加客户端状态管理。 - 添加持久数据存储 [使用像 Postgres 或 MongoDB 的数据库](https://docs.deno.com/runtime/tutorials/connecting_to_databases/) 和像 [Drizzle](https://docs.deno.com/examples/drizzle_tutorial/) 或 [Prisma](https://docs.deno.com/runtime/tutorials/how_to_with_npm/prisma/) 的 ORM - 使用 Qwik 的延迟加载功能来处理恐龙图像和组件 - 添加客户端状态管理 - 将你的应用自托管到 [AWS](https://docs.deno.com/runtime/tutorials/aws_lightsail/), [Digital Ocean](https://docs.deno.com/runtime/tutorials/digital_ocean/), 和 [Google Cloud Run](https://docs.deno.com/runtime/tutorials/google_cloud_run/) --- # 使用 Vite 构建 React 应用 > Deno 和 Vite 构建 React 应用的完整指南。学习如何搭建项目、实现路由、添加 API 接口以及部署你的全栈 TypeScript 应用。 URL: https://docs.deno.com/examples/react_tutorial/ [React](https://reactjs.org) 是最广泛使用的 JavaScript 前端库。 在本教程中,我们将使用 Deno 构建一个简单的 React 应用。该应用会展示一列恐龙列表。点击其中一个时,会跳转到该恐龙详情页面。你可以查看 [完成版应用的 GitHub 仓库](https://github.com/denoland/tutorial-with-react) 以及 [Deno Deploy 上的应用演示](https://tutorial-with-react.deno.deno.net/) :::info 部署你自己的应用 想跳过教程,立即部署完成版应用?点击下面按钮,立即部署一份完整的 SvelteKit 恐龙应用的副本到 Deno Deploy。你将获得一个可用的实时应用,便于你在学习过程中自定义和修改! [![Deploy on Deno](https://deno.com/button)](https://console.deno.com/new?clone=https://github.com/denoland/tutorial-with-react&mode=dynamic&entrypoint=api/main.ts&build=deno+task+build&install=deno+install) ::: ## 使用 Vite 创建基础 React 应用 本教程将使用 [Vite](https://vitejs.dev/) 在本地提供服务。 Vite 是现代 Web 项目的构建工具和开发服务器。它与 React 和 Deno 配合良好,利用 ES 模块,允许你直接导入 React 组件。 在终端中运行下面命令,使用 TypeScript 模板创建新的 React 应用: ```sh $ deno init --npm vite my-react-app --template react-ts ``` ## 运行开发服务器 切换到新创建的 React 应用目录并安装依赖: ```sh cd deno install ``` 现在你可以通过下面命令启动新 React 应用的服务: ```sh deno run dev ``` 这会启动 Vite 服务器,点击输出中的 localhost 链接,在浏览器中查看你的应用。 ## 配置项目 我们将构建一个带有 Deno 后端的全栈 React 应用。需要配置 Vite 和 Deno 以实现协同工作。 安装 Vite 的 deno 插件、React 类型声明和 Vite React 插件: ```sh deno add npm:@deno/vite-plugin@latest npm:@types/react@latest npm:@vitejs/plugin-react@latest ``` 还需安装 Deno 的 Oak Web 框架处理 API 请求,以及 CORS 中间件允许来自 React 应用的跨域请求: ```sh deno add jsr:@oak/oak jsr:@tajpouria/cors ``` 这会将依赖写入新的 `deno.json` 文件。 我们还将在该文件中添加几个任务,方便在开发与生产模式下运行,并添加配置使 Deno 支持 React 和 Vite。将如下内容添加到你的 `deno.json` 文件: ```json "tasks": { "dev": "deno run -A npm:vite & deno run server:start", "build": "deno run -A npm:vite build", "server:start": "deno run -A --watch ./api/main.ts", "serve": "deno run build && deno run server:start" }, "nodeModulesDir": "auto", "compilerOptions": { "types": [ "react", "react-dom", "@types/react" ], "lib": [ "dom", "dom.iterable", "deno.ns" ], "jsx": "react-jsx", "jsxImportSource": "react" } ``` 你可以同时使用 `package.json` 和 `deno.json` 来管理依赖和配置,也可以删除 `package.json`,只使用 `deno.json`,但需先把 `package.json` 中的依赖迁移到 `deno.json` 的 imports。 ## 添加后端 API 我们的项目将拥有提供恐龙数据的后端 API。该 API 使用 Deno 和 Oak 构建,提供获取恐龙列表和特定恐龙详细信息的接口,数据来自一个 JSON 文件。实际生产中一般会是数据库,这里用静态 JSON 文件作为示例。 在项目根目录新建 `api` 文件夹。该目录下新建 `data.json` 文件,复制 [恐龙数据](https://github.com/denoland/tutorial-with-react/blob/main/api/data.json) 进该文件。 接着在 `api` 目录中创建 `main.ts` 文件,含处理 API 请求的 Oak 服务器代码。先导入依赖,创建 Oak 应用和路由器: ```ts title="api/main.ts" import { Application, Router } from "@oak/oak"; import { oakCors } from "@tajpouria/cors"; import routeStaticFilesFrom from "./util/routeStaticFilesFrom.ts"; import data from "./data.json" with { type: "json" }; export const app = new Application(); const router = new Router(); ``` 然后定义两个主要 API 路由: ```ts title="api/main.ts" router.get("/api/dinosaurs", (context) => { context.response.body = data; }); router.get("/api/dinosaurs/:dinosaur", (context) => { if (!context?.params?.dinosaur) { context.response.body = "未提供恐龙名称。"; } const dinosaur = data.find((item) => item.name.toLowerCase() === context.params.dinosaur.toLowerCase() ); context.response.body = dinosaur ?? "未找到恐龙。"; }); ``` 最后配置服务器中间件并启动监听: ```ts title="api/main.ts" app.use(oakCors()); app.use(router.routes()); app.use(router.allowedMethods()); app.use(routeStaticFilesFrom([ `${Deno.cwd()}/dist`, `${Deno.cwd()}/public`, ])); if (import.meta.main) { console.log("服务器正在 http://localhost:8000 上监听"); await app.listen({ port: 8000 }); } ``` 服务器配置了 CORS,提供 API 路由,并从 `dist`(构建产物)和 `public` 目录提供静态文件。 ## 提供静态文件 Oak 服务器还会提供构建后的 React 应用。我们需要配置它从 Vite 输出的 `dist` 目录提供静态文件。这里用工具函数 `routeStaticFilesFrom` 实现。在 `api` 目录下新建 `util/routeStaticFilesFrom.ts` 文件,内容如下: ```ts title="api/util/routeStaticFilesFrom.ts" import { Context, Next } from "jsr:@oak/oak"; export default function routeStaticFilesFrom(staticPaths: string[]) { return async (context: Context>, next: Next) => { for (const path of staticPaths) { try { await context.send({ root: path, index: "index.html" }); return; } catch { continue; } } await next(); }; } ``` 该函数尝试从给定路径中提供静态文件,找不到时调用下一个中间件。它会提供 `dist` 目录中的 `index.html`,即 React 应用的入口。 你可以通过运行 `deno run dev`,再在浏览器访问 `localhost:8000/api/dinosaurs` 测试 API,查看返回的所有恐龙 JSON 数据。 ## React 应用设置 ### 入口文件 React 应用入口在 `src/main.tsx`,这里无需修改,但值得关注这里将 React 应用挂载到 DOM。`react-dom/client` 中的 `createRoot` 将 `App` 组件渲染到 `index.html` 中的 `root` 节点。以下为 `src/main.tsx` 代码: ```tsx title="src/main.tsx" import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./App.tsx"; createRoot(document.getElementById("root")!).render( , ); ``` ## 添加路由 应用将有两个路由:`/` 和 `/:dinosaur`。 在 `src/App.tsx` 中设置路由: ```tsx title="src/App.tsx" import { BrowserRouter, Route, Routes } from "react-router-dom"; import Index from "./pages/index.tsx"; import Dinosaur from "./pages/Dinosaur.tsx"; function App() { return ( } /> } /> ); } export default App; ``` ## 代理以转发 API 请求 Vite 在端口 `3000` 提供 React 应用,API 在端口 `8000`。我们需要在 `vite.config.ts` 配置代理,将 API 请求转发: ```ts title="vite.config.ts" import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import deno from "@deno/vite-plugin"; export default defineConfig({ server: { port: 3000, proxy: { "/api": { target: "http://localhost:8000", changeOrigin: true, }, }, }, plugins: [react(), deno()], optimizeDeps: { include: ["react/jsx-runtime"], }, }); ``` ## 创建页面 创建新目录 `pages`,再创建两个文件 `src/pages/index.tsx` 和 `src/pages/Dinosaur.tsx`。 `Index` 页面列出所有恐龙,`Dinosaur` 页面展示单个恐龙详情。 ### index.tsx 该页面从 API 获取恐龙列表,渲染成链接: ```tsx title="src/pages/index.tsx" import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; export default function Index() { const [dinosaurs, setDinosaurs] = useState([]); useEffect(() => { (async () => { const response = await fetch(`/api/dinosaurs/`); const allDinosaurs = await response.json(); setDinosaurs(allDinosaurs); })(); }, []); return (

🦕 Dinosaur app

点击下方恐龙,了解详情。

{dinosaurs.map((dinosaur: { name: string; description: string }) => { return ( {dinosaur.name} ); })}
); } ``` ### Dinosaur.tsx 该页面从 API 获取特定恐龙详情,并显示: ```tsx title="src/pages/Dinosaur.tsx" import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; export default function Dinosaur() { const { selectedDinosaur } = useParams(); const [dinosaur, setDino] = useState({ name: "", description: "" }); useEffect(() => { (async () => { const resp = await fetch(`/api/dinosaurs/${selectedDinosaur}`); const dino = await resp.json(); setDino(dino); })(); }, [selectedDinosaur]); return (

{dinosaur.name}

{dinosaur.description}

🠠 返回所有恐龙
); } ``` ### 美化你的应用 我们为你编写了 [基础样式](https://raw.githubusercontent.com/denoland/tutorial-with-react/refs/heads/main/src/index.css),可以复制到 `src/index.css`。 ## 运行应用 运行应用,使用 `deno.json` 中定义的开发任务: ```sh deno run dev ``` 此命令将: 1. 在 3000 端口启动 Vite 开发服务器 2. 在 8000 端口启动 API 服务器 3. 配置代理,将前端 `/api` 请求转发给后端 在浏览器访问 `localhost:3000`,你应能看到恐龙应用,点击列表查看详情。 ## 理解项目结构 看看项目关键文件与目录: ```text tutorial-with-react/ ├── api/ # 后端 API │ ├── data.json # 恐龙数据(700+ 种) │ ├── main.ts # 带 API 路由的 Oak 服务器 │ └── util/ │ └── routeStaticFilesFrom.ts ├── src/ # React 前端 │ ├── main.tsx # React 应用入口 │ ├── App.tsx # 主应用及路由 │ ├── index.css # 全局样式 │ └── pages/ │ ├── index.tsx # 含恐龙列表的首页 │ └── Dinosaur.tsx # 单个恐龙页面 ├── public/ # 静态资源 ├── deno.json # Deno 配置和任务 ├── package.json # Vite 的 npm 依赖 ├── vite.config.ts # 带代理的 Vite 配置 └── index.html # HTML 模板 ``` ### 关键概念 1. **混合依赖管理**:项目同时使用 Deno 和 npm 依赖。服务器端依赖(如 Oak)用 Deno 管理,前端依赖则由 Vite 通过 npm 管理。 2. **开发与生产环境**:开发时,Vite 在 3000 端口提供 React 应用,并代理 API 请求到 8000 端口的 Oak 服务器。生产时,Oak 服务器在 8000 端口同时提供构建好的 React 应用和 API。 3. **现代 React 模式**:应用使用 React 18(注:React 19 尚未发布,文中应为 React 18),函数组件,Hooks 和 React Router 进行导航。 4. **类型安全**:示例中未使用独立类型文件,但大型项目里建议为数据结构创建 TypeScript 接口。 你可以在 [Deno Deploy 运行的应用](https://tutorial-with-react.deno.deno.net/) 看到示例。 ## 构建和部署 我们配置了一个 `serve` 任务,构建 React 应用并由 Oak 后端提供。运行以下命令以生产模式构建和启动: ```sh deno run build deno run serve ``` 此操作会: 1. 使用 Vite 构建 React 应用(输出到 `dist/`) 2. 启动 Oak 服务器,提供 API 及构建后的 React 应用 浏览器访问 `localhost:8000` 查看生产版本! 你可以将该应用部署到喜欢的云平台。推荐使用 [Deno Deploy](https://deno.com/deploy),部署方便快捷。只需创建 GitHub 仓库并推送代码,然后连接到 Deno Deploy 即可。 ### 创建 GitHub 仓库 [创建新的 GitHub 仓库](https://github.com/new),初始化并推送应用代码: ```sh git init -b main git remote add origin https://github.com//.git git add . git commit -am 'my react app' git push -u origin main ``` ### 部署到 Deno Deploy 代码上传 GitHub 后,你可以 [将其部署至 Deno Deploy](https://console.deno.com/)。 完整部署流程请参考 [Deno Deploy 教程](/examples/deno_deploy_tutorial/)。 🦕 现在你已准备好用 Vite 和 Deno 脚手架搭建和开发 React 应用!你可以构建超高速 Web 应用。希望你喜欢探索这些前沿工具,期待看到你的作品! --- # 如何在 Deno 中使用 Redis > Step-by-step guide to using Redis with Deno. Learn how to set up caching, implement message brokers, handle data streaming, and optimize your applications with Redis's in-memory data store. URL: https://docs.deno.com/examples/redis_tutorial/ [Redis](https://redis.io/) 是一个内存数据存储,你可以用它来缓存、作为消息代理或用于流数据处理。 [在这里查看源代码。](https://github.com/denoland/examples/tree/main/with-redis) 在这里,我们将设置 Redis 来缓存 API 调用的数据,从而加快对该数据后续请求的响应速度。我们将会: - 设置一个 Redis 客户端,将每个 API 调用的数据保存在内存中 - 设置一个 Deno 服务器,以便我们可以方便地请求特定数据 - 在服务器处理程序中调用 Github API 来在第一次请求时获取数据 - 在每一次后续请求中从 Redis 提供数据 我们可以在一个文件 `main.ts` 中完成这一切。 ## 连接到 Redis 客户端 我们需要两个模块。第一个是 Deno 服务器。我们将使用这个模块来获取用户的信息以查询我们的 API。第二个是 Redis。我们可以使用 `npm:` 修饰符获取 Redis 的节点包: ```tsx import { createClient } from "npm:redis@^4.5"; ``` 我们使用 `createClient` 创建一个 Redis 客户端并连接到我们的本地 Redis 服务器: ```tsx // 连接到本地的 Redis 实例 const client = createClient({ url: "redis://localhost:6379", }); await client.connect(); ``` 你还可以在这个 [配置](https://github.com/redis/node-redis/blob/master/docs/client-configuration.md) 对象中单独设置主机、用户、密码和端口。 ## 设置服务器 我们的服务器将作为 Github API 的一个封装。客户端可以通过 URL 路径名调用我们的服务器,格式为 `http://localhost:3000/{username}`。 解析路径名并调用 Github API 将在我们服务器的处理函数内进行。我们去掉了前导斜杠,这样就得到了一个可以传递给 Github API 作为用户名的变量。然后我们将响应返回给用户。 ```tsx Deno.serve({ port: 3000 }, async (req) => { const { pathname } = new URL(req.url); // 去掉前导斜杠 const username = pathname.substring(1); const resp = await fetch(`https://api.github.com/users/${username}`); const user = await resp.json(); return new Response(JSON.stringify(user), { headers: { "content-type": "application/json", }, }); }); ``` 我们用下面的命令来运行它: ```tsx deno run --allow-net main.ts ``` 如果我们在 Postman 中访问 [http://localhost:3000/ry](http://localhost:3000/ry),我们将获得 Github 的响应: ![uncached-redis-body.png](./images/how-to/redis/uncached-redis-body.png) 让我们使用 Redis 来缓存这个响应。 ## 检查缓存 一旦我们从 Github API 得到响应,我们可以使用 `client.set` 将其缓存到 Redis 中,将我们的用户名作为键,用户对象作为值: ```tsx await client.set(username, JSON.stringify(user)); ``` 下次请求相同的用户名时,我们可以使用 `client.get` 来获取缓存的用户: ```tsx const cached_user = await client.get(username); ``` 如果密钥不存在,这将返回 null。因此我们可以在某些流程控制中使用它。当我们得到用户名时,我们将首先检查是否已经在缓存中拥有该用户。如果有,我们将提供缓存的结果。如果没有,我们将调用 Github API 获取用户,缓存它,然后提供 API 结果。在这两种情况下,我们将添加一个自定义头来显示我们正在提供的版本: ```tsx const server = new Server({ handler: async (req) => { const { pathname } = new URL(req.url); // 去掉前导斜杠 const username = pathname.substring(1); const cached_user = await client.get(username); if (cached_user) { return new Response(cached_user, { headers: { "content-type": "application/json", "is-cached": "true", }, }); } else { const resp = await fetch(`https://api.github.com/users/${username}`); const user = await resp.json(); await client.set(username, JSON.stringify(user)); return new Response(JSON.stringify(user), { headers: { "content-type": "application/json", "is-cached": "false", }, }); } }, port: 3000, }); server.listenAndServe(); ``` 第一次运行这段代码我们将获得与上述相同的响应,并且我们将看到 `is-cached` 头被设置为 `false`: ![uncached-redis-header.png](./images/how-to/redis/uncached-redis-header.png) 但当再次使用相同的用户名调用时,我们得到了缓存的结果。内容是相同的: ![cached-redis-body.png](./images/how-to/redis/cached-redis-body.png) 但头部显示我们有缓存: ![cached-redis-header.png](./images/how-to/redis/cached-redis-header.png) 我们还可以看到响应快了大约 200 毫秒! 你可以在 [这里](https://redis.io/docs/) 查看 Redis 文档,以及在 [这里](https://github.com/redis/node-redis) 查看 Redis 节点包。 --- # Run a script > A guide to creating and running basic scripts with Deno. Learn how to write and execute JavaScript and TypeScript code, understand runtime environments, and get started with fundamental Deno concepts. URL: https://docs.deno.com/examples/run_script_tutorial/ Deno 是一个安全的 JavaScript 和 TypeScript 运行时。 运行时是代码执行的环境。它提供了程序运行所需的基础设施,处理内存管理、I/O 操作以及与外部资源的交互等事务。运行时负责将高层次的代码(JavaScript 或 TypeScript)转换为计算机可以理解的机器指令。 当你在网页浏览器中运行 JavaScript(如 Chrome、Firefox 或 Edge)时,你正在使用浏览器运行时。 浏览器运行时与浏览器本身紧密耦合。它们提供了用于操作文档对象模型(DOM)、处理事件、发起网络请求等的 API。这些运行时是沙箱化的,它们在浏览器的安全模型中运行。它们无法访问浏览器之外的资源,比如文件系统或环境变量。 当你使用 Deno 运行代码时,你是在主机上直接执行你的 JavaScript 或 TypeScript 代码,而不是在浏览器的上下文中。因此,Deno 程序可以访问主机计算机上的资源,比如文件系统、环境变量和网络套接字。 Deno 提供了无缝的 JavaScript 和 TypeScript 代码运行体验。无论你喜欢 JavaScript 的动态特性还是 TypeScript 的类型安全,Deno 都能满足你的需求。 ## 运行脚本 在本教程中,我们将使用 Deno 创建一个简单的 "Hello World" 示例,分别用 JavaScript 和 TypeScript 来演示。 我们将定义一个 `capitalize` 函数,该函数将单词的首字母大写。然后,我们定义一个 `hello` 函数,该函数返回带有大写名字的问候消息。最后,我们用不同的名字调用 `hello` 函数并将输出打印到控制台。 ### JavaScript 首先,创建一个 `hello-world.js` 文件并添加以下代码: ```js title="hello-world.js" function capitalize(word) { return word.charAt(0).toUpperCase() + word.slice(1); } function hello(name) { return "Hello " + capitalize(name); } console.log(hello("john")); console.log(hello("Sarah")); console.log(hello("kai")); ``` 使用 `deno run` 命令运行脚本: ```sh $ deno run hello-world.js Hello John Hello Sarah Hello Kai ``` ### TypeScript 这个 TypeScript 示例与上面的 JavaScript 示例完全相同,代码只是增加了 TypeScript 支持的类型信息。 创建一个 `hello-world.ts` 文件并添加以下代码: ```ts title="hello-world.ts" function capitalize(word: string): string { return word.charAt(0).toUpperCase() + word.slice(1); } function hello(name: string): string { return "Hello " + capitalize(name); } console.log(hello("john")); console.log(hello("Sarah")); console.log(hello("kai")); ``` 使用 `deno run` 命令运行 TypeScript 脚本: ```sh $ deno run hello-world.ts Hello John Hello Sarah Hello Kai ``` 🦕 恭喜你!现在你知道如何用 JavaScript 和 TypeScript 创建一个简单的脚本,并且如何使用 `deno run` 命令在 Deno 中运行它。继续探索教程和示例,以了解更多有关 Deno 的信息! --- # 简单的 API 服务器 URL: https://docs.deno.com/examples/simple_api_tutorial/ Deno 使得仅使用 Web 平台的基础(Request、Response、fetch)及内置数据存储 KV,构建轻量级、标准化的 HTTP API 变得简单。在本教程中,你将构建并部署一个基于 Deno KV 的小型链接缩短器,然后将其推送到 Deno Deploy 的生产环境。 我们将使用 [Deno KV](/deploy/kv/manual) 实现一个简单的链接缩短服务。现代的 Deno 运行时提供 `Deno.serve()`,可以零配置启动 HTTP 服务器。 ## 创建本地 API 服务器 为你的项目创建一个新目录,并运行 `deno init` 创建一个基础的 Deno 项目。 将 `main.ts` 文件更新为以下代码: ```ts title="main.ts" const kv = await Deno.openKv(); interface CreateLinkBody { slug: string; url: string; } function json(body: unknown, init: ResponseInit = {}) { const headers = new Headers(init.headers); headers.set("content-type", "application/json; charset=utf-8"); return new Response(JSON.stringify(body), { ...init, headers }); } function isValidSlug(slug: string) { return /^[a-zA-Z0-9-_]{1,40}$/.test(slug); } export function handler(req: Request): Promise | Response { return (async () => { // 基础 CORS 支持(可选 - 不需要时可移除) if (req.method === "OPTIONS") { return new Response(null, { headers: { "access-control-allow-origin": "*", "access-control-allow-methods": "GET,POST,OPTIONS", "access-control-allow-headers": "content-type", }, }); } if (req.method === "POST") { let body: CreateLinkBody; try { body = await req.json(); } catch { return json({ error: "无效的 JSON 请求体" }, { status: 400 }); } const { slug, url } = body; if (!slug || !url) { return json({ error: "'slug' 和 'url' 是必填项" }, { status: 400, }); } if (!isValidSlug(slug)) { return json({ error: "无效的 slug 格式" }, { status: 422 }); } try { new URL(url); } catch { return json({ error: "'url' 必须是绝对 URL" }, { status: 422, }); } // 使用原子操作防止覆盖已存在的 slug const key = ["links", slug]; const txResult = await kv.atomic().check({ key, versionstamp: null }).set( key, url, ).commit(); if (!txResult.ok) { return json({ error: "Slug 已存在" }, { status: 409 }); } return json({ slug, url }, { status: 201 }); } // 重定向短链 - 从路径名中提取 slug const slug = new URL(req.url).pathname.slice(1); // 去除开头的 '/' if (!slug) { return json({ message: "请在路径中提供 slug,或者通过 POST 创建一个。", }, { status: 400 }); } const result = await kv.get<[string] | string>(["links", slug]); const target = result.value as string | null; if (!target) { return json({ error: "未找到该 slug" }, { status: 404 }); } return Response.redirect(target, 301); })(); } export function startServer(port = 8000) { return Deno.serve({ port }, handler); } startServer(); ``` ## 本地运行和测试服务器 更新 `deno.json` 文件中的 `dev` 任务,授予网络权限并添加 `--unstable-kv` 标志,以允许本地使用 Deno KV: ```json title="deno.json" { "tasks": { "dev": "deno run --unstable-kv -N main.ts" } } ``` 现在可以用以下命令运行服务器: ```sh deno task dev ``` > 为了快速迭代,你也可以授予全部权限(`-A`),而不仅仅是网络权限(`-N`),但不建议在生产环境这样做。 ### 测试你的 API 服务器 服务器将响应 HTTP `GET` 和 `POST` 请求。`POST` 处理器期望请求体中是包含 `slug` 和 `url` 属性的 JSON 文档。`slug` 是短链接组件,`url` 是你希望跳转到的完整 URL。 下面用 cURL 创建短链接的示例(期望返回 201 Created): ```shell curl --header "Content-Type: application/json" \ --request POST \ --data '{"url":"https://docs.deno.com/","slug":"denodocs"}' \ http://localhost:8000/ ``` 响应中,服务器返回描述存储链接的 JSON: ```json { "slug": "denodocs", "url": "https://docs.deno.com/" } ``` 如果你重复执行上述 curl 创建相同 slug,将收到 409 Conflict: ```json { "error": "Slug 已存在" } ``` 对服务器的 `GET` 请求使用 URL slug 作为路径参数,并重定向到对应的 URL。你可以在浏览器访问,也可以用 cURL 来测试效果: ```shell curl -v http://localhost:8000/denodocs ``` ## 部署你的 API 服务器 ::: Deno Deploy 账号必需 你需要有一个 Deno Deploy 账号才能完成本节内容。如果还没有,[注册一个免费的 Deno Deploy 账号](https://console.deno.com/)。 ::: ### 在 Deno Deploy 上预置 KV 数据库 首先,我们在 Deno Deploy 上为部署的应用“预置”一个 KV 数据库。 1. 访问 [Deno Deploy](https://console.deno.com/) 并点击“Databases”标签页。 2. 点击“+ Provision database”按钮。 3. 点击“Provision”按钮,创建免费的 KV 数据库。 4. 给数据库命名,选择地区,然后点击“Provision Database”。 ### 部署你的服务器 使用以下命令部署服务器: ```sh deno deploy ``` 它会短暂跳转到浏览器进行 Deno Deploy 账号认证,认证完成后返回终端。 1. 选择组织(如果你有多个组织)。 2. 选择“创建新应用”。 3. 回到浏览器给新项目命名。 4. 点击“Create App”。 5. 应用创建成功后,点击左侧“Timelines”菜单项。 6. 点击 Databases 部分旁的“Manage”。 7. 找到之前创建的 KV 数据库,点击“Assign”。 8. 选择刚创建的应用。 9. 点击“Assignments”列中应用名返回应用页面。 10. 点击最近一次部署的链接(会失败,因为尚未分配 KV)。 11. 点击“Retry Build”按钮,重新部署应用并关联 KV 数据库。 构建成功后,在“Overview”标签页可以看到生产环境 URL,你现在可以用 curl 命令测试部署好的 API 了。 ## 试用你刚刚的链接缩短器 无需额外配置(Deno KV 在 Deploy 环境下开箱即用),你的应用运行效果应当与本地完全相同。 你可以像以前一样使用 `POST` 添加新链接,只要把 `localhost` 部分替换为你部署的生产环境 URL: ```shell curl --header "Content-Type: application/json" \ --request POST \ --data '{"url":"https://docs.deno.com/runtime/","slug":"denodocs"}' \ https://your-project.yourusername.deno.net/ ``` 同样,你可以在浏览器访问缩短的 URL,或者用 cURL 获取跳转响应: ```shell curl -v https://your-project.yourusername.deno.net/denodocs ``` 🦕 现在你知道如何用 Deno 创建一个基础 API,并将它部署到 Deno Deploy。既然你的 url 缩短器已经可以使用了,可以考虑为其制作一个前端,允许用户创建和管理自己的短链接。可以参考我们的(web 框架)[/frameworks](/examples/#web-frameworks-and-libraries) 页面,获取如何开始的灵感! --- # Snapshot testing > Learn how to use snapshot testing in Deno to compare outputs against recorded references, making it easier to detect unintended changes in your code URL: https://docs.deno.com/examples/snapshot_test_tutorial/ Snapshot testing is a testing technique that captures the output of your code and compares it against a stored reference version. Rather than manually writing assertions for each property, you let the test runner record the entire output structure, making it easier to detect any unexpected changes. The [Deno Standard Library](/runtime/reference/std/) has a [snapshot module](https://jsr.io/@std/testing/doc/snapshot), which enables developers to write tests which assert a value against a reference snapshot. This reference snapshot is a serialized representation of the original value and is stored alongside the test file. ## Basic usage The `assertSnapshot` function will create a snapshot of a value and compare it to a reference snapshot, which is stored alongside the test file in the `__snapshots__` directory. To create an initial snapshot (or to update an existing snapshot), use the `-- --update` flag with the `deno test` command. ### Basic snapshot example The below example shows how to use the snapshot library with the `Deno.test` API. We can test a snapshot of a basic object, containing string and number properties. The `assertSnapshot(t, a)` function compares the object against a stored snapshot. The `t` parameter is the test context that Deno provides, which the snapshot function uses to determine the test name and location for storing snapshots. ```ts title="example_test.ts" import { assertSnapshot } from "jsr:@std/testing/snapshot"; Deno.test("isSnapshotMatch", async (t) => { const a = { hello: "world!", example: 123, }; await assertSnapshot(t, a); }); ``` You will need to grant read and write file permissions in order for Deno to write a snapshot file and then read it to test the assertion. If it is the first time you are running the test a do not already have a snapshot, add the `--update` flag: ```bash deno test --allow-read --allow-write -- --update ``` If you already have a snapshot file, you can run the test with: ```bash deno test --allow-read ``` The test will compare the current output of the object against the stored snapshot. If they match, the test passes; if they differ, the test fails. The snapshot file will look like this: ```ts title="__snapshots__/example_test.ts.snap" export const snapshot = {}; snapshot[`isSnapshotMatch 1`] = ` { example: 123, hello: "world!", } `; ``` You can edit your test to change the `hello` string to `"everyone!"` and run the test again with `deno test --allow-read`. This time the `assertSnapshot` function will throw an `AssertionError`, causing the test to fail because the snapshot created during the test does not match the one in the snapshot file. ## Updating snapshots When adding new snapshot assertions to your test suite, or when intentionally making changes which cause your snapshots to fail, you can update your snapshots by running the snapshot tests in update mode. Tests can be run in update mode by passing the `--update` or `-u` flag as an argument when running the test. When this flag is passed, then any snapshots which do not match will be updated. ```bash deno test --allow-read --allow-write -- --update ``` :::note New snapshots will only be created when the `--update` flag is present. ::: ## Permissions When running snapshot tests, the `--allow-read` permission must be enabled, or else any calls to `assertSnapshot` will fail due to insufficient permissions. Additionally, when updating snapshots, the `--allow-write` permission must be enabled, as this is required in order to update snapshot files. The assertSnapshot function will only attempt to read from and write to snapshot files. As such, the allow list for `--allow-read` and `--allow-write` can be limited to only include existing snapshot files, if desired. ## Version Control Snapshot testing works best when changes to snapshot files are committed alongside other code changes. This allows for changes to reference snapshots to be reviewed along side the code changes that caused them, and ensures that when others pull your changes, their tests will pass without needing to update snapshots locally. ## Options The `assertSnapshot` function can be called with an `options` object which offers greater flexibility and enables some non standard use cases: ```ts import { assertSnapshot } from "jsr:@std/testing/snapshot"; Deno.test("isSnapshotMatch", async (t) => { const a = { hello: "world!", example: 123, }; await assertSnapshot(t, a, {/*custom options go here*/}); }); ``` ### serializer When you run a test with `assertSnapshot`, the data you're testing needs to be converted to a string format that can be written to the snapshot file (when creating or updating snapshots) and compared with the existing snapshot (when validating), this is called serialization. The `serializer` option allows you to provide a custom serializer function. This custom function will be called by `assertSnapshot` and be passed the value being asserted. Your custom function must: 1. Return a `string` 2. Be deterministic, (it will always produce the same output, given the same input). The code below shows a practical example of creating and using a custom serializer function for snapshot testing. This serialiser removes any ANSI colour codes from a string using the [`stripColour`](https://jsr.io/@std/fmt/doc/colors) string formatter from the Deno Standard Library. ```ts title="example_test.ts" import { assertSnapshot, serialize } from "jsr:@std/testing/snapshot"; import { stripColor } from "jsr:@std/fmt/colors"; /** * Serializes `actual` and removes ANSI escape codes. */ function customSerializer(actual: string) { return serialize(stripColor(actual)); } Deno.test("Custom Serializer", async (t) => { const output = "\x1b[34mHello World!\x1b[39m"; await assertSnapshot(t, output, { serializer: customSerializer, }); }); ``` ```ts title="__snapshots__/example_test.ts.snap" snapshot = {}; snapshot[`Custom Serializer 1`] = `"Hello World!"`; ``` Custom serializers can be useful in a variety of scenarios: - To remove irrelevant formatting (like ANSI codes shown above) and improve legibility - To handle non-deterministic data. Timestamps, UUIDs, or random values can be replaced with placeholders - To mask or remove sensitive data that shouldn't be saved in snapshots - Custom formatting to present complex objects in a domain-specific format ### Serialization with `Deno.customInspect` Because the default serializer uses `Deno.inspect` under the hood, you can set the property `Symbol.for("Deno.customInspect")` to a custom serialization function if desired: ```ts title="example_test.ts" // example_test.ts import { assertSnapshot } from "jsr:@std/testing/snapshot"; class HTMLTag { constructor( public name: string, public children: Array = [], ) {} public render(depth: number) { const indent = " ".repeat(depth); let output = `${indent}<${this.name}>\n`; for (const child of this.children) { if (child instanceof HTMLTag) { output += `${child.render(depth + 1)}\n`; } else { output += `${indent} ${child}\n`; } } output += `${indent}`; return output; } public [Symbol.for("Deno.customInspect")]() { return this.render(0); } } Deno.test("Page HTML Tree", async (t) => { const page = new HTMLTag("html", [ new HTMLTag("head", [ new HTMLTag("title", [ "Simple SSR Example", ]), ]), new HTMLTag("body", [ new HTMLTag("h1", [ "Simple SSR Example", ]), new HTMLTag("p", [ "This is an example of how Deno.customInspect could be used to snapshot an intermediate SSR representation", ]), ]), ]); await assertSnapshot(t, page); }); ``` This test will produce the following snapshot. ```ts title="__snapshots__/example_test.ts.snap" export const snapshot = {}; snapshot[`Page HTML Tree 1`] = ` Simple SSR Example

Simple SSR Example

This is an example of how Deno.customInspect could be used to snapshot an intermediate SSR representation

`; ``` In contrast, when we remove the `Deno.customInspect` method, the test will produce the following snapshot: ```ts title="__snapshots__/example_test.ts.snap" export const snapshot = {}; snapshot[`Page HTML Tree 1`] = `HTMLTag { children: [ HTMLTag { children: [ HTMLTag { children: [ "Simple SSR Example", ], name: "title", }, ], name: "head", }, HTMLTag { children: [ HTMLTag { children: [ "Simple SSR Example", ], name: "h1", }, HTMLTag { children: [ "This is an example of how Deno.customInspect could be used to snapshot an intermediate SSR representation", ], name: "p", }, ], name: "body", }, ], name: "html", }`; ``` You can see that this second snapshot is much less readable. This is because: 1. The keys are sorted alphabetically, so the name of the element is displayed after its children 2. It includes a lot of extra information, causing the snapshot to be more than twice as long 3. It is not an accurate serialization of the HTML which the data represents Note that in this example it would be possible to achieve the same result by calling: ```ts await assertSnapshot(t, page.render(0)); ``` However, depending on the public API you choose to expose, this may not be practical. It is also worth considering that this could have an impact beyond your snapshot testing. For example, `Deno.customInspect` is also used to serialize objects when calling `console.log` (and in some other cases). This may or may not be desirable. ### `dir` and `path` The `dir` and `path` options allow you to control where the snapshot file will be saved to and read from. These can be absolute paths or relative paths. If relative, they will be resolved relative to the test file. For example, if your test file is located at `/path/to/test.ts` and the `dir` option is set to `snapshots`, then the snapshot file would be written to `/path/to/snapshots/test.ts.snap`. - `dir` allows you to specify the snapshot directory, while still using the default format for the snapshot file name. - `path` allows you to specify the directory and file name of the snapshot file. If your test file is located at `/path/to/test.ts` and the `path` option is set to `snapshots/test.snapshot`, then the snapshot file would be written to `/path/to/snapshots/test.snapshot`. :::note If both `dir` and `path` are specified, the `dir` option will be ignored and the `path` option will be handled as normal. ::: ### `mode` The `mode` option controls how `assertSnapshot` behaves regardless of command line flags and has two settings, `assert` or `update`: - `assert`: Always performs comparison only, ignoring any `--update` or `-u` flags. If snapshots don't match, the test will fail with an `AssertionError`. - `update`: Always updates snapshots. Any mismatched snapshots will be updated after tests complete. This option is useful when you need different snapshot behaviors within the same test suite: ```ts // Create a new snapshot or verify an existing one await assertSnapshot(t, stableComponent); // Always update this snapshot regardless of command line flags await assertSnapshot(t, experimentalComponent, { mode: "update", name: "experimental feature", }); // Always verify but never update this snapshot regardless of command line flags await assertSnapshot(t, criticalComponent, { mode: "assert", name: "critical feature", }); ``` ### `name` The name of the snapshot. If unspecified, the name of the test step will be used instead. ```ts title="example_test.ts" import { assertSnapshot } from "jsr:@std/testing/snapshot"; Deno.test("isSnapshotMatch", async (t) => { const a = { hello: "world!", example: 123, }; await assertSnapshot(t, a, { name: "Test Name", }); }); ``` ```ts title="__snapshots__/example_test.ts.snap" export const snapshot = {}; snapshot[`Test Name 1`] = ` { example: 123, hello: "world!", } `; ``` When `assertSnapshot` is run multiple times with the same value for name, then the suffix will be incremented as normal. i.e. `Test Name 1`, `Test Name 2`, `Test Name 3`, etc. ### `msg` Used to set a custom error message. This will overwrite the default error message, which includes the diff for failed snapshots: ```ts Deno.test("custom error message example", async (t) => { const userData = { name: "John Doe", role: "admin", }; await assertSnapshot(t, userData, { msg: "User data structure has changed unexpectedly. Please verify your changes are intentional.", }); }); ``` When the snapshot fails, instead of seeing the default diff message, you'll see your custom error message. ## Testing Different Data Types Snapshot testing works with various data types and structures: ```ts Deno.test("snapshot various types", async (t) => { // Arrays await assertSnapshot(t, [1, 2, 3, "four", { five: true }]); // Complex objects await assertSnapshot(t, { user: { name: "Test", roles: ["admin", "user"] }, settings: new Map([["theme", "dark"], ["language", "en"]]), }); // Error objects await assertSnapshot(t, new Error("Test error message")); }); ``` ## Working with Asynchronous Code When testing asynchronous functions, ensure you await the results before passing them to the snapshot: ```ts Deno.test("async function test", async (t) => { const fetchData = async () => { // Simulate API call return { success: true, data: ["item1", "item2"] }; }; const result = await fetchData(); await assertSnapshot(t, result); }); ``` ## Best Practices ### Keep Snapshots Concise Avoid capturing large data structures that aren't necessary for your test. Focus on capturing only what's relevant. ### Descriptive Test Names Use descriptive test names that clearly indicate what's being tested: ```ts Deno.test( "renders user profile card with all required fields", async (t) => { // ... test code await assertSnapshot(t, component); }, ); ``` ### Review Snapshots During Code Reviews Always review snapshot changes during code reviews to ensure they represent intentional changes and not regressions. ### Snapshot Organization For larger projects, consider organizing snapshots by feature or component: ```ts await assertSnapshot(t, component, { path: `__snapshots__/components/${componentName}.snap`, }); ``` ## Snapshot Testing in CI/CD ### GitHub Actions Example When running snapshot tests in CI environments, you'll typically want to verify existing snapshots rather than updating them: ```yaml title=".github/workflows/test.yml" name: Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: denoland/setup-deno@v2 with: deno-version: v2.x - name: Run tests run: deno test --allow-read ``` For pull requests that intentionally update snapshots, reviewers should verify the changes are expected before merging. ## Practical Examples ### Testing HTML Output HTML output testing with snapshots is particularly useful for web applications where you want to ensure your components render the expected markup. This approach allows you to catch unintended changes in your HTML structure, attributes, or content that might affect the visual appearance or functionality of your UI components. By capturing a snapshot of the HTML output, you can: - Verify that UI components render correctly with different props/data - Detect regressions when refactoring rendering logic - Document the expected output format of components ```ts Deno.test("HTML rendering", async (t) => { const renderComponent = () => { return `

User Profile

Username: testuser

`; }; await assertSnapshot(t, renderComponent()); }); ``` ### Testing API Responses When building applications that interact with APIs, snapshot testing helps ensure that the structure and format of API responses remain consistent. This is particularly valuable for: - Maintaining backward compatibility when updating API integrations - Verifying that your API response parsing logic works correctly - Documenting the expected shape of API responses for team collaboration - Detecting unexpected changes in API responses that could break your application ```ts Deno.test("API response format", async (t) => { const mockApiResponse = { status: 200, data: { users: [ { id: 1, name: "User 1" }, { id: 2, name: "User 2" }, ], pagination: { page: 1, total: 10 }, }, }; await assertSnapshot(t, mockApiResponse); }); ``` 🦕 Snapshot testing is a powerful technique that complements traditional unit tests by allowing you to capture and verify complex outputs without writing detailed assertions. By incorporating snapshot tests into your testing strategy, you can catch unintended changes, document expected behavior, and build more resilient applications. --- # Deno 沙箱快照教程 > 使用只读镜像创建隔离且可复现的环境。 URL: https://docs.deno.com/examples/snapshots_tutorial/ 快照对于创建只读镜像非常有用,这些镜像可以用来实例化多个拥有相同基础环境的沙箱。如果你经常需要创建带有相同依赖或工具集,或者需要较长设置时间的沙箱,这很实用。 让我们来构建一个“秒开机”沙箱:我们将预装 Node.js、TypeScript 和一个 CLI 到可引导卷中,对其快照,然后启动多个继承同一环境的沙箱,而无需再次运行安装程序。 我们的步骤是: 1. 从 `builtin:debian-13` 基础镜像开始。 2. 只安装一次 Node.js 和一些全局工具。 3. 将准备好的卷快照为 `my-toolchain-snapshot`。 4. 从该快照启动新的沙箱,验证工具在沙箱启动时即可使用。 ## 认证并初始化客户端 为了使用 Deno 沙箱 API,我们首先需要设置一个 Deno 沙箱访问令牌。 1. 在你的 Deno Deploy 控制台,导航至 **Sandboxes** 部分。 2. 创建一个新令牌,复制令牌值。 3. 将该令牌设置为本地环境变量 `DENO_DEPLOY_TOKEN`。 然后我们可以用 SDK 初始化客户端: ```tsx import { Client } from "@deno/sandbox"; const client = new Client(); ``` ## 创建一个可引导的工作空间 本教程中,我们将使用 `ord` 区域,并在卷、快照及沙箱中统一使用该区域。 基于 `builtin:debian-13` 镜像创建一个新卷: ```tsx const volume = await client.volumes.create({ region: "ord", slug: "my-toolchain", capacity: "10GiB", from: "builtin:debian-13", }); console.log(`可引导卷已准备好: ${volume.slug}`); ``` 设置 `from` 参数使该卷具备引导功能。沙箱可以将其挂载为根文件系统,并直接写入更改。 ## 定制镜像 我们安装 Node.js、npm 和 TypeScript 到该卷中。 ```tsx await using build = await client.sandboxes.create({ region: "ord", root: volume.slug, labels: { job: "toolchain-build" }, }); await build.sh`sudo apt-get update`; await build.sh`sudo apt-get install -y nodejs npm`; await build.sh`npm install -g typescript`; await build.fs.writeTextFile( "/opt/banner.txt", "该沙箱启动时预装了 Node.js、npm 和 TypeScript。\n", ); ``` 该会话中的所有操作都会持久保存回可引导卷。 ## 快照结果 卷定制好后,我们可以将其快照以便快速复用: ```tsx const snapshot = await client.volumes.snapshot(volume.id, { slug: "my-toolchain-snapshot", }); console.log(`快照已准备好: ${snapshot.slug} (${snapshot.region})`); ``` 通过运行命令 `deno run -A main.ts` 在挂载可引导卷的沙箱内执行安装步骤。 你也可以用 CLI 创建快照,命令如下: ```bash deno sandbox snapshots create my-toolchain my-toolchain-snapshot ``` 快照是只读副本,可以同时支持多个沙箱。因为文件系统已预先配置,启动速度大幅提升。 ## 从快照启动并使用 快照准备就绪后,我们可以启动新的沙箱,将该快照挂载为根文件系统: ```tsx import { Client, Sandbox } from "@deno/sandbox"; const client = new Client(); await using dev = await client.sandboxes.create({ region: "ord", root: snapshot.slug, labels: { job: "dev-shell" }, }); const nodeVersion = await dev.sh`node --version`; const tscVersion = await dev.sh`tsc --version`; const banner = await dev.fs.readTextFile("/opt/banner.txt"); console.log({ nodeVersion: nodeVersion.stdout, tscVersion: tscVersion.stdout }); console.log(banner); ``` 该沙箱内的写操作是短暂的,会话结束后消失;但读取操作直接来自快照的文件系统,因此每个沙箱都能瞬间看到同一个精心配置的环境。 ## 快照的迭代或废弃 需要更新的工具链?你可以从快照派生一个可写卷,进行修改,然后再次快照。 ```tsx const fork = await client.volumes.create({ region: "ord", slug: "my-toolchain-fork", capacity: "10GiB", from: snapshot.slug, }); ``` 当快照过时时,可以将其删除: ```tsx await client.snapshots.delete(snapshot.slug); ``` 🦕 现在你拥有了一个具体的工作流程,用于发布可复现环境:一次构建,快照保存,并将快照别名交给团队成员,令其几秒内启动完全配置好的沙箱。 --- # 使用 Deno 构建 SolidJS 应用 > Build a SolidJS application with Deno. Learn how to set up a project, implement reactive components, handle routing, create API endpoints with Hono, and build a full-stack TypeScript application. URL: https://docs.deno.com/examples/solidjs_tutorial/ [SolidJS](https://www.solidjs.com/) 是一个声明式 JavaScript 库,用于 创建用户界面,强调细粒度的响应性和最小的开销。当与 Deno 的现代运行时环境相结合时,您将获得一个强大且高效的堆栈,用于构建 web 应用程序。在本教程中, 我们将构建一个简单的恐龙目录应用,演示这两种技术的关键特性。 我们将详细介绍如何使用 Deno 构建一个简单的 SolidJS 应用: - [使用 Vite 搭建 SolidJS 应用](#scaffold-a-solidjs-app-with-vite) - [设置 Hono 后端](#set-up-our-hono-backend) - [创建我们的 SolidJS 前端](#create-our-solidjs-frontend) - [后续步骤](#next-steps) 您可以直接跳到 [源代码](https://github.com/denoland/examples/tree/main/with-solidjs) 或按照下面的步骤进行操作! ## 使用 Vite 搭建 SolidJS 应用 让我们使用 [Vite](https://vite.dev/) 设置我们的 SolidJS 应用,这是一个现代的构建工具,提供了出色的开发体验,具备热模块替换和优化构建等特性。 ```bash deno init --npm vite@latest solid-deno --template solid-ts ``` 我们的后端将由 [Hono](https://hono.dev/) 提供支持,我们可以通过 [JSR](https://jsr.io) 安装它。我们还将添加 `solidjs/router` 以实现客户端路由和恐龙目录页面之间的导航。
```bash deno add jsr:@hono/hono npm:@solidjs/router ```
了解更多关于 deno add 和将 Deno 用作包管理器的信息。
我们还需要更新我们的 `deno.json` 文件,以包含一些任务和 `compilerOptions` 来运行我们的应用:
```json { "tasks": { "dev": "deno task dev:api & deno task dev:vite", "dev:api": "deno run --allow-env --allow-net --allow-read api/main.ts", "dev:vite": "deno run -A npm:vite", "build": "deno run -A npm:vite build", "serve": { "command": "deno task dev:api", "description": "运行构建,然后启动 API 服务器", "dependencies": ["deno task build"] } }, "imports": { "@hono/hono": "jsr:@hono/hono@^4.6.12", "@solidjs/router": "npm:@solidjs/router@^0.14.10" }, "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "solid-js", "lib": ["DOM", "DOM.Iterable", "ESNext"] } } ```
您可以将 tasks 作为对象编写。在这里,我们的 serve 命令包含一个 descriptiondependencies
很好!接下来,让我们设置我们的 API 后端。 ## 设置我们的 Hono 后端 在我们的主目录中,我们将建立一个 `api/` 目录并创建两个文件。首先,我们的恐龙数据文件, [`api/data.json`](https://github.com/denoland/examples/blob/main/with-solidjs/api/data.json): ```jsonc // api/data.json [ { "name": "Aardonyx", "description": "Sauropods 进化的早期阶段。" }, { "name": "Abelisaurus", "description": "\"阿贝尔的蜥蜴\" 仅从单个头骨重建。" }, { "name": "Abrictosaurus", "description": "Heterodontosaurus 的一个早期亲属。" }, ... ] ``` 这是我们将提取数据的地方。在完整的应用中,这些数据将来自数据库。 > ⚠️️ 在本教程中,我们是硬编码数据。但您可以连接到 > [多种数据库](https://docs.deno.com/runtime/tutorials/connecting_to_databases/) 和 [甚至使用 Prisma 等 ORM](https://docs.deno.com/runtime/tutorials/how_to_with_npm/prisma/) 与 > Deno。 其次,我们需要我们的 Hono 服务器,`api/main.ts`: ```tsx // api/main.ts import { Hono } from "@hono/hono"; import data from "./data.json" with { type: "json" }; const app = new Hono(); app.get("/", (c) => { return c.text("欢迎来到恐龙 API!"); }); app.get("/api/dinosaurs", (c) => { return c.json(data); }); app.get("/api/dinosaurs/:dinosaur", (c) => { if (!c.req.param("dinosaur")) { return c.text("未提供恐龙名称。"); } const dinosaur = data.find((item) => item.name.toLowerCase() === c.req.param("dinosaur").toLowerCase() ); console.log(dinosaur); if (dinosaur) { return c.json(dinosaur); } else { return c.notFound(); } }); Deno.serve(app.fetch); ``` 这个 Hono 服务器提供了两个 API 端点: - `GET /api/dinosaurs` 获取所有恐龙,并 - `GET /api/dinosaurs/:dinosaur` 根据名称获取特定恐龙 当我们运行 `deno task dev` 时,该服务器将在 `localhost:8000` 上启动。 最后,在我们开始构建前端之前,让我们更新我们的 `vite.config.ts` 文件如下,特别是 `server.proxy`,这告知我们的 SolidJS 前端 API 端点的位置信息。 ```tsx // vite.config.ts import { defineConfig } from "vite"; import solid from "vite-plugin-solid"; export default defineConfig({ plugins: [solid()], server: { proxy: { "/api": { target: "http://localhost:8000", changeOrigin: true, }, }, }, }); ``` ## 创建我们的 SolidJS 前端 在我们开始构建前端组件之前,让我们快速在 `src/types.ts` 中定义 `Dino` 类型: ```tsx // src/types.ts export type Dino = { name: string; description: string; }; ``` `Dino` 类型接口确保了我们整个应用的类型安全,定义了我们的恐龙数据的形状,并启用了 TypeScript 的静态类型检查。 接下来,让我们设置我们的前端以接收数据。我们将要有两个页面: - `Index.tsx` - `Dinosaur.tsx` 下面是 `src/pages/Index.tsx` 页面的代码: ```tsx // src/pages/Index.tsx import { createSignal, For, onMount } from "solid-js"; import { A } from "@solidjs/router"; import type { Dino } from "../types.ts"; export default function Index() { const [dinosaurs, setDinosaurs] = createSignal([]); onMount(async () => { try { const response = await fetch("/api/dinosaurs"); const allDinosaurs = (await response.json()) as Dino[]; setDinosaurs(allDinosaurs); console.log("获取的恐龙:", allDinosaurs); } catch (error) { console.error("获取恐龙失败:", error); } }); return (

欢迎来到恐龙应用

点击下面的恐龙以了解更多信息。

{(dinosaur) => ( {dinosaur.name} )}
); } ``` 使用 SolidJS 时,有一些关键区别于 React 需要注意: 1. 我们使用 SolidJS 特有的原语: - `createSignal` 来代替 `useState` - `createEffect` 来代替 `useEffect` - `For` 组件来代替 `map` - `A` 组件来代替 `Link` 2. SolidJS 组件使用细粒度的反应性,因此我们像调用函数一样调用信号,例如 `dinosaur()` 而不仅仅是 `dinosaur` 3. 路由由 `@solidjs/router` 处理,而不是 `react-router-dom` 4. 组件导入使用 Solid 的 [`lazy`](https://docs.solidjs.com/reference/component-apis/lazy) 进行代码拆分 `Index` 页面使用 SolidJS 的 `createSignal` 来管理恐龙列表,并在组件加载时使用 `onMount` 来获取数据。我们使用 `For` 组件,这是 SolidJS 高效渲染列表的方式,而不是使用 JavaScript 的 map 函数。来自 `@solidjs/router` 的 `A` 组件创建了指向单个恐龙页面的客户端导航链接,避免了完整页面的重新加载。 现在是单个恐龙数据页面代码在 `src/pages/Dinosaur.tsx` 中: ```tsx // src/pages/Dinosaur.tsx import { createSignal, onMount } from "solid-js"; import { A, useParams } from "@solidjs/router"; import type { Dino } from "../types.ts"; export default function Dinosaur() { const params = useParams(); const [dinosaur, setDinosaur] = createSignal({ name: "", description: "", }); onMount(async () => { const resp = await fetch(`/api/dinosaurs/${params.selectedDinosaur}`); const dino = (await resp.json()) as Dino; setDinosaur(dino); console.log("恐龙", dino); }); return (

{dinosaur().name}

{dinosaur().description}

返回所有恐龙
); } ``` `Dinosaur` 页面展示了 SolidJS 处理动态路由的方法,通过使用 `useParams` 来访问 URL 参数。它遵循与 `Index` 页面类似的模式,使用 `createSignal` 进行状态管理和 `onMount` 进行数据获取,但专注于单个恐龙的细节。这个 `Dinosaur` 组件还展示了如何在模板中访问信号值,通过将它们作为函数调用(例如,`dinosaur().name`),这是与 React 状态管理的重要区别。 最后,为了将所有内容串联在一起,我们将更新 `App.tsx` 文件,该文件将作为组件服务于 `Index` 和 `Dinosaur` 页面。`App` 组件使用 `@solidjs/router` 配置我们的路由,定义两个主要路由:一个用于我们的恐龙列表的索引路由,以及一个用于单个恐龙页面的动态路由。路径中的 `:selectedDinosaur` 参数创建了一个动态部分,可以与 URL 中的任何恐龙名称进行匹配。 ```tsx // src/App.tsx import { Route, Router } from "@solidjs/router"; import Index from "./pages/Index.tsx"; import Dinosaur from "./pages/Dinosaur.tsx"; import "./App.css"; const App = () => { return ( ); }; export default App; ``` 最后,这个 `App` 组件将从我们的主索引中调用: ```tsx // src/index.tsx import { render } from "solid-js/web"; import App from "./App.tsx"; import "./index.css"; const wrapper = document.getElementById("root"); if (!wrapper) { throw new Error("未找到包装 DIV"); } render(() => , wrapper); ``` 我们应用的入口点使用 SolidJS 的 `render` 函数将 App 组件挂载到 DOM 中。它包括一个安全检查,以确保根元素在尝试渲染之前存在,从而在初始化过程中提供更好的错误处理。 现在,让我们运行 `deno task dev` 同时启动前端和后端:
## 后续步骤 🦕 现在您可以使用 Deno 构建和运行 SolidJS 应用!以下是一些您可以增强恐龙应用的方法: - 添加持久化数据存储 [使用像 Postgres 或 MongoDB 的数据库](https://docs.deno.com/runtime/tutorials/connecting_to_databases/) 和像 [Drizzle](https://deno.com/blog/build-database-app-drizzle) 或 [Prisma](https://docs.deno.com/runtime/tutorials/how_to_with_npm/prisma/) 的 ORM - 使用 SolidJS 的 [`createContext`](https://docs.solidjs.com/reference/component-apis/create-context) 实现全局状态,在组件之间共享数据 - 使用 [`createResource`](https://docs.solidjs.com/reference/basic-reactivity/create-resource) 的 loading 属性添加加载状态 - 实现基于路由的代码分割,使用 [`lazy`](https://docs.solidjs.com/reference/component-apis/lazy) 导入 - 使用 `Index` 组件提升列表渲染效率 - 将您的应用部署到 [AWS](https://docs.deno.com/runtime/tutorials/aws_lightsail/), [Digital Ocean](https://docs.deno.com/runtime/tutorials/digital_ocean/),或 [Google Cloud Run](https://docs.deno.com/runtime/tutorials/google_cloud_run/) SolidJS 独特的反应式原语、真实的 DOM 重新协调和 Deno 的现代运行时组合,为网络开发提供了一个极其高效的基础。在没有虚拟 DOM 开销和仅在需要的地方进行细粒度更新的情况下,您的应用可以实现最佳性能,同时保持清晰、可读的代码。 --- # Stubbing in tests > Learn how to use stubs in Deno to isolate code during testing by replacing function implementations with controlled behavior URL: https://docs.deno.com/examples/stubbing_tutorial/ Stubbing is a powerful technique for isolating the code you're testing by replacing functions with controlled implementations. While [spies](/examples/mocking_tutorial/#spying) monitor function calls without changing behavior, stubs go a step further by completely replacing the original implementation, allowing you to simulate specific conditions or behaviors during testing. ## What are stubs? Stubs are fake implementations that replace real functions during testing. They let you: - Control what values functions return - Simulate errors or specific edge cases - Prevent external services like databases or APIs from being called - Test code paths that would be difficult to trigger with real implementations Deno provides robust stubbing capabilities through the [Standard Library's testing tools](https://jsr.io/@std/testing/doc/mock#stubbing). ## Basic stub usage Here's a simple example demonstrating how to stub a function: ```ts import { assertEquals } from "jsr:@std/assert"; import { stub } from "jsr:@std/testing/mock"; // Wrap dependencies so they can be stubbed safely from tests. const deps = { getUserName(_id: number): string { // In a real app, this might call a database return "Original User"; }, }; // Function under test function greetUser(id: number): string { const name = deps.getUserName(id); return `Hello, ${name}!`; } Deno.test("greetUser with stubbed getUserName", () => { // Create a stub that returns a controlled value const getUserNameStub = stub(deps, "getUserName", () => "Test User"); try { // Test with the stubbed function const greeting = greetUser(123); assertEquals(greeting, "Hello, Test User!"); } finally { // Always restore the original function getUserNameStub.restore(); } }); ``` In this example, we: 1. Import the necessary functions from Deno's standard library 2. Create a stub for the `getUserName` function that returns "Test User" instead of calling the real implementation 3. Call our function under test, which will use the stubbed implementation 4. Verify the result meets our expectations 5. Restore the original function to prevent affecting other tests ## Using stubs in a testing scenario Let's look at a more practical example with a `UserRepository` class that interacts with a database: ```ts import { assertSpyCalls, returnsNext, stub } from "jsr:@std/testing/mock"; import { assertThrows } from "jsr:@std/assert"; type User = { id: number; name: string; }; // This represents our database access layer const database = { getUserById(id: number): User | undefined { // In a real app, this would query a database return { id, name: "Ada Lovelace" }; }, }; // The class we want to test class UserRepository { static findOrThrow(id: number): User { const user = database.getUserById(id); if (!user) { throw new Error("User not found"); } return user; } } Deno.test("findOrThrow method throws when the user was not found", () => { // Stub the database.getUserById function to return undefined using dbStub = stub(database, "getUserById", returnsNext([undefined])); // We expect this function call to throw an error assertThrows(() => UserRepository.findOrThrow(1), Error, "User not found"); // Verify the stubbed function was called once assertSpyCalls(dbStub, 1); }); ``` In this example: 1. We're testing the `findOrThrow` method, which should throw an error when a user is not found 2. We stub `database.getUserById` to return `undefined`, simulating a missing user 3. We verify that `findOrThrow` throws the expected error 4. We also check that the database method was called exactly once Note that we're using the `using` keyword with `stub`, which is a convenient way to ensure the stub is automatically restored when it goes out of scope. ## Advanced stub techniques ### Returning different values on subsequent calls Sometimes you want a stub to return different values each time it's called: ```ts import { returnsNext, stub } from "jsr:@std/testing/mock"; import { assertEquals } from "jsr:@std/assert"; Deno.test("stub with multiple return values", () => { const dataService = { fetchData: () => "original data", }; const fetchDataStub = stub( dataService, "fetchData", // Return these values in sequence returnsNext(["first result", "second result", "third result"]), ); try { assertEquals(dataService.fetchData(), "first result"); assertEquals(dataService.fetchData(), "second result"); assertEquals(dataService.fetchData(), "third result"); } finally { fetchDataStub.restore(); } }); ``` ### Stubbing with implementation logic You can also provide custom logic in your stub implementations: ```ts import { stub } from "jsr:@std/testing/mock"; import { assertEquals } from "jsr:@std/assert"; Deno.test("stub with custom implementation", () => { // Create a counter to track how many times the stub is called let callCount = 0; const mathService = { calculate: (a: number, b: number) => a + b, }; const calculateStub = stub( mathService, "calculate", (a: number, b: number) => { callCount++; return a + b * 2; // Custom implementation }, ); try { const result = mathService.calculate(5, 10); assertEquals(result, 25); // 5 + (10 * 2) assertEquals(callCount, 1); } finally { calculateStub.restore(); } }); ``` ## Stubbing API calls and external services One of the most common uses of stubs is to replace API calls during testing: ```ts import { assertEquals } from "jsr:@std/assert"; import { stub } from "jsr:@std/testing/mock"; const apiClient = { fetch: globalThis.fetch, }; async function fetchUserData(id: string) { const response = await apiClient.fetch(`https://api.example.com/users/${id}`); if (!response.ok) { throw new Error(`Failed to fetch user: ${response.status}`); } return await response.json(); } Deno.test("fetchUserData with stubbed fetch", async () => { const mockResponse = new Response( JSON.stringify({ id: "123", name: "Jane Doe" }), { status: 200, headers: { "Content-Type": "application/json" } }, ); // Replace apiClient.fetch with a stubbed version const fetchStub = stub( apiClient, "fetch", () => Promise.resolve(mockResponse), ); try { const user = await fetchUserData("123"); assertEquals(user, { id: "123", name: "Jane Doe" }); } finally { fetchStub.restore(); } }); ``` ## Best practices 1. **Always restore stubs**: Use `try/finally` blocks or the `using` keyword to ensure stubs are restored, even if tests fail. 2. **Use stubs for external dependencies**: Stub out database calls, API requests, or file system operations to make tests faster and more reliable. 3. **Keep stubs simple**: Stubs should return predictable values that let you test specific scenarios. 4. **Combine with spies when needed**: Sometimes you need to both replace functionality (stub) and track calls (spy). 5. **Stub at the right level**: Stub at the interface boundary rather than deep within implementation details. 🦕 Stubs are a powerful tool for isolating your code during testing, allowing you to create deterministic test environments and easily test edge cases. By replacing real implementations with controlled behavior, you can write more focused, reliable tests that run quickly and consistently. For more testing resources, check out: - [Testing in isolation with mocks](/examples/mocking_tutorial/) - [Deno Standard Library Testing Modules](https://jsr.io/@std/testing) - [Basic Testing in Deno](/examples/testing_tutorial/) --- # 创建子进程 > 在 Deno 中使用子进程的指南。学习如何启动进程、处理输入/输出流、管理进程生命周期,以及安全实现进程间通信模式。 URL: https://docs.deno.com/examples/subprocess_tutorial/ ## 概念 - Deno 可以通过 [Deno.Command](https://docs.deno.com/api/deno/~/Deno.Command) 启动子进程。 - 启动子进程需要 `--allow-run` 权限。 - 启动的子进程不在安全沙箱中运行。 - 通过 [stdin](https://docs.deno.com/api/deno/~/Deno.stdin)、[stdout](https://docs.deno.com/api/deno/~/Deno.stdout) 和 [stderr](https://docs.deno.com/api/deno/~/Deno.stderr) 流与子进程进行通信。 ## 简单示例 该示例相当于从命令行运行 `echo "Hello from Deno!"`。 ```ts title="subprocess_simple.ts" // 定义用于创建子进程的命令 const command = new Deno.Command("echo", { args: [ "Hello from Deno!", ], }); // 创建子进程并收集输出 const { code, stdout, stderr } = await command.output(); console.assert(code === 0); console.log(new TextDecoder().decode(stdout)); console.log(new TextDecoder().decode(stderr)); ``` 运行它: ```shell $ deno run --allow-run=echo ./subprocess_simple.ts Hello from Deno! ``` ## 安全性 创建子进程需要 `--allow-run` 权限。请注意,子进程不在 Deno 沙箱中运行,因此具有与您自己从命令行运行命令时相同的权限。 ## 与子进程通信 默认情况下,当您使用 `Deno.Command()` 时,子进程继承父进程的 `stdin`、`stdout` 和 `stderr`。如果您想与启动的子进程进行通信,您必须使用 `"piped"` 选项。 ## 管道输出到文件 该示例相当于在 bash 中运行 `yes &> ./process_output`。 ```ts title="subprocess_piping_to_files.ts" import { mergeReadableStreams, } from "jsr:@std/streams@1.0.0-rc.4/merge-readable-streams"; // 创建要附加到进程的文件 const file = await Deno.open("./process_output.txt", { read: true, write: true, create: true, }); // 启动进程 const command = new Deno.Command("yes", { stdout: "piped", stderr: "piped", }); const process = command.spawn(); // 示例:将 stdout 和 stderr 合并并发送到文件 const joined = mergeReadableStreams( process.stdout, process.stderr, ); // 返回一个 Promise,直到进程被终止/关闭时解析 joined.pipeTo(file.writable).then(() => console.log("管道合并完成")); // 手动停止进程,“yes” 将永远不会自行结束 setTimeout(() => { process.kill(); }, 100); ``` 运行它: ```shell $ deno run --allow-run=yes --allow-read=. --allow-write=. ./subprocess_piping_to_file.ts ``` ## 使用便捷方法读取子进程输出 在使用启动的子进程时,您可以对 `stdout` 和 `stderr` 流使用便捷方法,轻松收集和解析输出。这些方法类似于 `Response` 对象上可用的方法: ```ts title="subprocess_convenience_methods.ts" const command = new Deno.Command("deno", { args: [ "eval", "console.log(JSON.stringify({message: 'Hello from subprocess'}))", ], stdout: "piped", stderr: "piped", }); const process = command.spawn(); // 使用便捷方法收集输出 const stdoutText = await process.stdout.text(); const stderrText = await process.stderr.text(); console.log("stdout:", stdoutText); console.log("stderr:", stderrText); // 等待进程完成 const status = await process.status; console.log("退出码:", status.code); ``` 可用的便捷方法包括: - `.text()` - 返回 UTF-8 编码的字符串输出 - `.bytes()` - 返回 `Uint8Array` 类型的输出 - `.arrayBuffer()` - 返回 `ArrayBuffer` 类型的输出 - `.json()` - 解析输出为 JSON 并返回该对象 ```ts title="subprocess_json_parsing.ts" const command = new Deno.Command("deno", { args: ["eval", "console.log(JSON.stringify({name: 'Deno', version: '2.0'}))"], stdout: "piped", }); const process = command.spawn(); // 直接解析 JSON 输出 const jsonOutput = await process.stdout.json(); console.log("解析后的 JSON:", jsonOutput); // { name: "Deno", version: "2.0" } await process.status; ``` --- # 构建一个 SvelteKit 应用 > 一个使用 Deno 构建 SvelteKit 应用的教程。学习如何设置 SvelteKit 项目,实现基于文件的路由,使用 load 函数管理状态,以及创建全栈 TypeScript 应用。 URL: https://docs.deno.com/examples/svelte_tutorial/ [SvelteKit](https://kit.svelte.dev/) 是一个构建在 [Svelte](https://svelte.dev/) 之上的网页框架,Svelte 是一个现代前端编译器,可以生成高度优化的原生 JavaScript。SvelteKit 提供了基于文件的路由、服务器端渲染以及全栈功能等特性。 在本教程中,我们将使用 Deno 构建一个简单的 SvelteKit 应用。该应用会显示恐龙列表,当你点击某个恐龙时,会跳转到显示更多详情的恐龙页面。你可以在 [GitHub 查看完整应用](https://github.com/denoland/tutorial-with-svelte)。 你也可以在 [Deno Deploy 上访问实时演示](https://tutorial-with-svelte.deno.deno.net/)。 :::info 部署你自己的应用 想跳过教程,立即部署完整的应用吗?点击下方按钮,即可瞬间将完整的 SvelteKit 恐龙应用部署到 Deno Deploy。你将获得一个可运行的在线应用,可以在学习过程中自定义和修改! [![Deploy on Deno](https://deno.com/button)](https://console.deno.com/new?clone=https://github.com/denoland/tutorial-with-svelte) ::: ## 使用 Deno 创建 SvelteKit 应用 我们将使用 [SvelteKit](https://kit.svelte.dev/) 创建一个新的 SvelteKit 应用。在终端中运行以下命令来创建新应用: ```shell deno run -A npm:sv create my-app ``` 按提示输入你的应用名称,选择“Skeleton project”模板。当询问是否使用 TypeScript 时,选择“是,使用 TypeScript 语法”。 创建完成后,进入新项目目录并运行以下命令安装依赖: ```shell deno install ``` 然后运行下面的命令启动你的 SvelteKit 应用: ```shell deno task dev ``` Deno 会执行 `package.json` 中的 `dev` 任务,启动 Vite 开发服务器。点击输出的 localhost 链接,在浏览器中打开你的应用。 ## 配置格式化工具 `deno fmt` 支持带有 [`--unstable-component`](https://docs.deno.com/runtime/reference/cli/fmt/#formatting-options-unstable-component) 参数的 Svelte 文件。使用命令: ```sh deno fmt --unstable-component ``` 若想让 `deno fmt` 始终格式化 Svelte 文件,在你的 `deno.json` 文件顶层添加: ```json "unstable": ["fmt-component"] ``` ## 添加后端 API 我们将使用 SvelteKit 内置的 API 功能构建 API 路由。SvelteKit 允许你通过在路由目录中创建 `+server.js` 或 `+server.ts` 文件来定义 API 端点。 在 `src/routes` 目录中创建 `api` 文件夹,在该文件夹内创建 `data.json`,用来存放硬编码的恐龙数据。 将该 [json 文件](https://github.com/denoland/tutorial-with-svelte/blob/main/src/routes/api/data.json) 复制并粘贴到 `src/routes/api/data.json` 文件中。(如果是实际项目,通常会从数据库或外部 API 获取数据。) 接下来我们将构建返回恐龙信息的 API 路由。SvelteKit 提供简单的接口来创建 API 端点,使用服务器文件。 创建 `src/routes/api/dinosaurs/+server.ts` 处理 `/api/dinosaurs` 端点,返回全部恐龙数据: ```js title="src/routes/api/dinosaurs/+server.ts" import { json } from "@sveltejs/kit"; import data from "../data.json" with { type: "json" }; export function GET() { return json(data); } ``` 然后创建 `src/routes/api/dinosaurs/[id]/+server.ts` 处理 `/api/dinosaurs/:id`,返回单个恐龙数据: ```ts title="src/routes/api/dinosaurs/[id]/+server.ts" import { json } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; import data from "../../data.json" with { type: "json" }; export const GET: RequestHandler = ({ params }) => { const dinosaur = data.find((item) => { return item.name.toLowerCase() === params.id.toLowerCase(); }); if (dinosaur) { return json(dinosaur); } return json({ error: "Not found" }, { status: 404 }); }; ``` SvelteKit 会根据文件结构自动处理路由。`+server.ts` 文件定义 API 端点,而 `[id]` 文件夹创建了动态路由参数。 ## 构建前端 ### 基于文件的路由和数据加载 SvelteKit 使用基于文件的路由,`src/routes` 目录结构决定应用的路由。与 Vue Router 不同,你无需手动配置路由 —— SvelteKit 会自动基于文件创建路由。 在 SvelteKit 中,`+page.svelte` 文件定义页面组件,`+page.ts` 文件定义加载数据的函数,会在页面加载前执行。这样内置了服务器端渲染和数据获取能力。 ### 页面与组件 SvelteKit 将前端代码组织成页面和组件。页面由路由目录下的 `+page.svelte` 文件定义,组件是可复用的代码段,可以放在项目中的任意位置。 每个 Svelte 组件文件包含三个可选部分:`

🦕 恐龙应用

点击下方恐龙了解更多信息。

{#each dinosaurs as dinosaur (dinosaur.name)} {dinosaur.name} {/each}
``` 该代码使用 Svelte 的 [each 块](https://svelte.dev/docs/logic-blocks#each) 遍历 `dinosaurs` 数组,将每只恐龙渲染为一个链接。`{#each}` 是 Svelte 渲染列表的语法,`(dinosaur.name)` 提供每项的唯一 key。 #### 恐龙详情页 详情页展示单个恐龙信息。SvelteKit 通过方括号命名的文件夹创建动态路由,`[dinosaur]` 文件夹捕获 URL 中的恐龙名称。 先创建 `src/routes/[dinosaur]/+page.ts` 加载单个恐龙数据: ```ts title="src/routes/[dinosaur]/+page.ts" import type { PageLoad } from "./$types"; import { error } from "@sveltejs/kit"; export const load: PageLoad = async ({ fetch, params }) => { const res = await fetch(`/api/dinosaurs/${params.dinosaur}`); const dinosaur = await res.json() as { name: string; description: string }; if (res.status === 404) { return error(404, "未找到恐龙"); } return { dinosaur }; }; ``` 该加载函数通过 `params` 对象访问 URL 参数 `dinosaur`。如果 API 返回 404,使用 SvelteKit 的 `error` 函数抛出 404 错误。 然后创建 `src/routes/[dinosaur]/+page.svelte` 显示恐龙详情: ```html title="src/routes/[dinosaur]/+page.svelte"

{dinosaur.name}

{dinosaur.description}

🠠 返回所有恐龙
``` 该页面显示恐龙名称和描述,并带有返回首页的链接。数据来源于加载函数,自动可用。 ## 运行应用 既然已经配置好前端和后端 API 路由,我们可以运行应用。终端执行: ```shell deno task dev ``` 这会启动带有 Vite 的 SvelteKit 开发服务器。SvelteKit 会同时处理前端页面和我们创建的 API 路由,无需运行多个服务器线程。 在浏览器中访问 `http://localhost:5173` 来查看应用。点击恐龙查看详情! 你也可以访问 [Deno Deploy 在线版本](https://tutorial-with-svelte.deno.deno.net/)。 ## 构建与部署 SvelteKit 内置构建能力。我们配置使用 Deno 适配器,优化构建以便部署到支持 Deno 的平台。执行以下命令构建生产版本: ```sh deno task build ``` 该命令会: 1. 使用 Vite 构建 SvelteKit 应用 2. 生成优化后的生产资源 3. 创建兼容 Deno 的服务器端代码 构建后的应用可部署到支持 Deno 的平台,如 Deno Deploy。 你可以将应用部署到你喜欢的云服务。我们推荐使用 [Deno Deploy](https://deno.com/deploy),简单又方便。你可以直接从 GitHub 部署,只需创建一个 GitHub 仓库并推送代码,然后连接到 Deno Deploy。 ### 创建 GitHub 仓库 [新建一个 GitHub 仓库](https://github.com/new),然后初始化并推送你的应用: ```sh git init -b main git remote add origin https://github.com//.git git add . git commit -am 'my svelte app' git push -u origin main ``` ### 部署到 Deno Deploy 当你的应用托管在 GitHub 后,你可以访问 [Deno DeployEA](https://console.deno.com/) 仪表盘进行 部署。 想了解完整部署流程,请查看 [Deno Deploy 教程](/examples/deno_deploy_tutorial/)。 🦕 现在你已经掌握如何使用 Deno 适配器运行 SvelteKit 应用,准备好构建真实世界的应用了! --- # 使用 sv 和 Deno 构建 SvelteKit 应用 URL: https://docs.deno.com/examples/sveltekit_tutorial/ 自上线以来,SvelteKit 一直深受欢迎,而随着 Svelte 版本 5 最近发布,截至撰写本文时,没有比现在更合适的时机来展示如何用 Deno 运行它! 通过本教程,我们将演示如何使用 sv CLI 工具轻松搭建一个 SvelteKit 项目,并讲解数据加载的模式。 你可以在[GitHub](https://github.com/s-petey/deno-sveltekit)上查看完成的应用。 ## 入门 我们可以通过 `npx sv create` 快速生成一个项目。这是[SvelteKit 的 CLI](https://github.com/sveltejs/cli),功能强大且实用。 如果你已经观看了上面的视频,那就太好了!如果没有,下面是选择设置: - 模板 - SvelteKit minimal - 类型检查 - Typescript - 项目附加 - tailwindcss - Tailwind 插件 - typography - forms - 包管理器 - Deno 接下来,我们需要保持后台运行 `deno task dev`,这样可以在本地实时查看改动和应用运行状态。 ### Deno 配置 `sv` 命令会生成一个 `package.json` 文件,我们需要将其转换为 `deno.json`。具体来说,将`scripts`改为`tasks`,并且把基于 `vite` 的命令加上 `npm:` 前缀。 示例: ```json "dev": "vite dev", ``` 转换为: ```json "dev": "deno run -A npm:vite dev", ``` 此时我们还希望集成 Deno 的格式化和代码检查工具,因此也加上这些任务。 ```json { "tasks": { "dev": "deno run -A npm:vite dev", "format": "deno fmt", "lint": "deno lint", "lint:fix": "deno lint --fix" } } ``` 配置好这些任务后,我们还需要针对 `format` 和 `lint` 命令设置规则。这里使用了 `unstable` 标记启用 `fmt-component`,它支持[svelte 组件文件](https://docs.deno.com/runtime/reference/cli/fmt/#supported-file-types)格式化。同时根据推荐设置增加了部分 lint 规则。 ```json { "fmt": {}, "lint": { "include": ["src/**/*.{ts,js,svelte}"], "exclude": ["node_modules", ".svelte-kit", ".vite", "dist", "build"], "rules": { "tags": ["recommended"] } }, "unstable": ["fmt-component"] } ``` 我们还需要在 `deno.json` 文件中设置 `nodeModulesDir`,以便命令能够正确找到 `node_modules` 目录。 ```json { "nodeModulesDir": "auto" } ``` 如果你使用 VSCode 或其他支持 `settings.json` 的编辑器,下面是一些推荐配置,实现保存自动格式化和代码检查。 ```json { "deno.enable": true, "deno.enablePaths": [ "./deno.json" ], "editor.defaultFormatter": "denoland.vscode-deno" } ``` ### 目录结构讲解 需要注意几个不同的目录: - `src`:应用代码根目录,大部分时间和精力都会在这里。 - `src/lib`:SvelteKit 的别名目录,用于快速导入,存放辅助函数或库代码。 - `src/routes`:存放应用渲染页面的目录,SvelteKit 采用文件夹路由机制。 #### 重要信息 SvelteKit 应用中我们将遵循一些约定(这里只介绍本教程涉及的): - 文件或文件夹名称中包含 `server` 的,**只允许在服务器端运行**,在客户端运行会报错。 - 在 `src/routes` 中,文件的命名有规范: - `+page.svelte` —— 浏览器端渲染的文件 - `+page.server.ts` —— 服务端运行的文件,允许向相邻的 `+page.svelte` 发送和接收类型安全的数据 - `+layout.svelte` —— 定义布局文件,能为同一目录或子目录的所有 `+page.svelte` 提供出口 - `+error.svelte` —— 自定义错误页,可以美化错误展示界面 稍后你会看到我们将恐龙数据文件 `dinosaurs.ts` 放在 `lib/server` 中,这表示该文件**只应被其他服务端文件访问**。 ### 设置“数据库” 出于简化考虑,我们会使用 TypeScript 文件中保存一个 `Map` 来访问和查找数据。新建文件及路径: ``` src/lib/server/dinosaurs.ts ``` 文件内容,定义 Dinosaur 类型,并存储恐龙数据转成 Map 导出。 ```ts export type Dinosaur = { name: string; description: string }; const dinosaurs = new Map(); const allDinosaurs: Dino[] = [ // 在这里粘贴你的所有恐龙信息 ]; for (const dino of allDinosaurs) { dinosaurs.set(dino.name.toLowerCase(), dino); } export { dinosaurs }; ``` 通过以上设置,我们完成了“数据库”的搭建!接下来学习如何在页面调用它。 ### 加载用于渲染的数据 现在我们需要创建一个位于 routes 根目录的新文件 `+page.server.ts`,此目录下应已有对应的 `+page.svelte`。 ``` src/routes/+page.server.ts ``` 新建文件后,初始化加载函数以载入恐龙数据! ```ts /// src/routes/+page.server.ts import { dinosaurs } from '$lib/server/dinosaurs.js'; export const load = async ({ url }) => { return { dinosaurs: Array.from(dinosaurs) }; }; ``` 这里做的事情是将 Map 转为数组,以便 `+page.svelte` 里渲染。你可在页面内移除不需要内容或简单添加以下内容: ```html
{#each data.dinosaurs as item} {item.name} {/each}
``` 注意在操作 `data` 时我们拥有类型安全,能确认 `data.dinosaurs` 存在且类型正确! ### 添加单独的恐龙详情页 既然我们渲染了恐龙列表并为每个添加了链接,可以添加相应路由来渲染详情。 ``` src/routes/[name]/+page.server.ts src/routes/[name]/+page.svelte ``` 这里有个特别点在于使用了带方括号的 `[name]` 文件夹名,代表路由参数命名。我们可以任意命名,但因为希望路由能访问形如 `localhost:5173/Ingenia` 的地址,故用参数 `name`。 理解后可以编写 server loader 获取恐龙数据并传递给页面: ```ts /// src/routes/[name]/+page.server.ts import { dinosaurs } from "$lib/server/dinosaurs.js"; import { error } from "@sveltejs/kit"; export const load = async ({ params: { name } }) => { const dino = dinosaurs.get(name.toLowerCase()); if (!dino) { throw error(404, { message: "Dino not found" }); } return { name: dino.name, description: dino.description }; }; ``` 这里我们抛出错误,提醒找不到恐龙。当前还未设置自定义错误页,因此访问不存在路径时会出现默认错误页。现在我们创建一个根目录级的错误页处理。 ``` src/routes/+error.svelte ``` 页面简单,可自行添加美化: ```html

{page.status}: {page.error?.message}

``` 显示错误状态码及错误信息即可。 解决了错误页干扰后,我们继续显示具体恐龙详情! ```html

{data.name}

{data.description}

``` 可以看到依然保持类型安全,确认 `name` 和 `description` 存在,并正常渲染。 但有个问题是:用户访问该详情页,无论是从首页链接点击或手动输入网址,无法方便返回首页。 ### 布局 希望所有页面共享一些通用信息或链接,可以利用 `+layout.svelte` 文件。我们来更新 routes 根目录下的布局。 目标是实现: 1. 允许用户导航到主页 2. 展示 Deno 和 SvelteKit 的优质文档链接 3. 页面显示萌萌的恐龙图片! ```html

Deno Sveltekit

Vite with Deno
{@render children()}
``` 这是我们第一次看到 `{@render children()}`,它相当于 React 里的“插槽”,渲染对应子页面内容。 回到应用,发现标题已带有返回首页的链接,十分方便。 ### 进阶路由、搜索参数及样式 我们不想一次性渲染所有恐龙,那样滚动太长。希望用户能搜索并分页浏览恐龙,同时体现 Svelte 5 靓功能——代码片段(snippets)! 打开首页和对应的服务端代码,做一些修改。 之前简单返回了恐龙数组,现在加入搜索和分页逻辑。 ```ts import { dinosaurs } from "$lib/server/dinosaurs.js"; export const load = async ({ url }) => { // 通过 SvelteKit 注入的 url 获取搜索参数 const queryParams = url.searchParams; // 使用 q 作为搜索关键字 const q = queryParams.get("q"); // 使用 page 确定当前页码 const pageParam = queryParams.get("page"); let page = 1; // 校验 page 参数,非数字则使用默认1 if (pageParam) { const parsedPage = parseInt(pageParam); if (!isNaN(parsedPage)) { page = parsedPage; } } // 使用 limit 控制每页数量 const limitParam = queryParams.get("limit"); let limit = 25; // 校验 limit 参数,非数字则默认25 if (limitParam) { const parsedLimit = parseInt(limitParam); if (!isNaN(parsedLimit)) { limit = parsedLimit; } } // 搜索处理:无 q 时展示全部,有 q 时进行名称匹配 const filteredDinosaurs = Array.from(dinosaurs.values()).filter((d) => { if (!q) { return true; } return d.name.toLowerCase().includes(q.toLowerCase()); }); // 计算分页数据 const offset = Math.abs((page - 1) * limit); const paginatedDinosaurs = Array.from(filteredDinosaurs).slice( offset, offset + limit, ); const totalDinosaurs = filteredDinosaurs.length; const totalPages = Math.ceil(totalDinosaurs / limit); // 最后返回更多分页信息以便前端展示 return { dinosaurs: paginatedDinosaurs, q, page, limit, totalPages, totalDinosaurs, }; }; ``` 呼,工作量不小,完成后添加分页和搜索输入控件到页面中。 ```html
{#each data.dinosaurs as item} {item.name} {/each} {#if data.dinosaurs.length === 0}

未找到恐龙

{/if}
{#if data.totalPages > 0}
{@render pageButton(data.page - 1, data.page === 1, false, '←')} {#each { length: data.totalPages }, page} {#if page >= data.page - 2 && page <= data.page + 2} {@render pageButton(page + 1, data.page === page + 1, data.page === page + 1, page + 1)} {:else if (page === 0 || page === 1) && page !== data.page - 1} {@render pageButton(page + 1, data.page === page + 1, data.page === page + 1, page + 1)} {:else if page >= data.totalPages - 2 && page !== data.page - 1} {@render pageButton(page + 1, data.page === page + 1, data.page === page + 1, page + 1)} {/if} {/each} {@render pageButton(data.page + 1, data.page === data.totalPages, false, '→')}
{/if} {#snippet pageButton(page: number, disabled: boolean, active: boolean, child: string | number)} {/snippet} ``` 注意搜索框使用了 `defaultValue={data.q ?? ''}`,防止页面渲染时显示 `undefined` 或 `null`。 利用 Snippets,可以定义可复用的 Svelte 代码片段,`{#snippet pageButton(...)}` 定义内容,后续用 `{@render pageButton(...)}` 调用并传递类型安全参数。分页按钮运用了该特性。 还有个 Svelte 巧妙点是页面中的 `