21k star 热榜聚合器,源码里藏着一颗会过期的 Cookie
- 热榜聚合器 NewsNow 将 66 个数据源聚合到单页面,以代码形式自部署,不依赖中心化服务
- 添加新源只需在 server/sources 目录下新增一个文件,无需修改注册表,实现零配置扩展
- 采用双时间窗口缓存策略,抓取失败时用旧缓存兜底,保证前端始终有数据可展示
- 架构分 server、shared、src 三层,服务端数据用 db0 抽象,推荐 Cloudflare D1,支持 Docker 部署
- 提出“可腐烂架构”,承认每个源会死,通过文件级隔离和缓存机制让死亡影响限定在单个模块
晚上十一点,你想知道今天全网都聊了什么。
微博开一遍,知乎开一遍,B 站再开一遍,抖音好像也有热榜,虎扑那边今天吵起来没有。十几个 App 轮着切,等你看完,半小时没了,真正想看的信息可能就那几条。
NewsNow 干的事很朴素,把这些热榜全部装进一个页面。GitHub 上 21,487 颗星,5,897 次 fork,TypeScript 写的,MIT 协议,作者 ourongxing,2024 年 9 月立项。它不追 AI,也不追什么 Agent 风口,就是一个热榜聚合器,互联网上古时期就有的品类。
那它凭什么。
我把源码翻了一遍,发现这个项目最有意思的地方,不是界面多干净,而是它对「抓取这件事随时会死」有着近乎偏执的防备。源码里甚至藏着一颗写死的微博 Cookie,明知道会过期,还是写死了。
这篇文章就顺着这条线拆。
热榜聚合是个老生意,NewsNow 换了个活法
聚合热榜的产品你大概率见过。今日热榜 TopHub 做了快十年,更早还有各种导航站和 RSS 阅读器。这个品类的宿命也几乎公开了,中心化服务要么被广告慢慢吃掉,要么因为平台封锁逐渐残废,要么干脆关停。
NewsNow 的差异点一句话能说清,它不是服务,是代码。你 Fork 一份,部署到 Cloudflare 或者自己服务器上,热榜聚合这件事就从「依赖别人的网站」变成了「自己的一件固定资产」。平台再怎么封锁,封的是部署方,你随时可以换 IP 换环境重跑。
这个定位在 README 里毫不掩饰,作者直接说当前版本是 demo,完整版之后再说。一个自称 demo 的项目拿了两万多星,说明它解决的问题足够真实。
加一个源,简单到只需要一个文件
先看它怎么组织抓取。整个项目 66 个数据源,微博、知乎、B 站、抖音、虎扑、雪球、财联社、Hacker News、GitHub Trending、Product Hunt,全放在 server/sources/ 目录下,一个源一个文件。
注册机制是我见过最短的之一。server/getters.ts 全文只有几行,核心就一句
import * as x from "glob:./sources/{*.ts,**/index.ts}"
Nitro 的 glob import 把目录下所有源文件自动收集进来,每个文件的 default 导出注册成一个 getter。你往目录里丢一个新文件,重启服务,新源就上线了,不用改任何注册表。zhihu 的实现短到可以整段贴出来,直接调知乎的私有接口
export default defineSource({
zhihu: async () => {
const url = "https://www.zhihu.com/api/v3/feed/topstory/hot-list-web?limit=20&desktop=true"
const res: Res = await myFetch(url)
return res.data.map(k => ({
id: k.target.link.url.match(/(\d+)$/)?.[1] ?? k.target.link.url,
title: k.target.title_area.text,
url: k.target.link.url,
}))
},
})
不是所有源都这么体面。server/utils/source.ts 里准备了三件套,defineSource 直接抓,defineRSSSource 解析 RSS,defineRSSHubSource 借道 RSSHub。最后这个值得注意,它的默认地址硬编码为公共实例 https://rsshub.rssforever.com,也就是说一部分源的可用性,寄托在别人维护的免费 RSSHub 上。这个伏笔后面会回收。
然后就是那颗 Cookie。打开 server/sources/weibo.ts,你会看到请求头里明晃晃写着一颗完整的 SUB=_2AkMWIuNSf8...,后面跟了长长一串。注释里还贴心地给了出处,引用自另一个开源项目 weibo-trending-hot-search。这是微博的游客会话 Cookie,社区里传来传去的那种,靠它才能访问 s.weibo.com 的热搜页。拿到 HTML 后用 cheerio 解析表格,连「新」「热」「爆」三种角标都映射成了微博官方的旗子图片。
一颗大家共享的、随时会被微博作废的 Cookie,就这么躺在两万星项目的源码里。
抓不到的时候,架构才真正开始工作
抓取的本质是寄生。宿主平台改版、加验证码、封 IP,任何一件事都能让一批源集体阵亡。NewsNow 服务端的核心 server/api/s/index.ts 花了最多笔墨处理的,恰恰是「抓不到怎么办」。
它设计了一个双时间窗口的缓存策略,shared/consts.ts 里定义了两个常量,TTL 是 30 分钟,Interval 默认 10 分钟。这两个数字控制着完全不同的两件事,作者在源码注释里自己解释了
// interval 刷新间隔,对于缓存失效也要执行的。本质上表示本来内容更新就很慢,
// 这个间隔内可能内容压根不会更新。
// 而 TTL 缓存失效时间,在时间范围内,就算内容更新了也要用这个缓存。
翻译成人话,interval 是「这个源的内容本来就更新得慢,别去烦它」,TTL 是「就算内容更新了,30 分钟内也先用缓存扛着」。请求进来先查缓存,两个窗口都不满足才真正去抓,抓回来 .slice(0, 30) 截前 30 条入库。如果抓取抛了异常,还有一层兜底,catch 块里直接把旧缓存吐回去,只要数据库里还有一份历史数据,前端就永远有东西可看。
更好的是 interval 不是全局一刀切,shared/sources.json 里给 66 个源逐个标了级。微博热搜和雪球是 2 分钟,华尔街见闻、财联社这些快讯源是 5 分钟,绝大多数热榜 10 分钟,Solidot 这种日更站直接放宽到 1 小时。更新快的抓得勤,更新慢的少打扰,既省资源,也别把宿主惹毛。
还有一个细节藏在 server/utils/source.ts 的 proxySource 函数里。它判断 process.env.CF_PAGES,如果部署在 Cloudflare Pages 上,微博这类国内源会改走一个代理地址去取。原因你猜得到,Cloudflare 的出口 IP 段早就被国内平台拉黑了,直连必挂。但反过来讲,这个保护只对 CF_PAGES 环境生效,用 Docker 起在自家机器上的用户,享受不到这层,得自己想办法。issue 区里「支持网络代理设置」的请求后面跟着一串留言,就是这个问题的回声。
三层结构,一处依赖一个文件
把上面的机制拼起来,整个项目其实是三层。server/ 是 Nitro 驱动的服务端,管抓取、缓存、OAuth 登录和数据同步。shared/ 放两端共用的类型和 66 个源的元数据。src/ 是 React 19 的前端,TanStack Router 加 TanStack Query 做路由和数据获取,jotai 管状态,@atlaskit/pragmatic-drag-and-drop 做栏目拖拽排序,UnoCSS 出样式,外加 PWA 离线可用。
服务端的数据落在 db0 抽象的数据库里,作者推荐 Cloudflare D1,建一张 cache 表就三列,id、updated、data,data 直接 JSON 字符串塞进去。简单粗暴,但和「反正 30 分钟就换一次」的场景严丝合缝。写缓存还用了 event.context.waitUntil,先响应请求,缓存慢慢写,Cloudflare Workers 的生命周期特性被用得很熟。源码注释里甚至记了一个坑,Cloudflare Worker 的运行时里 Date.now() 整个生命周期不会更新,这种经验一般只有真踩过才写得出来。
把服务搬回自己家
部署给了三条路。最简单是 Fork 后导入 Cloudflare Pages 或 Vercel,构建命令 pnpm run build,输出目录 dist/output/public。要登录和数据同步就去建一个 GitHub App,回调地址设成 https://your-domain.com/api/oauth/github,配好 G_CLIENT_ID、G_CLIENT_SECRET、JWT_SECRET 三个环境变量。自己有机器的,项目根目录 docker compose up 一步到位。首次启动记得把 INIT_TABLE 设为 true,建完表再关。
有个配置项很多人会忽略,ENABLE_CACHE。默认开,关掉的话每次请求都真抓,自己的 IP 自己负责。
说个题外的玩法,README 里挂了一个 MCP Server 的接入方式,npx 一行 newsnow-mcp-server,BASE_URL 指到你自己部署的实例。也就是说你把这个项目部署好之后,Claude 或者其他 AI 客户端可以直接查你这份热榜,「现在微博在爆什么」变成了一句可调用的工具指令。热榜聚合器顺手把自己变成了 AI 的数据源,这个组合比我预期的自然。
它随时会碎,区别在于碎得体面
回到开头那颗 Cookie。它不是孤例,是这个品类的缩影。微博哪天把这个共享会话作废,weibo 源就整个黑掉,直到有人发现并换上新的。issue 区里高赞的「知乎板块显示获取失败」挂着 8 条讨论没关,知乎的私有接口 api/v3/feed/topstory/hot-list-web 同样说变就变。借道 RSSHub 的那些源,命悬在 rsshub.rssforever.com 这个公共实例的存活上。你自己部署的实例再稳,也只能保证服务不宕,保证不了源不腐烂。
项目的维护状态也要如实说。18 位贡献者,主力一直是作者本人。最新版本 v0.0.41 发布于 2026 年 6 月 26 日,README 里那句划掉「欢迎贡献」、改写为「新版本即将推出,不再接受贡献」的公告,加上 Roadmap 末尾那句 italic 的「release when ready」,读起来更像一个漫长的等待。对一个标着 v0.0.x 的项目,这个信号不能装作看不见。
还有个小门道,Docker 部署的用户想在 66 个源里做增删,得改 shared/pre-sources.ts 再重新构建,issue 里「docker 部署的,是否可以修改、添加、禁用某些源」的提问有 7 条回复,都是绕路的答案。现阶段它对非 Cloudflare 部署的友好度,坦白讲一般。
但这些碎法都被设计过。一个源死掉,影响半径是它自己那一个文件。抓取挂了,缓存兜底。缓存也没有,前端只是那一栏空着,其他 65 个源照常工作。它没有假装自己不会碎,它只是让每一次碎,都碎在一个可以单独替换的格子里。
可腐烂架构
拆完之后我一直在想,这个项目有什么可以带走的。
我认为是一个可以被命名的思路,暂且叫可腐烂架构。做抓取类系统的人常有一种执念,想用技术手段一劳永逸地解决反爬,堆更深的代理池,养更真的浏览器指纹。NewsNow 反着来,它承认每一个源都会死,然后把「死」设计成系统的常规状态。源与源之间文件级隔离,死了不影响邻居。缓存让死亡有潜伏期,回退让死亡有尸体可用,双窗口让大部分死亡根本不会被用户看见。
这套思路的适用范围不止抓取。任何依赖外部不可控组件的系统,第三方 API、汇率接口、大模型服务、供应链里的某个开源包,都可以问自己同一个问题,如果它明天就死,我的影响半径是多少。半径能缩到一个可单独替换的模块,你就有资格睡个好觉。
至于该不该用 NewsNow,我的结论很简单。你想要一个自己掌控的热榜主页,愿意每月花几分钟维护部署,它现在就值得装,Docker 一条命令的事。你想要开箱即用、永远新鲜、英文源齐全的成品,那就等作者的完整版,或者先去演示站 newsnow.busiyi.world 过过眼瘾。
一颗会过期的 Cookie 不可怕,可怕的是没有为它的过期做任何准备。

评论互动