NewsNow 项目技术架构文档
快速预览:本文从宏观视角拆解 NewsNow 项目的技术架构——一个基于 Vite + Nitro 的全栈应用,前端 React SPA + TanStack Router,后端 H3 API + 多数据库适配,支持 45+ 新闻源的聚合抓取,可部署到 Cloudflare Pages、Node.js、Vercel、Docker 等多种环境。
关键词:NewsNow、架构、React、Vite、Nitro、TanStack Router、Jotai、UnoCSS、Docker、Cloudflare Pages
核心观点
NewsNow 的架构可以用一句话概括:Vite 统一构建,Nitro 适配部署,shared 目录桥接前后端。
它不是传统的“前端 + 后端 API”分离架构,而是把 React SPA 和 H3 Server 打包成一个统一的构建产物。前端路由由 TanStack Router 处理,API 路由由 Nitro 处理,两者共享 shared/ 目录中的类型、工具函数和数据源定义。
整体架构图
┌─────────────────────────────────────┐
│ Vite Build │
│ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ React SPA│ │ Nitro Server │ │
│ │ (Client) │ │ (Server) │ │
│ └────┬─────┘ └──────┬───────┘ │
│ │ │ │
│ └──────┬──────────┘ │
│ │ │
│ ┌───────┴────────┐ │
│ │ shared/ │ │
│ │ types, sources, │ │
│ │ utils, metadata │ │
│ └────────────────┘ │
└─────────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌─────┴─────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ Node.js │ │ Cloudflare │ │ Docker │
│ Server │ │ Pages │ │ Container │
│ │ │ │ │ │
│ SQLite │ │ D1 Database │ │ SQLite │
└───────────┘ └─────────────┘ └─────────────┘
目录结构
newsnow/
├── src/ # 前端 React 应用
│ ├── routes/ # TanStack Router 文件路由
│ ├── components/ # UI 组件
│ ├── hooks/ # 自定义 Hooks
│ ├── atoms/ # Jotai 状态原子
│ ├── utils/ # 客户端工具函数
│ └── styles/ # 全局样式
├── server/ # 后端 Nitro 服务
│ ├── api/ # API 路由(H3)
│ ├── sources/ # 新闻源抓取器(45+)
│ ├── database/ # 数据库层(db0)
│ ├── middleware/ # 中间件(JWT 认证)
│ ├── mcp/ # MCP 协议服务
│ └── utils/ # 服务端工具函数
├── shared/ # 前后端共享代码(核心桥梁)
│ ├── pre-sources.ts # 数据源原始定义
│ ├── sources.json # 构建时生成的扁平数据源
│ ├── metadata.ts # 栏目定义
│ ├── types.ts # 共享类型
│ └── utils.ts # 共享工具函数
├── scripts/ # 构建时脚本
├── public/ # 静态资源
├── tools/ # 构建工具(rollup-glob 插件)
├── vite.config.ts # Vite 主配置(插件链)
├── nitro.config.ts # Nitro 部署配置
├── uno.config.ts # UnoCSS 配置
└── pwa.config.ts # PWA 配置
前端架构
路由:TanStack Router 文件路由
路由通过 @tanstack/router-plugin 自动生成 routeTree.gen.ts。项目有三个路由:
| 文件 | 路径 | 功能 |
|---|---|---|
__root.tsx | 全局 | 布局框架:Header + Main + Footer + Toast + SearchBar |
index.tsx | / | 首页,显示“关注”或“最热”栏目 |
c.$column.tsx | /c/:column | 栏目页,支持 focus/hottest/realtime |
状态管理:Jotai Atoms
状态层用 Jotai,核心是一个持久化到 localStorage 的 primitiveMetadataAtom,存储用户的所有栏目配置和关注源。衍生出 focusSourcesAtom、currentColumnIDAtom、currentSourcesAtom 等派生原子。
数据获取:TanStack Query
数据获取的核心模式是:每个新闻卡片用 useQuery 以 ["source", sourceId] 为 key 请求 /api/s?id={id}。staleTime 设为 Infinity,永远不会自动过期,依赖手动刷新或缓存判断。还有一个批量接口 POST /api/s/entire,一次性获取所有关注源的数据。
样式:UnoCSS
用 UnoCSS 的 presetWind3(Tailwind 兼容),加了一个巧妙的设计:动态 safelist。因为新闻源的颜色是动态的(从 sources.json 读取),UnoCSS 在静态分析时无法感知,所以配置里自动把所有源的 color 字段对应的 class 加入 safelist,确保不被 tree-shake 掉。
自动导入(unimport)
客户端通过 unimport 扫描 src/hooks、shared、src/utils、src/atoms 四个目录,所有导出自动成为全局可用。React 的 useState、useEffect,Jotai 的 useAtom、useAtomValue,以及 clsx(别名 $)都不需要手动 import。
$() 就是 clsx() 的短名,贯穿所有组件:$("flex items-center", condition && "text-red")。
后端架构
API 路由(H3 + Nitro)
| 路由 | 方法 | 功能 |
|---|---|---|
/api/s?id={id} | GET | 获取单个新闻源数据(带缓存) |
/api/s/entire | POST | 批量获取多个新闻源的缓存数据 |
/api/login | GET | 跳转 GitHub OAuth 授权页 |
/api/oauth/github | GET | OAuth 回调,签发 JWT |
/api/me/sync | GET/POST | 用户元数据同步(登录后) |
/api/enable-login | GET | 检查登录功能是否启用 |
/api/latest | GET | 返回当前版本号 |
/api/mcp | POST | MCP 协议端点(给 AI 助手用) |
缓存策略
数据缓存有两层时间控制:TTL(30 分钟)和 Interval(10 分钟,每个源可自定义)。
在 Interval 内,永远返回缓存;在 Interval 到 TTL 之间,未登录用户返回缓存,登录用户可以通过 ?latest 参数强制刷新;超过 TTL 则必定尝试重新抓取,抓取失败时回退到过期缓存。在 Cloudflare Workers 环境下,缓存写入用 waitUntil 异步执行,不阻塞响应返回。
数据源系统(Sources)
这是 NewsNow 最核心的部分。数据源的定义和抓取分三个阶段:
定义阶段:shared/pre-sources.ts 中定义了约 45 个原始数据源,每个包含名称、颜色、所属栏目、抓取间隔等元信息。支持子源(sub),如 cls 下有 cls-telegraph、cls-depth 等子源。
构建阶段:scripts/source.ts 在构建时运行,调用 genSources() 将原始定义扁平化为 shared/sources.json,同时生成拼音索引用于搜索。这是构建流程的一部分,由 npm run presource 触发。
抓取阶段:server/sources/ 目录下每个文件对应一个或一组数据源。通过自定义的 glob: 导入语法(tools/rollup-glob.ts 实现),所有源文件被自动注册到 getters 对象中,无需手动维护索引。
每个源文件遵循统一模式:用 defineSource() 包裹一个异步函数,返回 NewsItem[]。辅助函数 defineRSSSource()、defineRSSHubSource() 分别处理 RSS 和 RSSHub 源。
数据库层(db0)
通过 db0(UnJS 的数据库抽象层)统一 API,根据部署环境切换底层连接器:
| 部署环境 | 连接器 | 说明 |
|---|---|---|
| Node.js / Docker | better-sqlite3 | 本地 SQLite 文件 |
| Cloudflare Pages | cloudflare-d1 | Cloudflare D1 数据库 |
| Bun | bun-sqlite | Bun 内置 SQLite |
| Vercel Edge | 无 | 需自行接入外部数据库 |
两张表:cache(按源 ID 缓存新闻数据)和 user(用户信息和元数据同步)。
构建流程
构建命令是 pnpm run build,展开后是 npm run presource && vite build:
-
presource:
tsx ./scripts/favicon.ts && tsx ./scripts/source.ts- 下载所有源的 favicon 图标到
public/icons/ - 生成
shared/sources.json和shared/pinyin.json
- 下载所有源的 favicon 图标到
-
vite build:按插件链顺序执行
- TanStack Router 生成路由树
- unimport 处理自动导入
- UnoCSS 生成样式
- React SWC 编译 JSX
- PWA 生成 Service Worker
- Nitro 编译服务端代码(根据
CF_PAGES等环境变量选择 preset)
最终产物在 dist/output/public/,包含前端静态资源和 Nitro 编译后的 Worker/Server 脚本。
部署方案
项目通过 nitro.config.ts 中的环境变量判断,适配 5 种部署目标:
Node.js Server(默认):pnpm start 启动,SQLite 数据库,最简单的部署方式。
Cloudflare Pages:CF_PAGES=1 触发,产物自动包含 _worker.js,配合 D1 数据库和 Pages Bindings。部分源(如 bilibili 热门视频)标记为 disable: "cf" 会在 Cloudflare 环境下自动禁用。
Docker:多阶段构建,编译阶段装依赖和构建,运行阶段只拷贝产物,最终镜像非常小。通过 docker-compose.yml 管理环境变量和数据卷持久化。
Vercel Edge 和 Bun:也支持,但数据库需要额外配置。
认证系统
GitHub OAuth 登录流程:用户点击登录 → 跳转 GitHub 授权 → 回调到 /api/oauth/github → 用 code 换 access_token → 获取用户信息 → 存入数据库 → 签发 JWT(60 天有效期)→ 重定向回首页并携带 token。
JWT 用于后续的 /api/me/sync(元数据同步)和 /api/s?latest(强制刷新)。如果环境变量 G_CLIENT_ID 等未配置,登录功能自动禁用,不影响基本浏览。
说点自己的感受
NewsNow 的架构设计有几个让我印象深刻的地方:
shared/ 目录作为前后端桥梁的设计非常巧妙。类型、数据源定义、工具函数、常量全放这里,客户端和服务端都通过自动导入直接使用,没有重复定义,也不需要维护两套类型。新增一个数据源,只需要在 shared/pre-sources.ts 加定义、在 server/sources/ 加抓取逻辑,前端自动就能用。
glob: 自定义导入语法也很聪明。45 个数据源文件,不需要手动维护一个注册表,Rollup 插件自动扫描目录、生成类型声明、建立映射关系。新增源文件只需创建文件,不用改任何配置。
多部署适配的思路也值得学习。不是用条件编译到处 if-else,而是在 Nitro 配置层统一处理——数据库连接器、运行时 preset、环境特定代码都在 nitro.config.ts 里集中管理,业务代码基本不感知部署环境的差异。
评论互动