在 2024 年 8 月 11 日,我使用 Fronti 这个开源的 Astro 主题构建了你们正在浏览的网站。当时非常匆忙,有两篇博文要写,首页就先保持初始状态了。
8 月 18 日,博文已经没有要写的了,就开始对首页进行大的整改,期间这几条 GitHub 统计数据可是花了我好多头发,但当时写的匆忙能跑不就行了,不知不觉明明知道给自己挖了一个坑
问题分析
我为了防止用户撞到 GitHub API 速率限制↗而无法加载我的 GitHub 统计数据,直接把我 GitHub 账号的 PAT (Personal access tokens) 令牌硬编码进请求头里让用户去请求数据,这样就出现了几个大问题:
- 每位用户都要去找 GitHub 请求数据,访问的人多起来或遭到 DDoS 攻击非常容易导致我的 GitHub 账号受到速率限制。
- 原因同上,有些用户无法访问 GitHub,他们也就无法正常加载我的 GitHub 统计数据。
- 虽然公开的是毫无权限的令牌,但有心之人可以拿我的令牌去不断请求 GitHub API,使我的 GitHub 账号受到速率限制,导致正常用户无法加载我的 GitHub 统计数据,我也会跟着遭殃。
如何实现
总的来说,我需要让服务端向 GitHub 请求数据,GitHub 把数据返回给服务端,服务端做一个缓存,然后把请求到的数据返回给用户,缓存未失效之前再有人访问时服务端会直接把数据发送给客户端,无需再向GitHub请求数据。
这,听起来反向代理挺适合的,但我不能完全代理 api.github.com↗,这样用户直接把我的反向代理当作镜像站来请求,这不就完犊子了!
那就给Astro加API端点吧!我这么想着,看了一会文档,开始尝试,构建失败7次,运行失败6次,我放弃了,开始寻找别的方法。
github-readme-stats 的启示
在那几条GitHub统计数据完工前,我首页贴着的是两张 github-readme-stats.vercel.app↗ 提供的卡片,这是一个开源项目,我便去他们的 GitHub 仓库↗翻了翻代码,找到了灵感。
最终的解决方案是使用 Vercel 的 Serverless 服务,实现一套带缓存的,只能访问特定的 GitHub API 地址的 API 端点。
实现过程
知道如何实现还不行,现在我对相关代码的编写还是一头雾水,还得寻找更多的资料。
首先,我在网络上搜集相关文档,翻到了 Serverless functions with Vercel↗ 这篇文章,了解了基本语法,顺便写了个Hello, world!。万事都从Hello, world!开始
export default async ( _: any, res: { json: (data: any) => void; setHeader: (key: string, value: string) => void; },) => { res.setHeader("Content-Type", "application/json"); res.json({ "Hello, ": "world!" });};部署完后,使用 curl 访问刚部署的 API 地址,得到了以下结果
{ "Hello, ": "world!" }接下来,就开始编写 API 吧!
获取项目信息的 API
先把要用到的常量保存在 src/consts.ts 文件中
const GITHUB_TOKEN = "ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";export const GITHUB_NAME = "Buer-Nahida";
export const method = "POST";export const headers = { "Content-Type": "application/json", Authorization: `Bearer ${GITHUB_TOKEN}`,};
export const PROJECT_BODY = JSON.stringify({ query: `{ r0: repository(owner: "${GITHUB_NAME}", name: "antonym.nvim") { stargazerCount watchers { totalCount } forkCount } r1: repository(owner: "AkashaTerm", name: "AkashaTerminal") { stargazerCount watchers { totalCount } forkCount } r2: repository(owner: "${GITHUB_NAME}", name: "fcitx5-switch.nvim") { stargazerCount watchers { totalCount } forkCount } }`,});再创建 api/project.ts 文件,编写获取项目信息的 API:
import { PROJECT_BODY, headers, method } from "../src/consts";
export default async ( _: any, res: { json: (data: any) => void; setHeader: (key: string, value: string) => void; },) => { res.setHeader("Content-Type", "application/json"); res.json( await fetch("https://api.github.com/graphql", { headers, method, body: PROJECT_BODY, }).then((r) => r.json()), );};推送到 Vercel 上部署,使用 curl 访问刚创建的 API,得到结果:
{ "data": { "r0": { "stargazerCount": 1, "watchers": { "totalCount": 1 }, "forkCount": 0 }, "r1": { "stargazerCount": 0, "watchers": { "totalCount": 0 }, "forkCount": 0 }, "r2": { "stargazerCount": 1, "watchers": { "totalCount": 1 }, "forkCount": 0 } }}解决跨域问题
好,看起来没问题,那就修改下我的网站的代码试试看:

果然没有那么简单,遇到了跨域问题,但难不倒我,经过一番搜索,寻找到了解决方法:
先向 src/consts.ts 文件中添加下面这一行:
export const SIDE = "https://nahida.im";再修改 api/project.ts 文件:
import { SIDE, PROJECT_BODY, headers, method } from "../src/consts";
export default async ( _: any, res: { json: (data: any) => void; setHeader: (key: string, value: string) => void; },) => { res.setHeader("Content-Type", "application/json"); //+++++++++++++++++++++++++++++++++++++++++++++++// res.setHeader("Access-Control-Allow-Origin", SIDE); //+++++++++++++++++++++++++++++++++++++++++++++++// res.json( await fetch("https://api.github.com/graphql", { headers, method, body: PROJECT_BODY, }).then((r) => r.json()), );};然后获取项目信息的 API 就完成了!
获取除了 Commit 数量外的其它信息的 API
还是一样,先向 src/consts.ts 文件中添加代码:
export const STATS_BODY = JSON.stringify({ query: `{ user(login: "${GITHUB_NAME}") { pullRequests(first: 1) { totalCount } openIssues: issues(states: OPEN) { totalCount } closedIssues: issues(states: CLOSED) { totalCount } contributionsCollection { totalCommitContributions } repositories(first: 100) { nodes { stargazerCount } pageInfo { hasNextPage, endCursor } } } }`,});再创建 api/stats/index.ts 文件:
import { SIDE, STATS_BODY, headers, method } from "../../src/consts";
export default async ( _: any, res: { json: (data: any) => void; setHeader: (key: string, value: string) => void; },) => { res.setHeader("Content-Type", "application/json"); res.setHeader("Access-Control-Allow-Origin", SIDE); res.json( await fetch("https://api.github.com/graphql", { headers, method, body: STATS_BODY, }).then((r) => r.json()), );};获取 Commit 数量的 API
创建 api/stats/total-commit-count.ts 文件:
import { SIDE, CACHE, GITHUB_NAME, headers } from "../../src/consts";
export default async ( _: any, res: { json: (data: any) => void; setHeader: (key: string, value: string) => void; },) => { res.setHeader("Content-Type", "application/json"); res.setHeader("Access-Control-Allow-Origin", SIDE); res.json( await fetch( `https://api.github.com/search/commits?q=author:${GITHUB_NAME}&per_page=1`, { headers }, ) .then((r) => r.json()) .then((v) => ({ total_count: v.total_count })), );};缓存功能
前面提到过,我不光要加快 api.github.com↗ 的访问速度,还要做一个缓存,在一番寻找后,我选择了 lru-cache↗ 这个库来实现缓存:
安装这个库
npm install lru-cache --save修改相关代码 & 最终预览
src/consts.ts
import { LRUCache } from "lru-cache";
export const SIDE = "https://nahida.im";
export const CACHE = new LRUCache({ max: 10, ttl: 1000 * 60 * 5,});
const GITHUB_TOKEN = "ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";export const GITHUB_NAME = "Buer-Nahida";
export const method = "POST";export const headers = { "Content-Type": "application/json", Authorization: `Bearer ${GITHUB_TOKEN}`,};
export const STATS_BODY = JSON.stringify({ query: `{ user(login: "${GITHUB_NAME}") { pullRequests(first: 1) { totalCount } openIssues: issues(states: OPEN) { totalCount } closedIssues: issues(states: CLOSED) { totalCount } contributionsCollection { totalCommitContributions } repositories(first: 100) { nodes { stargazerCount } pageInfo { hasNextPage, endCursor } } } }`,});export const PROJECT_BODY = JSON.stringify({ query: `{ r0: repository(owner: "${GITHUB_NAME}", name: "antonym.nvim") { stargazerCount watchers { totalCount } forkCount } r1: repository(owner: "AkashaTerm", name: "AkashaTerminal") { stargazerCount watchers { totalCount } forkCount } r2: repository(owner: "${GITHUB_NAME}", name: "fcitx5-switch.nvim") { stargazerCount watchers { totalCount } forkCount } }`,});api/project.ts
import { SIDE, CACHE, PROJECT_BODY, headers, method } from "../src/consts";
export default async ( _: any, res: { json: (data: any) => void; setHeader: (key: string, value: string) => void; },) => { const k = 0; let v = CACHE.get(k); if (!v) { v = await fetch("https://api.github.com/graphql", { headers, method, body: PROJECT_BODY, }).then((r) => r.json()); CACHE.set(k, v); } res.setHeader("Content-Type", "application/json"); res.setHeader("Access-Control-Allow-Origin", SIDE); res.json(v);};api/stats/index.ts
import { SIDE, CACHE, PROJECT_BODY, headers, method } from "../src/consts";
export default async ( _: any, res: { json: (data: any) => void; setHeader: (key: string, value: string) => void; },) => { const k = 0; let v = CACHE.get(k); if (!v) { v = await fetch("https://api.github.com/graphql", { headers, method, body: PROJECT_BODY, }).then((r) => r.json()); CACHE.set(k, v); } res.setHeader("Content-Type", "application/json"); res.setHeader("Access-Control-Allow-Origin", SIDE); res.json(v);};api/stats/total-commit-count.ts
import { SIDE, CACHE, GITHUB_NAME, headers } from "../../src/consts";
export default async ( _: any, res: { json: (data: any) => void; setHeader: (key: string, value: string) => void; },) => { const k = 3; let v = CACHE.get(k); if (!v) { v = await fetch( `https://api.github.com/search/commits?q=author:${GITHUB_NAME}&per_page=1`, { headers }, ) .then((r) => r.json()) .then((v) => ({ total_count: v.total_count })); CACHE.set(k, v); } res.setHeader("Content-Type", "application/json"); res.setHeader("Access-Control-Allow-Origin", SIDE); res.json(v);};全文完。
