两年前,我通过 Ray(@so1ve)的博客系统 dolan-x 了解到了 Deno 这一新的 JavaScript 运行时,以及其对应的 SaaS 服务 Deno Deploy。为了尝试一下,我用 Deno + oak + LeanCloud 写了一个简单的友链管理 API。
不过,Deno 为了「用基于 HTTP 的分布式系统取代 npm」,「消除对 package.json 文件和 node_modules 文件夹的依赖,从而简化项目结构」等等原因1,采用了基于 HTTP 的模块管理,在代码中需要类似这样导入模块:
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";
assertEquals(1, 2);然而,
长链接会使代码库显得杂乱无章,尤其是在大型项目中; 随着项目规模的增长,管理长 URL 和版本变得越来越复杂和费时; URL 地址没有语义版本控制,使得依赖管理变得困难。1
如今,deno 支持了更加易用的导入方式。
最近,Deno Deploy 又被群友提及,看到 Deno 本身变化已经很大,便决定再尝试用 Deno 写点东西。恰好个人主页想加一个活动监测器,于是决定用 Deno 来实现。
Web 框架的选择
相比之前的友链,这次要做的东西比较简单,因此没有使用任何 Web 框架,而是直接使用标准库中的 Deno.serve。
Deno.serve 接受一个处理函数,函数接收 Request 对象并返回 Response。
const handler = (req: Request) => {
return new Response("Hello, World!");
};
Deno.serve(handler);路由
Deno 本身并没有提供路由系统,不过可以通过解析 pathname 手动实现。
const handler = (req: Request) => {
const { pathname } = new URL(req.url);
if (pathname === "/") {
return new Response("Hello, World!");
}
if (pathname === "/foo") {
return new Response("bar");
}
return new Response("Not Found", { status: 404 });
};如果路由稍微复杂一点,可以把所有处理函数封装到一个 Map(或者对象)中:
type Handler = (req: Request) => Response;
type HandlerMap = Record<string, Handler>;
const handlers: HandlerMap = {
"/": () => new Response("Hello, World!"),
"/foo": () => new Response("bar"),
};
const handler = (req: Request) => {
const { pathname } = new URL(req.url);
const matchedHandler = handlers[pathname];
if (matchedHandler) {
return matchedHandler(req);
}
return new Response("Not Found", { status: 404 });
};这种方式虽然简单,但对于只有几个接口的小项目来说已经完全够用了。
格式化响应
为了统一接口返回格式,我封装了一个响应对象,统一管理 success、message、data 等字段。
class FmtResponse<T> {
private code = 200;
private success = true;
private message = "success";
private data: T | null = null;
constructor(opts: { code?: number; message?: string; data?: T }) {
if (opts.code) {
this.code = opts.code;
if (opts.code >= 400) {
this.success = false;
}
}
this.message = opts.message ?? "success";
this.data = opts.data ?? null;
}
}再提供一个 json() 方法直接返回 JSON 响应:
public json() {
return new Response(
JSON.stringify({
success: this.success,
message: this.message,
data: this.data,
}),
{
status: this.code,
headers: {
"content-type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods":
"GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers":
"Content-Type, apikey",
},
},
);
}这样整个接口层就可以统一返回:
return new FmtResponse({
data: {
username: "redish101",
},
}).json();数据存储
选择 Deno Deploy 的很大一部分原因,就是它内置了 Deno KV,可以非常方便地进行持久化存储,而不需要额外搭建数据库。
const kv = await Deno.openKv();
await kv.set(["settings", "username"], "redish101");
const username = await kv.get(["settings", "username"]);
console.log(username.value);
// "redish101"Deno KV 使用键数组作为 Key,不仅支持层级组织数据,也方便后续范围查询。
值得一提的是,虽然 Deno KV 可以在本地使用,但目前仍需要启用对应的实验特性:
deno run --unstable-kv main.ts监控数据上报
监控客户端没什么复杂的,用 Rust 写了一个小程序,每隔 20 分钟向接口发送一次请求,上报当前设备在线状态,并作为 macOS 后台服务运行。
use std::env;
use tokio::time;
use tracing::info;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
info!("Welcome to remonitor!");
let apiurl = "https://redish101-remonitor.deno.dev/remonitor";
let apikey = env::var("APIKEY").unwrap();
let mut interval = time::interval(time::Duration::from_secs(1200));
loop {
interval.tick().await;
info!("Post status");
reqwest::Client::new()
.get(apiurl)
.header("apikey", apikey.clone())
.send()
.await
.expect("Failed to send request");
info!("Sent request");
}
}在 macOS 中作为后台服务运行
macOS 的 launchd 服务不会直接读取 Shell 中的环境变量,而只能读取通过 launchctl setenv 设置的变量,因此需要提前执行:
launchctl setenv APIKEY your_api_key然后编写一个简单的 plist 文件,并开启 KeepAlive,即可实现后台常驻运行。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>top.redish101.remonitor</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/client</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>这样,即使程序异常退出,也会被 launchd 自动重新拉起。
部署
最终选择部署到 Deno Deploy。
Deno Deploy 提供了 Deno 程序部署、KV 存储、定时任务等能力,并且部署流程非常简单,整体速度也不错。对于这种只有几个接口的小服务来说,基本可以做到开箱即用,不需要再维护服务器。
总结
相比两年前,Deno 已经成熟了不少,标准库更加完善,开发体验也提升了很多。
虽然我仍然认为 Deno 目前还不太适合作为大型生产项目的首选方案,但对于一些小工具、小型 Web API 或个人项目来说,它已经足够好用了。尤其配合 Deno Deploy 与 Deno KV,可以很方便的完成一个小服务的开发与部署。
如果只是想快速实现一个接口,而不是折腾各种框架和基础设施,那么 Deno 仍然是一个值得尝试的选择。
脚注
-
"What we got wrong about HTTP imports"(《我们对 HTTP 导入的错误认知》), Ryan Dahl, Deno Blog, 2024 年 7 月。 ↩ ↩2