Chapter 07
自测题库与判别场景
前六章建立了从路由到变更的完整模型。这一章不教任何新东西,只逼你把它取出来用。最后一层"判别题"是这份概念教程的压轴——它给真实场景,要求在前几章的方案之间做选择,那才是检验是否真学会的地方。
所有答案集中在页面最底部一个折叠块里。每道题先把答案写在纸上或编辑器里,整组做完再展开对照。中途瞄一眼答案,这一章就退化成了"再读一遍"——而再读一遍对记忆几乎没有增益。卡住是正常的,卡住的地方就是你该回去重看的章节。
题目分三层,难度递增。概念层和原理层各对应前面的章节;应用判别层把多个章节的概念混在一个真实场景里,要求你做选择并说明理由——这一层最接近真实工作,也最值得花时间。
A概念层 · 直接回忆
这一层考"是什么"。答不出说明对应章节的词汇表还没建立,回去重看。
- App Router 里一个组件默认在哪台机器上运行?要让它能用
useState和onClick,需要做什么?[02] app/(marketing)/about/page.tsx对应的 URL 是什么?为什么(marketing)那层不出现在 URL 里?[01]layout.tsx与template.tsx在页面间导航时的行为差别是什么?[01]- 哪些值能作为 prop 从 Server Component 跨界传给 Client Component?举一个不能传的例子。[02]
loading.tsx这个文件约定,底层等价于把page包进了什么 React 机制?[01·03]
B原理层 · 理解机制
这一层考"为什么/怎么做到的"。答案要能讲到机制,而不只是复述结论。
- 调用
cookies()为什么会把一个路由从静态渲染变成动态渲染?这个影响会"传染"给它的子树吗?[03] <Suspense>为什么能让慢的一块不拖慢整页首屏?它靠什么底层传输机制把慢内容补上?[03]- Server Component 渲染后,发给浏览器的 RSC payload 里一个 Client Component 以什么形式出现?为什么发的不是渲染好的 HTML?[02]
- Next 16 里一段不加任何指令的
await fetch(url),结果会被跨请求缓存吗?把 v14 / v15 / Next 16 三代的默认行为各说一句。[05] revalidateTag('product-42')为什么能精确失效"和这个商品相关"的缓存,而不动其他缓存?[05]- Server Action 是 02 章"函数不能跨边界"那条规则的例外。
'use server'把这个函数编译成了什么?为什么每个 action 在安全上都要当成公开端点处理?[06·02]
C应用判别层 · 跨章场景
这一层给真实场景,要求你在前几章的方案之间做选择并讲清依据。每题都横跨至少两章——这是这份概念教程的压轴,也是面试最爱问的形态。
- 一个营销首页:全站统一、内容几乎不变,唯独页脚要显示"当前在线人数"(每秒都在变)。你会让整页走静态渲染还是动态渲染?"在线人数"这一小块怎么处理,才能不牺牲首页其余部分的静态速度?[03 静态/动态/Suspense · 02 边界]
- 一段内容需要读数据库才能显示,但它必须放进一个用
useState控制展开/收起的可折叠面板里。方案 A:把面板做成 Client Component,在它内部 fetch 数据。方案 B:面板做成 Client Component 只管交互,数据内容由服务端父组件渲染好作为children传进去。选哪个?另一个为什么不行?[02 组合模式] - 商品价格更新了。你只想让"用到这个商品的页面"重新渲染,别的页面缓存照旧。该用
revalidatePath还是revalidateTag?为什么它能做到"精确"而不是"全清"?[05 失效] - 两个写入需求:(a) 接收第三方支付平台发来的"支付成功" webhook;(b) 用户在你的页面表单里点"提交订单"。这两个分别该用 Route Handler 还是 Server Action?给出判别依据。[06 判别]
- 一个仪表盘页:顶部用户信息(查询约 50ms)、中间一张报表(聚合查询约 3s)。要求用户 50ms 内就看到顶部、报表用骨架占位随后补上,且"当前用户"在整页只查一次数据库。把哪些机制组合起来?[04 并行+记忆化 · 03 Suspense]
合上教程,在纸上或 Excalidraw 里画出 App Router 处理一次动态页面请求的旅程——只画 5–6 个框:浏览器请求 → 服务端渲染 Server Components → RSC payload 流式传输 → 浏览器 reconcile + 水合 → Client Component 获得交互。
画完回到 02 章图 2.2 和 03 章的流式图对照,回答两个问题:你画的图里,跨网络流动的那段是 HTML 还是 RSC payload?05 章的四层缓存,分别落在你这条链路的哪几个位置?
§答案(三层都做完再展开)
答案在下面这一个折叠块里。确认三层都写完了,再点开。
展开全部答案
概念层
- 默认在服务器上运行(Server Component),代码不进浏览器 bundle。要用
useState/onClick,需在组件文件顶部加'use client',把它升级为 Client Component。 - URL 是
/about。带圆括号的(marketing)是路由组,只用于组织代码、共享 layout,不产生 URL 段。 layout在同一 layout 下的页面间导航时不卸载、不重渲染,能保留 UI 状态(滚动位置、展开态);template每次导航都重新挂载一个新实例,state 重置。- 能传:字符串、数字、布尔、null、普通对象/数组、Promise、已渲染的 JSX(children)。不能传:函数 / 事件处理器(无法序列化过网络)、自定义类实例、Symbol。一个不能传的例子:
onSave={() => save()}这样的回调。 - 等价于 Next 自动用一层
<Suspense>把page包起来,loading.tsx的内容就是那个fallback。
原理层
cookies()读取的是"请求时才存在"的信息,构建时无从得知,所以渲染器只能把这棵子树推迟到请求时渲染——即动态渲染。它会传染:在某个层级读了请求数据,从该点往下都转为动态。在 root layout 顶层读 cookies 会把整站拖成动态。- 服务端渲染遇到
<Suspense>边界先输出fallback占位、不阻塞其余内容;被包住的子树在服务端 ready 后,其结果作为后续数据块通过 chunked transfer(分块传输)乱序发出,浏览器收到后替换进对应占位。于是慢的一块不再卡住快的整页外壳。 - 以模块引用的形式出现("去某个 chunk 加载名为 X 的组件,props 是 …"),不是渲染结果。发 RSC payload(UI 描述)而非 HTML,是为了让客户端导航时能把新内容 reconcile 进当前还活着的组件树、保留已有状态,而不是整页替换——HTML 不携带"这是可被 reconcile 的 React 树"这层信息。
- 不会被缓存。Next 16 默认不缓存,要显式写
'use cache'才进 Data Cache。三代默认:v14 默认缓存fetch;v15 默认不缓存(no-store);Next 16 改为'use cache'显式开启(Cache Components)。 - 因为
cacheTag()在数据加载后给缓存条目打了标签(标签可由数据派生,如product-42)。失效时按"tag → 条目"索引只作废带该 tag 的条目,并级联到依赖这些数据的路由,其余缓存不受影响。 - 编译成一个加密的 POST 端点,按每次构建生成的、不可猜的 action ID(源码位置 hash)路由;客户端拿到的是指向该端点的引用,不是函数本体。要当公开端点处理,是因为这个端点任何人都能打,且 TypeScript 类型在运行时被擦除——必须运行时校验输入、并校验"这个用户有没有权限动这条数据"(授权,不只是认证)。
应用判别层
- 整页尽量静态(首页其余部分构建时渲染、可被 CDN 复用)。把"在线人数"隔进一个独立的动态子组件,用
<Suspense>包住——它读请求时数据、走动态渲染并流式补上,而页面其余静态部分不受影响。这正是 PPR / Cache Components 的"静态外壳 + 动态洞"形态。 - 选 方案 B(组合模式)。方案 A 不行:把数据 fetch 放进 Client Component,等于在浏览器侧取数,既失去服务端直连数据库的优势,客户端也常常根本无法访问数据库 / 密钥。方案 B 让面板只做交互、留一个
children洞,由服务端父组件渲染好数据内容传进去——服务端内容留在服务端,只把结果跨界。 - 用
revalidateTag。给该商品的数据缓存打上product-<id>这样的数据派生 tag,价格变更后按 tag 失效,只命中带该 tag 的条目并级联到用到它的路由。revalidatePath是按具体路径失效,难以覆盖"所有用到该商品的页面"且容易误伤。 - (a) webhook 用 Route Handler:调用方是第三方机器、走标准 HTTP、需要你掌控请求/响应与验签。(b) 表单提交用 Server Action:触发者是你自己 UI 里的用户,省去手写端点与 fetch,内置 CSRF 防护与渐进增强。判别依据:人从你的 UI 触发 → Action;机器经 HTTP 调用 → Handler。
- 把"当前用户"查询用
React.cache()包裹(同一次渲染内只查一次,即使多处调用)。顶部信息直接await、快速渲染;把 3s 的报表隔进一个 async 子组件并用<Suspense fallback={骨架}>包住,让顶部 50ms 先出、报表 ready 后流式补上。组合的是 04 章的并行 + 请求记忆化和 03 章的流式渲染。
给一个需求,自己走一遍全链路决策
需求:一个博客平台的"文章详情页"。文章正文一天改几次;右栏作者卡片几乎不变;底部评论区要能实时提交新评论并立即看到。请独立写出这套设计:哪些组件是 Server / Client(02),路由怎么组织(01),正文/作者卡/评论各走什么渲染与缓存策略(03·05),评论提交用什么写入机制、写完怎么失效(06·05)。写完对照前六章,检查每个决策你都能说出"为什么不是另一个选择"。
提示(卡住再展开)
正文:'use cache' + 较短 cacheLife 或按 tag 失效。作者卡:长缓存 / 静态。评论列表:可作为动态子树用 Suspense 包;提交用 Server Action 写库后 revalidateTag('comments-<postId>'),配合客户端乐观更新(useOptimistic)即时反馈。整页适合 PPR:静态外壳(正文+作者卡)+ 动态洞(评论)。