Deno Deploy现已支持静态文件

3,839

在用户侧生成动态内容方面,Deno Deploy 一直表现很出色,通过运行JavaScript代码大大减少响应时间的延迟。但很多应用程序都并不是完全动态的,它们会包含静态资产,例如CSS文件、客户端JS、图像等等。

到目前为止,Deno Deploy还没有一个很好的方法来处理静态资产。一般的处理方式是将静态文件编码到JavaScript代码中,手工滚动CDN,或者从GitHub仓库中提取文件,当然这些选择都不是很理想。

现在,Deno Deploy可以很好地支持静态文件。开发者在部署代码时,静态文件被存储在网络上,然后被分发到靠近用户的地方。使用Deno文件系统的API,以及fetch ,就可以在边缘运行的JavaScript代码中访问这些文件。

企业微信20220214-114028@2x.png

因为文件的实际服务仍然由运行在边缘的代码控制,开发者可以完全控制所有的响应,甚至是对静态文件的响应。例如,

  • 只向已登录的用户提供文件

  • 为文件添加 CORS 头信息

  • 在提供文件之前,在边缘修改带有一些动态内容的文件

  • 根据用户的浏览器提供不同的文件。

在Deno Deploy中,静态文件不是一个完全独立的系统。开发者可以做的最基本的事情是将整个文件读入内存并提供给用户。

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

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

console.log("Listening on http://localhost:8000");
serve(async () => {
  return new Response(HTML, {
    headers: {
      "content-type": "text/html",
    },
  });
});

这对小文件很有效。对于较大的文件,可以将文件直接流给用户,而不是在内存中缓冲。

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

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

console.log("Listening on http://localhost:8000");
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.120.0/http/server.ts";

console.log("Listening on http://localhost:8000");
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",
    },
  });
});

同时,可以利用[标准库的文件服务工具](deno.land/std/http/fi…来提供静态文件。这些工具将设置适当的Content-Type 标头,并支持更复杂的功能,如开箱即用的[Range](developer.mozilla.org/en-US/docs/… 请求。

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

console.log("Listening on http://localhost:8000");
serve(async (req) => {
  return await serveFile(req, `${Deno.cwd()}/static/index.html`);
});

如果是使用一个成熟的HTTP框架如 [oak](oakserver.github.io/),可以这样来提供静态内容。

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

const app = new Application();
app.use((ctx) => {
  try {
    await context.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 获取文件或文件夹的路径,在解决了符号链接之后。

Github集成

如何将这些新奇的静态文件添加到我的部署中?

默认情况下,如果将GitHub仓库连接到Deploy,仓库中的所有文件都会以静态文件的形式出现,不需要任何修改。如果静态文件是用于存储在仓库中的少数资产,例如图片或博客的markdown文件,就更好了。

如果想在部署时生成静态文件,例如在使用Remix.run框架,或者使用静态网站生成器,开发者可以用deployctl工具部署代码和静态资产,然后在边缘提供当前的工作目录。

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

如果只是部署一次或者代码是从本地机器上部署,这种方式比较适合。但是,在实际操作中,托管在Github上的项目会希望使用Github Actions来运行构建步骤,生成HTML或其他静态内容,然后上传到Deno Deploy。

- 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构建的静态网站提供服务。该网站是由一个静态网站生成器建立的,除了静态文件,还包括一个/api/time 端点,动态返回当前时间。

该项目使用的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 });

部署的速度有多快?在这个 repo 上,从git push 到全球上线的时间大约是 25 秒。其中15秒是等待GitHub Actions runner的准备时间。对于不涉及GitHub Actions的 "automatic "模式部署,平均部署时间为1-10秒。