上一篇完成了纯组件与本地交互,本篇把任务看板迁入 Next.js App Router。我们会把页面、布局、动态段与加载边界落到目录结构中,并依据数据时效、个性化程度和交互需求选择渲染位置。
一、痛点:路由不只是页面跳转
单页应用若把筛选条件只放 state,刷新即丢失,也无法复制链接给同事。路由是应用的公开状态协议:/projects/42/tasks?status=open同时表达资源与视图。Next.js 使用文件系统路由,app/projects/[id]/page.tsx对应动态项目页,父级layout.tsx保存共享导航,loading.tsx定义流式加载占位,error.tsx隔离段级错误。
服务端组件是 App Router 的默认值:可直接读取后端数据,不把数据库 SDK 与秘密发给浏览器,也不增加客户端 JavaScript。只有事件处理、状态、Effect 或浏览器 API 才需要在边界文件顶部写'use client'。这个标记会把该文件及其客户端依赖纳入浏览器图,因此边界应尽量靠近真正交互的叶子。
可以先画一张“路由—数据—身份”表:每个 URL 对应什么资源,数据由谁读取,是否依赖当前用户,允许缓存多久,错误落在哪个边界。表格迫使团队在写组件前回答安全与时效问题。例如项目导航可共享布局,任务列表却必须按项目和身份查询;筛选条需要浏览器事件,但筛选结果仍可由服务端输出。迁移到别的全栈框架时,这张表仍然有效。
二、原理:按数据生命周期选渲染
静态内容可在构建或后台再验证时预渲染,适合帮助页和变化不频繁的项目简介;依赖 cookie、header 或每次请求权限的数据应请求时渲染;点击后的临时 UI 在客户端处理。不要用“SSR 比 CSR 更快”这种口号决策,首字节、HTML 可见时间、可交互时间与服务器成本是不同指标。
渲染选择还应区分“数据获取位置”和“交互执行位置”。服务端先输出列表,不妨碍客户端接管筛选按钮;客户端组件也可以接收服务端已读取的可序列化数据。真正需要避免的是边界反复横跳:服务端已经取得同一数据,客户端挂载后又无条件请求一次,会浪费流量并产生闪烁。后续缓存篇会用脱水或统一查询层解决这个衔接。
下面以规则引擎把需求映射为策略。它刻意先判断私有请求,再判断时效,避免把用户专属数据误缓存成共享页面。
fromdataclassesimportdataclass@dataclass(frozen=True)classPageNeed:name:strpersonalized:boolfreshness_seconds:intbrowser_only:bool=Falsedefchoose_rendering(need):ifneed.browser_only:return"client component"ifneed.personalized:return"request-time server"ifneed.freshness_seconds==0:return"request-time server"ifneed.freshness_seconds<=300:returnf"cached server ({need.freshness_seconds}s)"return"prerendered"pages=[PageNeed("帮助中心",False,86400),PageNeed("公开项目",False,60),PageNeed("我的任务",True,0),PageNeed("拖拽面板",False,0,True),]forpageinpages:print(f"{page.name}:{choose_rendering(page)}")运行输出:
帮助中心: prerendered 公开项目: cached server (60s) 我的任务: request-time server 拖拽面板: client component三、实现:让 URL 成为筛选契约
页面服务端读取params获取项目 ID,读取searchParams获取status,先做白名单归一化,再查询数据。筛选控件是小型客户端组件,用URLSearchParams保留其他参数并调用路由替换。这样刷新、后退、分享与服务端首屏都得到同一筛选结果。内部导航用Link,获得预取与客户端转换;真正的外站链接仍用普通锚点。
动态路径参数永远是不可信输入。项目 ID 先验证格式,再检查当前用户权限;不存在或不可见时返回一致的 not-found 体验,避免通过错误差异枚举私有项目。generateMetadata也必须使用经过授权的数据。布局适合外壳,不要依赖它在每次导航重跑来刷新业务数据。
URL 参数需要稳定规范。缺省值和显式status=all最好归一到一种形式,未知参数要决定保留还是丢弃,多值参数要定义顺序,否则缓存和统计会把同一视图当成多个页面。替换筛选时保留owner等正交维度,改变项目时则清理不再适用的页码。把解析函数放在路由边界并测试,页面内部只消费已经验证的领域值。
加载和错误边界也属于产品逻辑。骨架只覆盖等待中的段,父导航仍可操作;错误界面提供重试与请求编号,却不能向浏览器暴露数据库异常。若一个次要统计模块失败,不应让整个项目页白屏,可以给它单独的 Suspense 与错误边界。边界粒度应对应可独立恢复的用户任务,而不是机械地每个组件包一层。
fromurllib.parseimportparse_qs,urlencode,urlsplit,urlunsplit ALLOWED={"all","open","done"}defnormalize_status(query):raw=parse_qs(query).get("status",["all"])[0]returnrawifrawinALLOWEDelse"all"defreplace_status(url,status):parts=urlsplit(url)params=parse_qs(parts.query)params["status"]=[statusifstatusinALLOWEDelse"all"]flattened=[]forkeyinsorted(params):forvalueinparams[key]:flattened.append((key,value))query=urlencode(flattened)returnurlunsplit((parts.scheme,parts.netloc,parts.path,query,""))current="/projects/42/tasks?owner=me&status=open"next_url=replace_status(current,"done")print("status=",normalize_status(urlsplit(next_url).query))print("url=",next_url)print("invalid=",normalize_status("status=deleted"))运行输出:
status= done url= /projects/42/tasks?owner=me&status=done invalid= all四、踩坑:缓存与动态性必须显式
最危险的错误是缓存了含用户身份的响应。缓存键必须包含所有影响结果的公开维度,而私有数据优先不进入共享缓存。反过来,每页都强制动态也会放弃 CDN、预渲染和稳定吞吐。先按页面的数据矩阵决定,再用实际指标验证。
不要把整个根布局标成客户端组件来使用一个按钮,这会扩大下载与水合范围。服务端组件可以把可序列化 props 传给客户端组件,但函数、数据库连接等不能跨边界。加载界面应与最终布局尺寸接近,避免跳动;错误边界提供重试,但日志仍要保留请求标识和服务端堆栈。
预取同样需要预算。Link的预取能让常用导航更快,但大量低概率链接可能争抢首屏带宽;长列表可以只保留高意图入口。缓存公开页面前,检查键是否包含语言、租户和其他影响内容的维度。任何依赖授权的响应都不应因忘记某个维度而进入共享缓存,这类错误比少一次缓存命中严重得多。
五、验证:从 URL 到渲染结果
列出路由表,覆盖合法动态段、缺失资源、无权限、非法查询、空结果和慢请求。直接访问深层 URL,确认不依赖从首页点击;刷新筛选页,确认状态保留;禁用 JavaScript检查服务端首屏核心内容。构建日志与响应头用于确认预渲染/请求时策略,不凭本地开发模式推断生产行为。
再用两个账号交叉请求同一路径,确认响应、缓存和元数据没有串租户;复制规范化后的 URL 到无痕窗口,确认公开状态可复现而私有状态重新鉴权。记录首字节、流式内容出现时间与水合脚本体积,才能知道渲染策略是否真正改善体验。若换成 Remix、Nuxt 或服务端模板,这套验证仍可直接迁移。
本篇让项目页具备可寻址、可分享和分层渲染能力。下一篇会处理跨页面的筛选、当前项目与草稿:区分服务器状态、URL 状态、局部 UI 状态,并用决策矩阵选择 Context、reducer 或轻量状态库。
参考来源
- Next.js:App Router
- Next.js:Layouts and Pages
- Next.js:Server and Client Components
👍 觉得有用就点个赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。
🚀 本文属于《现代前端框架实战》系列,持续更新,关注不迷路。
📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。