NewsNow 项目技术架构文档

发布于 2026年05月27日 11:00 #架构#Github

NewsNow 项目技术架构文档 封面图

快速预览:本文从宏观视角拆解 NewsNow 项目的技术架构——一个基于 Vite + Nitro 的全栈应用,前端 React SPA + TanStack Router,后端 H3 API + 多数据库适配,支持 45+ 新闻源的聚合抓取,可部署到 Cloudflare Pages、Node.js、Vercel、Docker 等多种环境。

关键词NewsNow架构ReactViteNitroTanStack RouterJotaiUnoCSSDockerCloudflare Pages

源码地址https://github.com/ourongxing/newsnow

核心观点

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,存储用户的所有栏目配置和关注源。衍生出 focusSourcesAtomcurrentColumnIDAtomcurrentSourcesAtom 等派生原子。

数据获取: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/hookssharedsrc/utilssrc/atoms 四个目录,所有导出自动成为全局可用。React 的 useStateuseEffect,Jotai 的 useAtomuseAtomValue,以及 clsx(别名 $)都不需要手动 import。

$() 就是 clsx() 的短名,贯穿所有组件:$("flex items-center", condition && "text-red")

后端架构

API 路由(H3 + Nitro)

路由方法功能
/api/s?id={id}GET获取单个新闻源数据(带缓存)
/api/s/entirePOST批量获取多个新闻源的缓存数据
/api/loginGET跳转 GitHub OAuth 授权页
/api/oauth/githubGETOAuth 回调,签发 JWT
/api/me/syncGET/POST用户元数据同步(登录后)
/api/enable-loginGET检查登录功能是否启用
/api/latestGET返回当前版本号
/api/mcpPOSTMCP 协议端点(给 AI 助手用)

缓存策略

数据缓存有两层时间控制:TTL(30 分钟)和 Interval(10 分钟,每个源可自定义)。

Interval 内,永远返回缓存;在 IntervalTTL 之间,未登录用户返回缓存,登录用户可以通过 ?latest 参数强制刷新;超过 TTL 则必定尝试重新抓取,抓取失败时回退到过期缓存。在 Cloudflare Workers 环境下,缓存写入用 waitUntil 异步执行,不阻塞响应返回。

数据源系统(Sources)

这是 NewsNow 最核心的部分。数据源的定义和抓取分三个阶段:

定义阶段shared/pre-sources.ts 中定义了约 45 个原始数据源,每个包含名称、颜色、所属栏目、抓取间隔等元信息。支持子源(sub),如 cls 下有 cls-telegraphcls-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 / Dockerbetter-sqlite3本地 SQLite 文件
Cloudflare Pagescloudflare-d1Cloudflare D1 数据库
Bunbun-sqliteBun 内置 SQLite
Vercel Edge需自行接入外部数据库

两张表:cache(按源 ID 缓存新闻数据)和 user(用户信息和元数据同步)。

构建流程

构建命令是 pnpm run build,展开后是 npm run presource && vite build

  1. presourcetsx ./scripts/favicon.ts && tsx ./scripts/source.ts

    • 下载所有源的 favicon 图标到 public/icons/
    • 生成 shared/sources.jsonshared/pinyin.json
  2. 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 PagesCF_PAGES=1 触发,产物自动包含 _worker.js,配合 D1 数据库和 Pages Bindings。部分源(如 bilibili 热门视频)标记为 disable: "cf" 会在 Cloudflare 环境下自动禁用。

Docker:多阶段构建,编译阶段装依赖和构建,运行阶段只拷贝产物,最终镜像非常小。通过 docker-compose.yml 管理环境变量和数据卷持久化。

Vercel EdgeBun:也支持,但数据库需要额外配置。

认证系统

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 里集中管理,业务代码基本不感知部署环境的差异。

评论互动

© 2026 王若风的技术博客 · Powered by Astro