Redish101 Blog
RSS

Deno再尝试:使用Deno编写一个简单的WebAPI

2024 年 7 月 23 日 · 技术 · 约 1.4 千字

两年前,我通过 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 });
};

这种方式虽然简单,但对于只有几个接口的小项目来说已经完全够用了。

格式化响应

为了统一接口返回格式,我封装了一个响应对象,统一管理 successmessagedata 等字段。

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 仍然是一个值得尝试的选择。

脚注

  1. "What we got wrong about HTTP imports"(《我们对 HTTP 导入的错误认知》), Ryan Dahl, Deno Blog, 2024 年 7 月。 2

Deno再尝试:使用Deno编写一个简单的WebAPI

https://blog.redish101.top/articles/try-deno-again
本文作者Redish101
发布于2024 年 7 月 23 日
许可协议CC-BY-NC-SA 4.0
转载或引用本文时请遵守许可协议,注明出处。

Command Palette

Search for a command to run...