在用户侧生成动态内容方面,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.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秒。