- 前端
- 后端
- 网页爬虫
【免费下载链接】newsnow
Elegant reading of real-time and hottest news
NewsNow 是一个主打"优雅阅读实时热门新闻"的开源 Web 应用,前端基于 React + Vite + UnoCSS,后端基于 Nitro(h3)构建,内置 30 分钟缓存、GitHub OAuth 登录与跨设备数据同步、按来源更新频率自适应的抓取间隔,并原生支持 MCP(Model Context Protocol)Server 接入。本指南以仓库根目录的 README.md 为主线,结合 server 与 shared 目录的源码实现,完整讲解从零部署(Cloudflare Pages / Docker)、GitHub OAuth 与数据库配置、本地开发,到新增数据源抓取器的全流程,并深入剖析缓存与自适应抓取机制的底层代码逻辑。
说明:当前版本为 DEMO,仅支持中文内容;正式版将提供更好的定制化功能与英文内容支持(见 README.md 顶部 NOTE)。本指南所引用的路径均为当前仓库根目录下的相对路径。
核心功能特性
根据 README.md 的功能清单,并结合源码逐一印证:
- 优雅的阅读界面:多专栏卡片式布局,顶部提供「更多 / 关注 / 最热 / 实时」等分类视图,支持来源搜索与自定义专栏(界面见 screenshots/preview-1.png、screenshots/preview-2.png)。
- 实时获取最新热点新闻:内置数十个内容源抓取器,分布在 server/sources/ 目录,涵盖微博热搜、V2EX、知乎、IT 之家、财联社、Hacker News、GitHub Trending、联合早报等。
- GitHub OAuth 登录与数据同步:登录后可将自定义的专栏元数据(
PrimitiveMetadata)同步到服务端数据库,跨设备保持一致。 - 默认 30 分钟缓存:缓存 TTL 定义于 shared/consts.ts,即
TTL = 30 * 60 * 1000;登录用户可通过latest参数强制刷新获取最新数据。 - 自适应抓取间隔(最快每 2 分钟):每个来源可在配置中声明自己的
interval,避免频繁抓取导致 IP 被封禁(详见下文"缓存与自适应抓取机制")。 - 支持 MCP Server:可将 NewsNow 作为 MCP Server 接入 Claude 等支持 MCP 的客户端。
缓存与自适应抓取机制的源码级解析
这是本项目最具技术价值的设计,值得先于部署深入理解。相关常量定义在 shared/consts.ts:
export const TTL = 30 * 60 * 1000 // 缓存过期时间(30 分钟) export const Interval = 10 * 60 * 1000 // 默认刷新间隔(10 分钟)来源级自适应间隔
不同来源的内容更新频率差异巨大(热搜每分钟都在变,而部分博客可能一天才更新一次),因此 shared/pre-sources.ts 定义了五档间隔常量:
const Time = { Test: 1, // 测试用 Realtime: 2 * 60 * 1000, // 实时类,最快每 2 分钟 Fast: 5 * 60 * 1000, // 快速类,每 5 分钟 Default: Interval, // 默认 10 分钟 Common: 30 * 60 * 1000, // 常规类,每 30 分钟 Slow: 60 * 60 * 1000, // 慢速类,每 60 分钟 }例如 shared/pre-sources.ts 中微博配置了interval: Time.Realtime(2 分钟),联合早报配置了interval: Time.Common(30 分钟)。该配置经过 scripts/source.ts 预处理后写入 shared/sources.json,供服务端运行时读取。
缓存表与读写
缓存底层是一个简单的 SQLite 风格数据表(通过db0访问,可平滑切换到 Cloudflare D1),实现在 server/database/cache.ts:
CREATE TABLE IF NOT EXISTS cache ( id TEXT PRIMARY KEY, updated INTEGER, data TEXT );set用INSERT OR REPLACE写入序列化后的新闻数组并记录updated时间戳;getEntire还兼容了 Cloudflare D1 返回{ results: [] }结构的差异,说明该实现刻意适配了 CF Worker 环境。
命中缓存的判定逻辑
核心判定逻辑位于 server/api/s/index.ts,这也是对外提供单来源数据的GET /api/s?id=<sourceId>接口:
- 若
now - cache.updated < sources[id].interval,直接返回缓存(status: "success")。注释明确说明:interval 是"刷新间隔",即使缓存未失效也要遵守,因为该来源本身内容更新就很慢; - 若缓存已超过 interval 但仍在 TTL(30 分钟)内,则区分两种情况返回
status: "cache":- 请求未带
latest参数; - 请求带了
latest,但服务器未配置登录,或用户未登录(匿名用户无法强制刷新,防止缓存被绕过导致对上游的频繁请求);
- 请求未带
- 只有登录用户携带
latest参数时,才会真正调用抓取器getters[id]()拉取最新数据,截取前 30 条(slice(0, 30))后回写缓存; - 若抓取失败,则回退返回旧缓存;无缓存可回退时才抛出 500。
从源码结构可以推断,这一设计同时兼顾了上游站点压力控制(interval 限频 + TTL 复用 + 匿名用户不可强制刷新)与用户体验(登录用户可手动获取最新)。值得注意的一个细节是Date.now()在 Cloudflare Worker 整个运行期内不会更新,代码中也对此做了注释说明。
抓取器的统一入口
所有来源抓取器通过 glob 自动收集,见 server/getters.ts:import * as x from "glob:./sources/{*.ts,**/index.ts}",将每个文件导出的defineSource结果合并为一个getters映射,这也是新增数据源无需修改路由代码即可生效的原因。
接入 MCP Server
NewsNow 原生支持作为 MCP Server 被 Claude Desktop 等 MCP 客户端调用,配置方式直接取自 README.md,写入客户端的 MCP 配置文件中:
{ "mcpServers": { "newsnow": { "command": "npx", "args": [ "-y", "newsnow-mcp-server" ], "env": { "BASE_URL": "https://newsnow.busiyi.world" } } } }npx -y newsnow-mcp-server会临时拉取并运行官方 MCP 服务端包,通过BASE_URL指定新闻数据接口的域名。部署到自己的域名后,请将BASE_URL替换为你的域名,例如https://your-domain.com,这样 LLM 便能通过 MCP 协议实时检索你部署实例上的热点新闻。
部署指南
基础部署(无需登录与缓存)
如果不需要登录、同步和缓存功能,这是最快的上线方式:
- Fork 本仓库;
- 导入到 Cloudflare Pages 或 Vercel 等平台,平台会自动识别构建配置。
Cloudflare Pages 配置
在 Cloudflare Pages 的项目设置中填写:
- 构建命令:
pnpm run build - 输出目录:
dist/output/public
build脚本定义在 package.json:"build": "npm run presource && vite build",其中presource会先执行 scripts/favicon.ts 与 scripts/source.ts 生成图标清单和来源配置文件,再执行 Vite 构建(配合vite-plugin-with-nitro输出 Nitro 服务端产物)。
GitHub OAuth 配置
开启登录功能前,需要先创建 GitHub OAuth 应用:
- 在 GitHub 的 Applications 页面创建一个 GitHub App(无需申请任何特殊权限);
- 回调 URL 设置为:
https://your-domain.com/api/oauth/github(将your-domain替换为实际域名); - 获取 Client ID 和 Client Secret,填入下方环境变量。
登录的完整实现见 server/api/oauth/github.ts:服务端用code换取 access_token 后请求 GitHub 用户信息,将用户写入user表,再使用jose的SignJWT签发 60 天有效期的 HS256 JWT(payload 含id与type: "github"),最后重定向回首页并把jwt与用户头像昵称以 query 参数带回前端。Nitro 在 Cloudflare 环境设置 Cookie 存在已知问题,因此代码采用 URL 参数方式传递(源码中有对应注释)。
环境变量配置
本地运行时参考 example.env.server,将其重命名为.env.server并填写:
# Github Client ID G_CLIENT_ID= # Github Client Secret G_CLIENT_SECRET= # JWT Secret, 通常就用 Client Secret JWT_SECRET= # 初始化数据库, 首次运行必须设置为 true,之后可以将其关闭 INIT_TABLE=true # 是否启用缓存 ENABLE_CACHE=true各变量的实际用途与源码对应关系如下:
G_CLIENT_ID/G_CLIENT_SECRET:GitHub OAuth 凭证,在 server/api/oauth/github.ts 中用于换取 access_token;JWT_SECRET:登录态签名密钥,server/api/oauth/github.ts 用它签发 JWT,server/middleware/auth.ts 用jwtVerify校验请求头Authorization: Bearer <token>并写入event.context.user。从 server/middleware/auth.ts 可看到:若未配置这三个变量,服务端会禁用登录并对/api/me等需要认证的接口返回 506;若 JWT 校验失败,访问/api/me会返回 401;INIT_TABLE:首次运行必须为true,用于自动建表(cache表见 server/database/cache.ts,user表见 server/database/user.ts),初始化成功后建议关闭;ENABLE_CACHE:是否启用缓存,关闭时 server/database/cache.ts 的getCacheTable()会直接返回空;PRODUCTHUNT_API_TOKEN(额外变量):example.env.server 中还存在该变量,供 Product Hunt 等需要 token 的来源抓取使用,可按需填写。
数据库支持
本项目通过db0统一访问数据库,支持其全部 connector(可在 db0 官方 connectors 文档中查询,本项目主推 Cloudflare Pages 与 Docker 部署,Vercel 需自行搞定数据库)。官方推荐使用 Cloudflare D1。
以 D1 为例,配置步骤:
- 在 Cloudflare Worker 控制面板创建 D1 数据库;
- 在
wrangler.toml中配置database_id和database_name; - 若仓库中没有
wrangler.toml,可将 example.wrangler.toml 重命名并修改配置,其模板为:
name = "newsnow" pages_build_output_dir = "dist/output/public" compatibility_date = "2024-10-03" [[d1_databases]] binding = "NEWSNOW_DB" database_name = "newsnow-db" database_id = ""- 重新部署即可生效。
Docker 部署
对于 Docker 部署,只需使用项目根目录的 docker-compose.yml,在项目根目录执行:
docker compose up镜像为ghcr.io/ourongxing/newsnow:latest,默认映射宿主机4444端口,并通过命名卷newsnow_data持久化数据库数据。同样可以通过docker-compose.yml中的environment段配置G_CLIENT_ID、G_CLIENT_SECRET、JWT_SECRET、INIT_TABLE、ENABLE_CACHE、PRODUCTHUNT_API_TOKEN等环境变量(模板中已预留这些字段)。
本地开发
前提:需要 Node.js >= 20。
corepack enable pnpm i pnpm devcorepack enable启用包管理器(仓库通过 package.json 固定了packageManager: pnpm@10.30.3);pnpm i安装依赖(注意:项目对dayjs应用了补丁 patches/dayjs.patch,并在pnpm.overrides中锁定了h3、nitropack等关键依赖版本,请勿随意改动);pnpm dev启动开发服务器,命令为npm run presource && cross-env NODE_OPTIONS=--use-env-proxy vite dev,会先执行预处理脚本再启动 Vite(含 Nitro 服务端)。
其他常用脚本(见 package.json):
pnpm build:生产构建,产物位于dist/output/public;pnpm start:node --env-file .env.server dist/output/server/index.mjs,以本地 Node 方式运行构建产物(需.env.server);pnpm test:运行vitest测试,现有测试见 server/utils/date.test.ts 与 test/common.test.ts;pnpm typecheck:分别对 Node 侧与 App 侧执行tsc --noEmit类型检查;pnpm preview/pnpm deploy:在CF_PAGES=1下构建并用 wrangler 本地预览 / 部署到 Cloudflare Pages。
添加新数据源
README 明确说明:添加数据源请关注shared/sources(即 shared 目录下的来源配置,核心为 shared/pre-sources.ts)与 server/sources/ 目录,项目类型完备、结构简单。详细的逐步教程见 CONTRIBUTING.md,核心流程如下:
1. 注册来源配置
在 shared/pre-sources.ts 中注册来源。若给已有来源添加子来源(如给 Bilibili 增加"热门视频"),在其sub字段下新增子项;若是全新来源,则新增顶层条目:
"newsource": { name: "New Source", color: "blue", home: "https://www.example.com", column: "tech", // 选择合适专栏 type: "hottest" // 或 "realtime" }可选字段还包括interval(刷新间隔,默认 10 分钟)、desc、disable等,完整定义见 shared/types.ts 中的Source接口。
2. 实现抓取器
在 server/sources/ 目录下新建或修改文件(如newsource.ts),使用项目提供的defineSource与myFetch辅助函数。myFetch定义于 server/utils/fetch.ts,内置了浏览器 UA、10 秒超时与 3 次重试,可显著降低被上游站点拒绝的概率。以 GitHub Trending 抓取器 server/sources/github.ts 为例:
const trending = defineSource(async () => { const html: any = await myFetch("https://github.com/trending?spoken_language_code=") const $ = cheerio.load(html) const news: NewsItem[] = [] // ... 解析 DOM 并构造 NewsItem return news }) export default defineSource({ "github": trending, "github-trending-today": trending, })一个来源可同时导出多个 source id(如github与github-trending-today共用同一抓取逻辑),defineSource的聚合逻辑见 server/getters.ts。返回的数据必须符合NewsItem结构(定义于 shared/types.ts):
interface NewsItem { id: string | number // 唯一标识 title: string // 标题 url: string // 原文链接 mobileUrl?: string // 可选:移动端链接 pubDate?: number | string // 可选:发布时间 extra?: { hover?: string // 悬停提示文本 date?: number | string // 格式化日期 info?: false | string // 附加信息(如"✰ 12.3k") diff?: number // 时间差 icon?: false | string | { url: string; scale: number } // 图标 } }3. 重新生成来源文件
修改配置或抓取器后,运行:
npm run presource该命令会重新生成 shared/sources.json 等产物文件,使新来源在前端与GET /api/s接口中生效。
4. 测试与提交
启动pnpm dev后在浏览器中确认新来源正常展示,然后提交变更并创建 Pull Request(git checkout -b feature-name→git commit→git push origin feature-name)。注意:README 的贡献章节已声明,当前版本即将被新版取代,暂时不再接受外部贡献,此流程仅供理解架构与自行扩展参考。
GitHub 登录与数据同步机制
登录态的完整链路可以概括为:GitHub OAuth 换取 JWT → 前端存储 → 请求携带 Bearer Token → 中间件校验 → 读写用户数据表。
- 登录触发:server/api/login.ts 负责发起 OAuth 跳转,server/api/oauth/github.ts 处理回调并签发 JWT(有效期 60 天);
- 认证校验:server/middleware/auth.ts 对所有
/api/*路径生效,对/api/s、/api/me解析Authorization头中的 JWT 并注入event.context.user;未配置密钥时对非白名单接口返回 506; - 用户存储:server/database/user.ts 维护
user表(id、email、data、type、created、updated),addUser支持幂等插入,setData/getData用于读写用户的自定义专栏元数据; - 同步接口:server/api/me/sync.ts 提供
GET /api/me/sync(拉取同步数据)与POST /api/me/sync(写入同步数据,服务端会调用verifyPrimitiveMetadata校验结构),前端对应的状态管理见 src/atoms/primitiveMetadataAtom.ts 与 hooks 目录下的 useSync.ts。
路线图与扩展方向
根据 README.md 的 Roadmap,项目后续计划包括:
- 添加多语言支持(英语、中文,更多语言即将推出);
- 改进个性化选项(基于分类的新闻、保存的偏好设置);
- 扩展数据源以覆盖多种语言的全球新闻。
开源协议
本项目基于 MIT 协议开源,版权归 © ourongxing 所有。
- 前端
- 后端
- 网页爬虫
【免费下载链接】newsnow
Elegant reading of real-time and hottest news
相关推荐
x64dbg 的 eStepInto/esti 命令详解:Trap-Flag 单步与首次异常传递策略
x64dbg 的 eStepInto/esti 命令详解:Trap Flag 单步与首次异常传递策略 eStepInto(别名 esti)是 x64dbg 调试
前端后端网页爬虫NewsNow终极指南:构建高效新闻聚合系统的完整方案
NewsNow终极指南:构建高效新闻聚合系统的完整方案 在信息爆炸的时代,新闻阅读效率成为现代专业人士的核心竞争力。NewsNow作为一款基于现代化技术栈构建的
前端后端网页爬虫如何快速构建新闻数据聚合平台:Newscatcher完全指南
如何快速构建新闻数据聚合平台:Newscatcher完全指南 Newscatcher是一款强大的Python工具,能够帮助开发者和数据爱好者轻松从几乎任何网站以
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考