使用 Vercel 有选择地反向代理 GitHub API

使用 Vercel 有选择地反向代理 GitHub API

2024年 八月 22日 星期四
1793 字 · 13 分钟

在 2024 年 8 月 11 日,我使用 Fronti 这个开源的 Astro 主题构建了你们正在浏览的网站。当时非常匆忙,有两篇博文要写,首页就先保持初始状态了。

8 月 18 日,博文已经没有要写的了,就开始对首页进行大的整改,期间这几条 GitHub 统计数据可是花了我好多头发,但当时写的匆忙能跑不就行了,不知不觉明明知道给自己挖了一个坑

问题分析

我为了防止用户撞到 GitHub API 速率限制而无法加载我的 GitHub 统计数据,直接把我 GitHub 账号的 PAT (Personal access tokens) 令牌硬编码进请求头里让用户去请求数据,这样就出现了几个大问题:

  1. 每位用户都要去找 GitHub 请求数据,访问的人多起来或遭到 DDoS 攻击非常容易导致我的 GitHub 账号受到速率限制。
  2. 原因同上,有些用户无法访问 GitHub,他们也就无法正常加载我的 GitHub 统计数据。
  3. 虽然公开的是毫无权限的令牌,但有心之人可以拿我的令牌去不断请求 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
}
}
}

解决跨域问题

好,看起来没问题,那就修改下我的网站的代码试试看:

x

果然没有那么简单,遇到了跨域问题,但难不倒我,经过一番搜索,寻找到了解决方法:

先向 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 这个库来实现缓存:

安装这个库

Terminal window
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);
};

全文完。


Thanks for reading!

使用 Vercel 有选择地反向代理 GitHub API

2024年 八月 22日 星期四
1793 字 · 13 分钟