最近又研究研究了vercel,看到了database,发现vercel可以直接整合各大数据库,其中MongoDB有免费够用的方案,于是我试了试,用数据库存储博客浏览量。

初始化

首先注册一个免费的MongoDB账号(无需绑卡,他真的,我哭死),免费版512 MB容量,普通使用绰绰有余,虽然是共享内存,但实测速度并不慢。 然后在vercel上一键整合到nuxt3-blog项目,控制界面就可以看到MONGODB_URI环境变量了: MONGODB_URI 实在太方便啦,关键还免费。OK,现在非代码部分已经over了,下面是喜闻乐见的Talk is cheap,show me the code环节。分两部分:

  1. 功能代码:数据库增删改查
  2. API代码:前端调用

功能代码

这里省略创建并缓存数据库连接的代码,我参考的是官方示例。另外有两个函数:

import { HeaderTabUrl } from "./../../../utils/types";
import { getCollection } from "./mongodb";

type VisitorsDb = {
  nid: number,
  ntype: HeaderTabUrl,
  nvisitors?: number,
}

// 只取 nid 和 nvisitors
const sqlOptions = {
  projection: { _id: 0, nid: 1, nvisitors: 1 }
};

// 获取整个列表的浏览量
export async function getVisitors (type: HeaderTabUrl) {
  const collection = await getCollection<VisitorsDb>();
  const query: Partial<VisitorsDb> = {
    ntype: type
  };
  const results = await collection.find(query, sqlOptions);
  return await results.toArray();
}

// 浏览量 +1
export async function increaseVisitors ({ id, type }: {id: number, type: HeaderTabUrl}) {
  const collection = await getCollection<VisitorsDb>();
  const preset: VisitorsDb = {
    nid: id,
    ntype: type
  };
  // 若已有,则 +1
  const result = await collection.findOneAndUpdate(preset, {
    $inc: {
      nvisitors: 1
    }
  }, sqlOptions);
  if (result) {
    return result.value.nvisitors + 1;
  } else {
    // 没有则新增
    await collection.insertOne({
      ...preset,
      nvisitors: 1
    });
    return 1;
  }
}

API代码

API代码分两部分,开发模式和部署模式。区别是:

  • 开发模式下,使用vite的HMR API调用功能代码。
  • 部署模式下,使用vercel的serverless function调用功能代码。

我发现这种方式可以封装为通用代码,目前写了一部分,挖个坑,放到后面写吧。下面贴一个示例:

// api/db/inc-visitors.ts 部署模式走这里,serverless服务端
// 另外还有 api/db/get-visitors.ts 这里不写了
import type { VercelRequest, VercelResponse } from "@vercel/node";
import { increaseVisitors } from "../../lib/api/db/visitors";

export default async function (req: VercelRequest, res: VercelResponse) {
  if (req.method.toUpperCase() === "POST") {
    try {
      res.status(200).send(await increaseVisitors({
        id: req.body.id,
        type: req.body.type
      }));
    } catch (e) {
      res.status(503).send(e.toString());
    }
  } else {
    res.status(405).send("Post only!");
  }
}
// dev-server/visitors.ts 开发模式走这里,devServer
import type { Plugin } from "vite";
import { getVisitors, increaseVisitors } from "../lib/api/db/visitors";

export default {
  name: "nb-visitors-plugin",
  configureServer (server) {
    server.ws.on("get-visitors", async (data, client) => {
      try {
        const result = await getVisitors(data.type);
        client.send("nb:get-visitors:result", result);
      } catch (e) {
        client.send("nb:get-visitors:result", e.toString());
      }
    });
    server.ws.on("increase-visitors", async (data, client) => {
      try {
        client.send("nb:inc-visitors:result", await increaseVisitors(data));
      } catch (e) {
        client.send("nb:inc-visitors:result", e.toString());
      }
    });
  }
} as Plugin;
// utils/public/detail.ts
// ......
if (item.id) {
  // 开发模式和部署模式用一样的callback
  const setVisitors = (data) => {
    item.visitors = data;
  };
  const query = {
    id: item.id,
    type: targetTab.url
  };
  if (isDev) {
    // 开发模式,HMR API 调用
    import.meta.hot.send("increase-visitors", query);
    devHotListen("nb:inc-visitors:result", setVisitors); // 封装的import.meta.hot.on,这里省略
  } else {
    // 部署模式,serverless 调用
    axios.post("/api/db/inc-visitors", query).then(res => setVisitors(res.data));
  }
}
// ......

其他

发现tinypng的api免费了,顺便集成到了图片上传,现在图片上传支持自动压缩 随着功能越做越多,感觉偏离了当初追求简单的核心。最近在学双拼,写完这篇文章可真艰难!