☰
Star History 后端架构解析:基于 Hono 的 GitHub Star 历史 SVG 图表服务
2026/10/7 7:42:02 网站建设 项目流程
  • 开发工具
  • 数据可视化

【免费下载链接】star-history

The de facto GitHub star history graph.

项目地址:https://gitcode.com/gh_mirrors/st/star-history
点击查看免费下载

Star History 的 backend/CLAUDE.md 定位为面向 Claude Code 的项目导览,但其内容浓缩了整个后端服务的架构骨架:一个基于 Hono 的服务器,专门负责把 GitHub 仓库的 Star 历史数据渲染成可嵌入、可缓存的 SVG 图表。本文将以此文档为核心骨架,逐层深入源码(backend/main.ts、backend/token.ts、backend/cache.ts 等),讲清从 Token 管理、数据抓取、LRU 缓存到 SVG 渲染优化的完整调用链,并给出可直接落地的开发、构建与配置实操。

一、整体架构:一条从 GitHub API 到 SVG 的流水线

该后端是一个典型的“单一职责”服务:不承载页面、不做数据库持久化,只暴露一个核心图表接口。根据 backend/CLAUDE.md 的 Architecture 章节,核心数据流可归纳为五个阶段:

  1. Token 管理(backend/token.ts):从环境文件中读取并轮换 GitHub API Token,规避单 Token 的速率限制。
  2. API 请求(shared/common/api.tsx):向 GitHub API 抓取 Star 历史原始数据。
  3. 缓存(backend/cache.ts):LRU 缓存,官方文档给出的容量指标为“10K 仓库、1GB 上限、24 小时 TTL”,缓存对象包括 Star 记录与 Logo URL。
  4. 图表生成(shared/packages/xy-chart.tsx):基于 D3 的 SVG 图表渲染,运行在 JSDOM 模拟的 DOM 环境里。
  5. SVG 优化:使用 SVGO 对渲染结果做多轮压缩,以最小化带宽占用。

这套流水线在 backend/main.ts 的/svg端点中完整串联:先查渲染结果缓存,命中即返回;未命中则查 Star 数据缓存,再未命中则携带 Token 去 GitHub 拉取数据,随后走 JSDOM + D3 渲染、SVGO 压缩、写回缓存的链路。

二、开发与构建命令

后端独立于前端维护自己的 TypeScript 工程,命令定义在 backend/package.json:

# 开发模式:tsx 直接运行,带热重载 pnpm dev # 构建:tsc 编译 TypeScript 项目 pnpm build

依赖集中在 backend/package.json 中,核心运行库包括:

  • 服务框架:hono(Web 框架)、@hono/node-server(Node 运行时适配);
  • 渲染管线:jsdom(DOM 模拟)、d3-axis / d3-scale / d3-selection / d3-shape(图表绘制)、svgo(SVG 优化)、satori(OG 卡片 HTML 到 SVG 转换);
  • 数据与基础设施:axios(GitHub API 请求)、lru-cache(缓存)、dayjs(日期处理)、winston(日志)。

三、GitHub Token 的加载与轮换机制

3.1 token.env 文件与 ENVPATH

文档明确给出了 Token 环境要求,结合 backend/token.ts 源码可以还原完整约定:

  • 服务启动时需要一个token.env文件,内容为 GitHub Token,每行一个;
  • 本地开发:通过环境变量ENVPATH指定 token 文件位置,例如ENVPATH=PATH_TO_YOUR_FILE pnpm dev;
  • 生产环境:默认读取仓库根目录下的./token.env(ENV_PATH_IN_RENDER常量,即process.env.ENVPATH || "./token.env")。

也就是说,Token 文件路径的解析优先级是:ENVPATH环境变量 > 默认的./token.env。

3.2 启动时的 Token 校验与冷启动退出

initTokenFromEnv()(backend/token.ts)在服务启动前执行三件事:

  1. 检查文件是否存在、内容是否为空,否则直接process.exit(-1)拒绝启动;
  2. 逐行切分 Token(兼容\r?\n换行),并调用api.getRepoStargazersCount("star-history/star-history", token)实测校验每个 Token 是否可用,不可用的 Token 会被剔除(日志中只打印前 8 位 + 后 4 位,避免泄露);
  3. 若没有任何可用 Token,同样process.exit(-1)终止进程。

3.3 轮换与冷却:应对速率限制

GitHub 未认证请求有严格的速率限制,单 Token 高并发容易触发 403。getNextToken()与markTokenExhausted()(backend/token.ts)实现了“轮流调度 + 限流冷却”:

  • getNextToken():以循环索引轮换使用可用 Token;若某个 Token 处于冷却期(exhaustedUntil中记录了过期时间戳)则跳过,全部冷却则返回null;
  • markTokenExhausted(token):当请求返回 403 时调用,将该 Token 冷却15 分钟(COOLDOWN_MS = 15 * 60 * 1000);
  • 主流程中,当getNextToken()返回null时,backend/main.ts 会返回 503“All GitHub API tokens are rate-limited, try again later”,配合 CDN 缓存兜底。

四、三层 LRU 缓存体系

backend/cache.ts 与文档描述的“10K repos / 1GB / 24h TTL”完全对应,且实际实现了三层独立缓存,每一层的关键参数如下:

缓存键条目上限内存上限TTL说明
cache(Star 数据)repo 名10,0001 GB24 h{ starRecords, starAmount, logoUrl },单仓库数据约 896 字节
svgCache(渲染结果)规范化查询串2,000400 MB24 h已渲染并压缩的图表 SVG
ogCardCache(OG 卡片)repo 名1,000200 MB24 h1200×630 的分享卡片 SVG

三层缓存统一使用lru-cache的maxSize+sizeCalculation做内存维度控制:

  • Star 数据层用utils.calcBytes(value)计算实际字节数;
  • SVG 层用Buffer.byteLength(value)计算字符串字节数。

缓存并非“黑盒”:recordCacheHit / recordCacheMiss / getAllCacheStats(backend/cache.ts)为每一层维护命中/未命中计数器,并输出entries、memory、hits、misses、hitRate统计,通过/healthz端点暴露(详见第八节),是排查“为什么没走缓存”的直接手段。

五、核心端点/svg全参数解析

5.1 查询参数规范化

在真正处理图表请求之前,backend/main.ts 会先做一次301 重定向规范化:把repos参数统一转为小写(GitHub 仓库名不区分大小写)后再拼接回查询串重定向。这样做的好处正如代码注释所言:让 CDN 对同一张图只缓存一个条目,避免大小写不同的 URL 击穿缓存。

5.2 参数清单与默认值

主端点(backend/main.ts)接受以下查询参数:

参数可选值默认值说明
repos逗号分隔的owner/repo列表必填,缺失返回 400单次最多MAX_REPOS_PER_REQUEST(20 个,见 backend/const.ts)
stylelandscape1空开启 OG 卡片模式,返回 1200×630 分享图
typedate/timelineDate坐标轴模式;也兼容date、timeline布尔型查询参数
sizemobile/laptop/desktoplaptop图表宽度,非法值回落到laptop
themedark/ 其他light明暗主题
transparenttruefalse透明背景
logscale任意值,false除外关闭对数坐标轴
legendbottom-right/ 其他top-left图例位置

文档中提到“Single/svgendpoint that accepts query params (repos, type, size, theme, transparent)”,源码将这一清单扩展为完整的参数矩阵,并在 backend/main.ts 中逐一解析。以type的解析逻辑为例:优先读type参数,其次兼容旧式?timeline、?date无值标记的写法;logscale只要出现(且不等于字符串"false")即开启对数轴。

5.3 图表类型与尺寸

对应文档的 Chart Types 与尺寸说明:

  • Date 模式:X 轴显示真实日期,适合观察项目在不同时间点的增长节奏;
  • Timeline 模式:X 轴显示从仓库创建起算的相对时间,适合对齐比较多个创建时间不同的仓库。

尺寸由getChartWidthWithSize()(backend/utils.ts)映射:

mobile -> 600px laptop -> 800px desktop -> 1000px

5.4 一次典型请求的完整链路

以文档给出的示例请求为蓝本(backend/main.ts):

/svg?repos=star-history/star-history&type=timeline&logscale&legend=bottom-right

其内部处理顺序为:

  1. 校验repos非空、数量不超过 20;
  2. style非landscape1,进入普通图表分支,解析 theme / transparent / type / logscale / legend / size;
  3. 以规范化后的查询串为键查svgCache,命中直接返回带缓存头的 SVG;
  4. 未命中则逐 repo 查 Star 数据缓存,缺失的 repo 集合携 Token 调用getRepoData()(shared/common/chart.tsx)抓数据与 Logo(Logo 并行转 base64);
  5. 数据经convertDataToChartData()(shared/common/chart.tsx 起)转换为图表坐标系;
  6. JSDOM 创建 DOM,D3 的XYChart渲染 SVG;
  7. fixJsdomSvgCasing()修复大小写,SVGO 压缩,写回svgCache并返回。

响应头固定为Content-Type: image/svg+xml;charset=utf-8与Cache-Control: public, s-maxage=86400, max-age=86400(backend/main.ts),即上下两层各缓存 24 小时。

六、数据抓取:如何用有限请求重建 Star 历史

Star 历史数据本身不在 API 的“一次返回”里,需要通过分页的stargazers端点重建。shared/common/api.tsx 的getRepoStarRecords()做了两处关键优化:

  • 分页采样:通过响应头Link解析总页数,当页数超过maxRequestAmount(后端默认 16,见 backend/const.ts)时,不是逐页拉取,而是均匀采样中间页,再结合每页首条记录的starred_at时间戳与“该页起始位置即当时 Star 数”的推论,重建出稀疏但趋势完整的星标时间线;
  • 末点锚定:最后调用getRepoStargazersCount()取当前总 Star 数,把“现在”这一时间点写入记录,保证曲线终点永远是最新值。

错误处理在 shared/common/chart.tsx 中分层:404 返回“Repo not found”,403 返回“rate limit exceeded”并触发 Token 冷却,401 返回 Token 无效;对于 404 与 501(无 Star 历史),后端会返回一条 count 为 0 的占位记录,从而让该 repo 也能写入缓存,避免对坏请求反复打 GitHub。

七、SVG 渲染与优化细节

图表本体由 shared/packages/xy-chart.tsx(D3 + 前端共用的图表库)负责,后端在其外层补了两道“后端特有”的工序:

7.1 JSDOM 大小写修复

D3 在生成滤镜(如feTurbulence、feDisplacementMap)时会输出驼峰命名,而 JSDOM 解析 SVG 时会强制小写,导致某些渲染器下滤镜失效。fixJsdomSvgCasing()(backend/utils.ts)在序列化后做精确替换,把元素名与属性名(filterUnits、baseFrequency、xChannelSelector、yChannelSelector)恢复为规范写法。这是“JSDOM 里跑 D3”这类方案最容易踩的坑,值得在自建同构渲染时直接复用。

7.2 SVGO 多轮压缩

const optimized = optimize(svgContent, { multipass: true }).data;

(backend/main.ts)multipass: true表示反复优化直至收敛,对内联的 base64 Logo 与坐标轴元素做最大化瘦身,直接决定每次嵌入 README 的带宽成本。

八、运维面:健康检查、日志与容器化

8.1 /healthz 与缓存观测

/healthz(backend/main.ts)返回 JSON,包含status: "OK"、commit(由环境变量GIT_COMMIT注入)以及三层缓存的完整统计(条目数、内存占用、命中率)。配合请求日志中间件(backend/main.ts,输出方法、路径、状态码与耗时),可以低成本地观测“缓存命中率是否健康、CDN 是否在替上游挡流量”。

8.2 日志与错误处理

日志基于winston(backend/logger.ts),级别由LOG_LEVEL环境变量控制(默认info),输出带时间戳与控制台着色;onError全局处理器(backend/main.ts)统一记录错误堆栈并返回 500。

8.3 Docker 部署

backend/Dockerfile 展示了生产部署形态:基于node:20-alpine,用 corepack 启用 pnpm 9,分层安装根目录共享依赖与 backend 依赖,并把shared/、gh/data/repos.json一并复制进镜像(OG 卡片模式依赖gh数据集中的仓库属性与排名),最终通过tsx main.ts启动,监听8080 端口。

九、共享代码的边界

文档特别强调:图表代码、API 客户端与类型定义全部放在根目录shared/(与前端共用),后端通过../shared/相对路径引用。这意味着对图表样式、坐标轴逻辑或数据类型结构的改动,会同时影响前端页面与后端 SVG 服务——在修改 shared/common/chart.tsx、shared/packages/xy-chart.tsx 等文件时,需要回归验证/svg的产物形态。这也解释了为什么后端依赖中同时出现 React 类型与 D3 相关包:渲染层本质是“复用前端图表库在服务端出图”。

结语

backend/CLAUDE.md用寥寥数十行勾勒出的,是一个高度工程化的“API 数据 → 服务端图表”闭环:Token 轮换与冷却应对 API 配额、三层 LRU 缓存对抗重复抓取、分页采样压低请求量、JSDOM + D3 + SVGO 实现服务端出图,最后用 CDN 友好的响应头与 301 规范化放大缓存效益。对于任何想构建“嵌入型图表服务”或“服务端 SVG 渲染”的开发者,这套组合的每一环——尤其是getRepoStarRecords的采样策略与fixJsdomSvgCasing的兼容性修补——都具备直接的借鉴价值。

  • 开发工具
  • 数据可视化

【免费下载链接】star-history

The de facto GitHub star history graph.

项目地址:https://gitcode.com/gh_mirrors/st/star-history
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询