跳到主要内容
Deno 2.4 发布,带来 deno bundle、bytes/text 导入、稳定版 OTel 等功能
了解更多

Deno Deploy 上的静态文件

Deno Deploy 在边缘动态生成内容方面一直表现出色。我们可以在靠近用户的地方运行 JavaScript 代码,这可以显著减少响应时间延迟。然而,许多应用程序并非完全动态:它们拥有像 CSS 文件、客户端 JS 和图像这样的静态资源。

到目前为止,Deno Deploy 还没有一种很好的方式来处理静态资源。你不得不选择将它们编码到 JavaScript 代码中、手动搭建 CDN,或者从 GitHub 仓库拉取文件。这些选项都不理想。

一个新的原语

Deno Deploy 现在对静态文件提供了一流的支持。你的静态文件在部署代码时存储在我们的网络上,然后分发到世界各地靠近用户的位置。你可以使用 Deno 文件系统 API 和 fetch 从在边缘运行的 JavaScript 代码中访问这些文件。

由于文件的实际服务仍然由在边缘运行的代码控制,你对所有响应(甚至静态文件的响应)都拥有完全控制权。例如,这可以用于:

  • 仅向已登录用户提供文件
  • 为文件添加 CORS 头
  • 在文件提供之前,在边缘使用一些动态内容修改文件
  • 根据用户浏览器提供不同的文件

在 Deno Deploy 中,静态文件并非一个完全独立存在的系统。

你能做的最基本的事情,就是将整个文件读入内存并提供给用户。

import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

const HTML = await Deno.readFile("./index.html");

serve(async () => {
  return new Response(HTML, {
    headers: {
      "content-type": "text/html",
    },
  });
});

这对于小文件非常有效。对于大文件,你可以直接将文件流式传输给用户,而不是在内存中缓冲它。

import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

const FILE_URL = new URL("/movie.webm", import.meta.url).href;

serve(async () => {
  const resp = await fetch(FILE_URL);
  return new Response(resp.body, {
    headers: {
      "content-type": "video/webm",
    },
  });
});

想要列出所有可用文件?使用 Deno.readDir 就能轻松实现。

import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

serve(async () => {
  const entries = [];
  for await (const entry of Deno.readDir(".")) {
    entries.push(entry);
  }

  const list = entries.map((entry) => {
    return `<li>${entry.name}</li>`;
  }).join("");

  return new Response(`<ul>${list}</ul>`, {
    headers: {
      "content-type": "text/html",
    },
  });
});

标准库的文件服务工具可以用来提供静态文件。这些工具将设置适当的 Content-Type 头,并开箱即用地支持更复杂的功能,例如 Range 请求

import { serve } from "https://deno.land/std@0.140.0/http/server.ts";
import { serveFile } from "https://deno.land/std@0.140.0/http/file_server.ts";

serve(async (req) => {
  return await serveFile(req, `${Deno.cwd()}/static/index.html`);
});

如果你使用像 oak 这样的成熟 HTTP 框架来提供静态内容,也可以使用 Deno Deploy。

import { Application } from "https://deno.land/x/oak/mod.ts";

const app = new Application();
app.use(async (ctx) => {
  try {
    await ctx.send({
      root: `${Deno.cwd()}/static`,
      index: "index.html",
    });
  } catch {
    ctx.response.status = 404;
    ctx.response.body = "404 File not found";
  }
});

await app.listen({ port: 8000 });

Deno Deploy 目前支持的文件系统 API 的完整列表:

  • Deno.readFile 将文件读入内存
  • Deno.readTextFile 将文件作为 UTF-8 字符串读入内存
  • Deno.readDir 获取文件夹中文件和文件夹的列表
  • Deno.open 以分块方式(用于流式传输)打开文件以供读取
  • Deno.stat 获取文件或文件夹的信息(获取大小或类型)
  • Deno.lstat 与上面相同,但不遵循符号链接
  • Deno.realPath 在解析符号链接后获取文件或文件夹的路径
  • Deno.readLink 获取符号链接的目标

Github 集成

现在你可能会问自己:如何将这些新潮的静态文件添加到我的部署中?

默认情况下,如果你已将 GitHub 仓库链接到 Deploy,你仓库中的所有文件都将作为静态文件可用。无需更改。如果你将静态文件用于存储在仓库中的少量资源,例如图像或博客的 Markdown 文件,这会非常方便。

然而,有时你希望在部署时生成静态文件。例如,当使用像 Remix.run 这样的框架,或者使用静态站点生成器时。对于这种情况,我们现在允许你使用 deployctl 工具部署你的代码和静态资源。在边缘服务当前工作目录就像这样简单:

deployctl deploy --project my-project --prod https://deno.land/std@0.140.0/http/file_server.ts

当你只想部署一次,或者你的代码没有版本控制且总是从本地机器部署时,这非常有效,但这不适用于大多数项目。

托管在 Github 上的项目会希望使用 Github Actions 来运行构建步骤以生成 HTML 或其他静态内容,然后上传到 Deno Deploy。我们为此提供了专门的 Github Action 步骤:

- name: Upload to Deno Deploy
  uses: denoland/deployctl@v1
  with:
    project: my-project
    entrypoint: main.js
    root: dist

甚至不需要配置任何访问令牌或密钥即可使其工作。只需在 Deno Deploy 仪表板中链接你的 GitHub 仓库,并将项目设置为“GitHub Actions”部署模式。身份验证由 GitHub Actions 透明处理。

为什么选择 GitHub Actions 而不是自定义 CI 系统?GitHub Actions 现在是持续集成的实际标准,许多开发人员已经熟悉它。为什么要重新发明已经很棒的东西呢?

示例:一个静态生成的站点

作为这篇博文的结尾,这里有一个真实的例子,展示了一个在 deploy 上运行的服务器,它提供了一个在 GitHub Actions 中构建的静态站点。该站点由一个静态站点生成器(在本例中是出色的 https://lumeland.github.io)构建。除了静态文件,该站点还包含一个 /api/time 端点,该端点动态返回当前时间。

https://lume-example.deno.dev/ 试用该示例。

这是该项目使用的 GitHub Actions 工作流文件:

name: ci
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Clone repository
        uses: actions/checkout@v2

      - name: Install Deno
        uses: denoland/setup-deno@main
        with:
          deno-version: 1.18.2

      - name: Build site
        run: deno run -A https://deno.land/x/lume/ci.ts

      - name: Upload to Deno Deploy
        uses: denoland/deployctl@v1
        with:
          project: lume-example
          entrypoint: server/main.ts

以及提供站点和 API 端点的实际代码:

import { Application, Router } from "https://deno.land/x/oak@v10.2.0/mod.ts";

const app = new Application();

// First we try to serve static files from the _site folder. If that fails, we
// fall through to the router below.
app.use(async (ctx, next) => {
  try {
    await ctx.send({
      root: `${Deno.cwd()}/_site`,
      index: "index.html",
    });
  } catch {
    next();
  }
});

const router = new Router();

// The /api/time endpoint returns the current time in ISO format.
router.get("/api/time", (ctx) => {
  ctx.response.body = { time: new Date().toISOString() };
});

// After creating the router, we can add it to the app.
app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8000 });

部署有多快?从 git push 到全球范围内的更改生效,这个仓库大约需要 25 秒。其中 15 秒是等待 GitHub Actions 运行器准备就绪。对于不涉及 GitHub Actions 的“自动”模式部署,我们平均部署时间在 1-10 秒之间。

如果您有任何问题、意见或建议,请在反馈仓库中提出问题,或发送电子邮件给我们。愉快的 Denoing!


您可以在我们的文档中阅读更多关于文件系统 API 的信息:https://deno.org.cn/deploy/docs/runtime-fs。有关 GitHub 集成的更多信息可以在这里找到:https://deno.org.cn/deploy/docs/projects#git-integration