观前提示:本文是一篇对于博客一个功能的实现思路,尽量少涉及实际代码。其中的一些实现的方式不适合用于真正的生产环境,请把每个部分当作单个技术的知识科普轻松观看。

引子

班固米爱看动画的读者肯定不陌生,它是一个可以记录你的看番历史记录的网站,会根据你的活动生成时间线。

班固米

于是,期末周被高数折磨到头秃边复习边沉迷看番的我,产生了一个天才的想法:

为什么不把我每天干了些什么事情记录下来呢?每天过完,回顾这一天,成就感满满不是吗?

期末复习?复习是什么?能吃吗?

这个想法就诞生了,给自己的博客加上一个 Done List(记录做完的事情的列表),实现以下功能:

  • 能让静态博客加载动态 Done List 数据文件,所有人都可以看到干了些什么
  • 后端能够提供 DL 的数据
  • 私有前端能够访问后端进行增删改查

可以说是赛博露出

需求可行性分析

要想给博客加上动态生成的内容非常简单,因为我这个 DL 和“说说”是高度相似的,可以直接用说说的逻辑。对于我的博客主题 Butterfly,我自己再清楚不过了,它支持说说(以及对说说的评论)。

很好,我们只需要在一个 URL 上面,提供一个 JSON 即可(以下摘自 Butterfly 的示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"author": "Butterfly",
"avatar": "https://butterfly.js.org/img/avatar.png",
"date": "2024-06-21 23:33:26",
"content": "This is a sample content for **Author 1**.",
"key": "key-1",
"tags": ["tags1", "tags2"]
},
{
"author": "Butterfly",
"avatar": "https://butterfly.js.org/img/avatar.png",
"date": "2024-06-20 23:33:26",
"content": "This is a sample content for **Author 2**.",
"key": "key-2",
"tags": ["tag2", "tag3"]
}
}

Great! 博客前端已经做好了我们不用干,现在我们只需要写后端和管理面板,然后用反向代理暴露出 JSON 绑定到域名即可。

ASP.NET

众所周知,我的博客本来就有评论区,它托管在一台垃圾小水管服务器上面。所有的服务都是 Docker 跑的。

而由于我目前比较熟练的只有 C# 和 dotnet,所以我自然选择了鞭打 AI 入门 ASP.NET。

创建应用不必说,Visual Studio 甚至贴心地生成了 Dockerfile,我们只要专注写代码即可。

本地 JSON 传出

创建好项目之后,首先映入眼帘的就是这一行:

1
app.MapGet("/", () => "Hello World");

吓死了。把鼠标放上去,发现第二个参数是个委托,而其中的则是 Lambda 表达式。

也就是说,我们可以光速翻开 C# 图解教程补基础改一改,让它返回一个本地文件:

1
2
3
4
5
6
7
8
9
10
app.MapGet("/done.json", () =>
{
    if (!File.Exists(_dataFile))
    {
        // 保证存在一个空数组文件
        File.WriteAllText(_dataFile, "[]");
    }
    var json = File.ReadAllText(_dataFile);
    return Results.Text(json, "application/json");
});

增删改查

根据上面的 Json,我们不难给它建模(你甚至可以直接把 Json 粘贴为一个类)。

继续鞭打 AI 黑奴

然后,给每一个数据项加一个唯一的 UUID,每次调用 POST 提交数据的时候自动生成 ID,并记录时间作为 Data。再写一个接口,根据这个 ID 进行删除操作即可。注意得避免请求过快或延迟导致多线程把文件炸掉,可以用锁对文件操作进行排队。

由于代码较为复杂,篇幅有限,这里略去,直接把接口描述出来,AI 都能完整给出实现。我们这篇文章主要记录整个项目结构和部署过程,而非 C# 代码的讲解。

写完后端之后,我们仔细来看看整个 ASP.NET 的结构:

项目结构

  • Program.cs,入口点,用于初始化和编写后端服务
  • appsettings.json,控制应用程序的设置,比如 Log 等级
  • Dockerfile,这个不多说,用于打包 Docker 镜像
  • wwwroot(一开始可能没有,你可以自己创建),把前端打包放到这里
  • Properties/launchSettings.json,用于配置启动时候的信息,比如默认端口号

很好,刚才我们成功写完了后端,既然知道前端的放置位置了,而且我的前端设计能力有限,我们直接让 Claude 老师写一个 Web 界面扔到 wwwroot,用 JS 实现对接口的请求:

前端界面

完美!接下来,就是把整个项目打包部署到服务器上了。

私有 Azure 存储库

Azure 容器初探中,我们已经涉及了 Azure 容器注册表,因此,推送和拉取在这里不再涉及。只给出一份去除了敏感信息的 Docker Compose 文件,作为部署关键节点的记录。

1
2
3
4
5
6
7
8
9
services:
donelistapi:
image: wtf.azurecr.io/donelistapi:latest
container_name: donelistapi
ports:
- "1234:8080"
user: "114:514"
volumes:
- ./done.json:/app/done.json

要注意,容器内部,应用程序所在的根目录是 /app,别挂载错位置了,程序会炸。

反向代理

配置你的 Nginx

我服务器上面 ACME 会自动给我的域名生成 *.samhou.top 的泛域名证书并重加载 Nginx,所以反向代理非常容易(SSL 怎么配我们在此不涉及)。我们只需要加一个 server 块,把路径映射到内部的 1234 端口就行了。记得添加 CORS 头,避免跨域问题导致请求失败(因为 CORS 只能加一个域名,多域名需要根据请求来匹配):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
map $http_origin $cors_allow_origin {
    default "";
    "https://blog.samhou.top" $http_origin;
    "http://localhost:4000" $http_origin;
}
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name api.samhou.top;
    location = /done.json {
        limit_except GET {
           deny all;
        }
        proxy_pass http://127.0.0.1:1234/done.json;
        if ($cors_allow_origin != "") {
            add_header Access-Control-Allow-Origin $cors_allow_origin;
            add_header Access-Control-Allow-Methods GET;
            add_header Access-Control-Allow-Headers Content-Type;
        }
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    location / {
        return 404;
    }
}

最近阿里云 CDN 免费了,大家也可以去嫖一下,反正给海外服务器上一点保护和加速不是坏事。只要回源服务器 IP,注意一下请求头就行了。

私有面板访问

哎,这个时候眼尖的读者可能已经发现了:你根本就没有把整个程序反向代理出去啊?怎么访问 WEB 面板呢?难道打开端口用 http 访问吗?

对于这个问题:我们的面板和 API 根本不需要开放到公网。反正使用的就我一个人,为什么要共享出去给别人看呢,还需要对增删的 API 进行鉴权。

这时候就轮到 Tailscale 登场了!它是一个 VPN 服务,允许我们穿过服务器的防火墙(不需要开放端口),并加密所有数据流量。

安装配置 Tailscale 的内容此处不再赘述。TS 会为我们提供一个虚拟 IP,用这个 IP + 服务器上实际服务的端口,就能够直接访问对应服务器上防火墙后面的服务。

结语

到这里,所有部署都已经完成了!现在回到第一步修改一下静态博客配置,然后部署。现在 DL 已经开始运行了:Done List