成为全栈·Next.js 网站前台篇·总复盘:做一个公开内容站的真实成本
页面数量不是公开内容站成本的主要来源。真正昂贵的是公开与私有数据共存、缓存的新鲜度承诺、会话竞态、内容安全,以及每一种“完成”都需要相应证据。
前言
M3 系列从 App Router 写到 Cloudflare,一共 24 篇。回头看,首页、列表、详情、会员中心和投稿编辑器都不是最困难的部分;真正反复消耗时间的,是边界没有天然答案。
哪些内容在服务端,哪些交给浏览器?公开数据可以旧多久?会员状态怎样避免串账号?接口失败时页面该保留什么?本地 Worker 通过以后,我们究竟能声称完成了什么?
这篇不再介绍一个组件,而是复盘这套前台为什么比“做若干页面”昂贵,以及哪些经验可以带到下一个项目。
最初看见的是页面,后来面对的是生命周期
URL 请求 → 路由与 metadata → 服务端数据和公开缓存 → HTML / RSC 交付 → 客户端 hydration 与会话恢复 → 私有互动、错误恢复和重新验证在 CSR 管理后台里,前端主要管理浏览器生命周期;App Router 内容站让前端同时参与服务端请求、缓存和 HTTP 响应。技术栈没有取消后端思维,而是把它带进页面工程。
24 篇文章最终落在六类成本
| 成本 | 代表问题 | 对应篇目 |
|---|---|---|
| 渲染边界 | RSC/Client、SSR/ISR、首屏接续 | M3-01~04、07 |
| 内容结构 | 首页、分类、Markdown、导航辅助 | M3-05~10 |
| 身份安全 | token、BFF、会员数据、互动 | M3-11~14 |
| 内容生产 | 评论审核、投稿状态机 | M3-15~16 |
| 体验质量 | 搜索、通知、配置、响应式、性能 | M3-17~21 |
| 交付证据 | 测试、Worker、R2、上线边界 | M3-22~23 |
页面只是这些决策的表面结果。
复用成功的部分
共享 OpenAPI 让新前台沿用后端契约,类型不必手写。文章卡片、分页、Markdown 渲染、反馈状态和请求错误模型也形成了稳定基础。
exporttypeArticleSummary=components['schemas']['ArticleSummary']exporttypeUser=components['schemas']['User']<ArticleCard article={article} categories={tree} /> <Failure retry={() => query.refetch()} />真正有效的复用建立在语义一致上。收藏、点赞、阅读历史虽然共用卡片,却没有被强行压成同一接口;公开请求和私有请求也没有为了“统一封装”走同一缓存通道。
返工最多的地方都与隐含假设有关
“清空缓存更安全”误伤公开文章流
会话恢复清空整个 QueryClient,导致已挂载的公开 Feed 停止工作。修正是保留public-feed,只移除私有查询。
“有重试按钮”不等于真的恢复
错误边界 reset 没有重新执行失败的服务端请求。改为完整 reload 后,故障注入才证明搜索结果恢复。
“接口测试通过”不等于按钮调用正确
退出端点本身正常,客户端适配器却省略 Authorization。浏览器路径发现问题后,才增加真实 logout 封装测试。
“两套 slug 算法写得一样”仍会分叉
目录与正文分别解析 Markdown,在重复中文和行内格式下失配。最终方案是共享同一语法树,而不是继续同步两份正则。
契约缺口不应该由前端文案掩盖
| 缺口 | 当前诚实方案 | 没有做的伪能力 |
|---|---|---|
| 无单篇收藏状态 | 遍历收藏分页 | 只查前 100 条却声称准确 |
| 点赞列表不分页 | 客户端切片并记录规模边界 | 声称网络已经分页 |
| 无阅读事件时间序列 | 显示累计热门 | 写“近 7 天热门” |
| 无文章版本 | 已发布修改后重新 pending | 声称公开版与编辑版并存 |
| 评论非按楼分页 | 显示“已加载回复” | 声称楼内回复总数完整 |
| 无远程部署 | 记录本地 Worker 证据 | 声称生产已上线 |
产品诚实不是保守措辞,而是让界面、代码和契约说同一件事。
缓存是业务承诺,不是性能开关
// 公开内容fetch(url,{cache:'force-cache',next:{revalidate:60}})// 私有内容fetch(url,{cache:'no-store'})response.headers.set('cache-control','private, no-store')60 秒意味着运营修改可能短暂不可见,过期首访还可能先得到旧值。no-store 则保护会员数据不被公共复用。缓存设计先回答“谁的数据、可以旧多久”,然后才是速度。
安全边界来自多层协作
浏览器:access token 仅内存 Cookie:refresh token HttpOnly + SameSite + Secure BFF:路径限制、Cookie 转换、Origin/Host 校验、no-store 后端:鉴权、角色与资源归属最终裁决任何单层都不能独立宣布安全。客户端守卫可被绕过,Origin 校验不替代鉴权,HttpOnly 也不能消除 XSS 代表用户发请求的风险。
“完成”必须带上证据范围
pnpmgen:apipnpmtypecheckpnpmlintpnpmtestpnpmbuildpnpmbuild:cf这些命令加上双运行时接口联调、页面冒烟、浏览器流程和缓存故障注入,证明了当前代码在本地指定环境下成立。
它们没有证明:真实手机全覆盖、生产性能 SLA、远程 R2 多节点传播、新机器零安装和正式域名 Cookie。边界写进报告,交付才可复核。
如果重新做一次,我会更早确定什么
- 先画公开数据、私有数据和本地状态的所有权表。
- 为每个路由写清新鲜度与错误语义,再选择渲染方式。
- 从第一天给查询 key 加身份与筛选维度。
- 将故障恢复纳入验收,不只做正常截图。
- 对缓存、认证和代理同时测试允许路径与拒绝路径。
- 用真实最长内容、四级分类和两个账号准备测试数据。
- 把本地构建、远程部署和生产指标分成三个里程碑。
哪些判断可以迁移到别的项目
| 判断 | 可迁移原则 |
|---|---|
| Server/Client 边界 | 先把无需浏览器的代码留在服务端 |
| 页面渲染策略 | 按数据所有权和新鲜度逐页决定 |
| 错误处理 | 核心失败与辅助降级分开 |
| 状态缓存 | query key 必须表达身份与筛选 |
| 树形数据 | UI 展开属前端,数据归属查询属后端 |
| 验证 | 每项证据只支持有限结论 |
下一阶段仍需面对的生产工作
正式 API 与内容 → 生产域名和 NEXT_PUBLIC_SITE_URL → 独立 Worker 与 R2 → Cookie、Origin 和缓存验收 → 发布与回退方案 → 真实用户性能与错误监测这不是 M3 实现未完成,而是线上发布本来就有环境前提。把两者分开,既不贬低已经完成的工程,也不提前透支生产结论。
小结
公开内容站的真实成本,不在页面数量,而在它同时面对搜索引擎、游客、会员、作者、缓存和两个运行时。每一类用户和环境都带来新的状态与证据要求。
这 24 篇最终形成的不是一套 Next.js API 清单,而是一种全栈判断方式:先确定数据属于谁、何时生成、可以旧多久、失败后保留什么,再写组件和请求。框架会继续变化,这些问题不会。
延伸阅读
- App Router 与 CSR 时代的思维差异
- 质量门禁
- OpenNext 部署 Cloudflare
如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏「成为全栈」:
🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html
📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer