跳到主要内容

AI 应用前端选型核对:React 19 的 Server Components、shadcn/ui、React Router 的嵌套路由

· 阅读需 18 分钟

一次选型讨论里落下来三条论断:

  1. React 19 的 Server Components 对 AI 应用有特殊意义——可以在服务端调用 AI 接口,减少客户端暴露
  2. shadcn/ui 风格现代、可定制性强,适合快速搭建 AI 产品界面
  3. React Router 的嵌套路由适合复杂的管理后台场景

三条听起来都对。但「听起来对」和「能拿出依据」之间差着一层:官方文档究竟怎么写的?哪部分是官方原话,哪部分是我自己的推论,哪部分是实测出来的?

这篇文章把三条论断逐条拆开核对。核对办法是三件事:curl 抓一手原文逐字比对、本机跑脚本实测、把推论明确标成推论。

先说结论​

论断核对结果依据类型
RSC 能减少客户端暴露机制成立,但官方文档里从没说过「避免 API key 暴露」——原文只给了「不下发浏览器」和「可直接访问数据层」两句推论(基于逐字原文)
shadcn/ui 可定制性强成立,官方自我定义就是「这不是组件库」官方逐字
shadcn/ui 风格现代主观判断,无法从文档核实;但可以从 registry 产物看到它今天真实长什么样实测
shadcn/ui 适合 AI 产品界面官方自己在往这个方向讲(文档站已开 AI SDK 章节)官方逐字
React Router 嵌套路由适合后台匹配机制成立且干净官方逐字 + 实测
React Router 7版本要更正:当天 npm latest 已经是 8.4.0,v7 线停在 7.18.4;v8.0.0 发布于 2026-06-17实测

下面逐条展开,附可复现的脚本与真实输出。

一、RSC:官方原文其实只说了两件事​

1.1 官方逐字(react.dev,server-components.md,2026-09-23 抓取)​

它是什么:

Server Components are a new type of Component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server.

有服务端时它能干什么——这是本文最关键的一句:

Server Components can also run on a web server during a request for a page, letting you access your data layer without having to build an API. They are rendered before your application is bundled, and can pass data and JSX as props to Client Components.

客户端到底能拿到什么:

When the page loads, the browser does not see the original Note and Author components; only the rendered output is sent to the client

代价(这是「不下发」的另一面):

Server Components are not sent to the browser, so they cannot use interactive APIs like useState.

1.2 「减少客户端暴露」是我推出来的,不是官方说的​

这点必须交代清楚:我把 react.dev 的 server-components.md、server-functions.md、use-client.md 三份原文都抓下来 grep -i 过一遍,API key、secret、token、env 这几个词的命中数都是 0。官方没有用「避免密钥泄露」这类措辞来推销 RSC。

所以推论链是这样的(以下是推论,不是转述官方立场):

  1. 官方:Server Component 的代码不会被发送到浏览器,浏览器只收到渲染结果
  2. 推论:这段代码里 process.env.LLM_API_KEY 的取值过程,也就不存在于客户端 bundle 里
  3. 推论:因此把「调用模型」这件事放在 Server Component / Server Function 里,key 天然只在服务端

第 1 步是官方原话,第 2、3 步是想当然的延伸——但延伸得可靠,因为它依赖的机制(代码不下发)是官方明确写死的行为。真正不成立的情况只有一种:你自己把 key 通过 props 传给了 Client Component,那不叫 RSC 的锅。

1.3 落到 AI 应用上,实际省掉的是什么​

「能减少客户端暴露」只是副产品。按官方原文推导,RSC 对 AI 应用真正的三个收益是:

① 省掉一层自己搭的转发服务。 官方原话就是 without having to build an API——以前「前端 -> 自建 BFF -> 模型 API」那层 BFF,如果只是转发和拼 prompt,现在可以直接在 Server Component 里写。

② 首屏直出,慢接口不阻塞。 Server Component 支持 async,可以 await 之后再渲染(官方文档把这叫 async components)。更关键的是**「在服务端创建 Promise、在客户端 await」**这个模式——官方给了逐字示例:笔记内容在服务端 await,评论的 Promise 不 await 直接传给客户端,客户端用 use() 挂起等待。模型的流式回答正好是这个形状:正文先出,回答边出边填。

③ 客户端 bundle 里没有那些只在服务端用的重依赖。 官方文档举的例子是 markdown 渲染那两个库(marked 35.9K / sanitize-html 206K,gzip 后 75K),注释写着 // Not included in bundle。AI 应用里的 SDK、tokenizer、向量库客户端,是同一类东西。

1.4 一个最小形状:服务端调模型,客户端管交互​

边界只有一条:用 "use client" 划开。Server Component 是默认的,不需要写任何指令。

// app/routes/chat.tsx —— Server Component(默认形态,不写指令)
import db from "./db.server";

export default async function ChatRoute() {
// 直接读库 / 调模型,不用先写一个 REST 接口
const history = await db.messages.recent(20);

return <Chat history={history} />; // Chat 是 Client Component
}
// app/chat.tsx —— 客户端边界:只有交互在这里
"use client";

import { useState } from "react";
import { ask } from "./actions";

export default function Chat({ history }: { history: Message[] }) {
const [text, setText] = useState("");

return (
<form action={async () => { await ask(text); setText(""); }}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">发送</button>
</form>
);
}
// app/actions.ts —— Server Function:key 只在这一侧
"use server";

export async function ask(text: string) {
const res = await fetch(process.env.LLM_ENDPOINT!, {
method: "POST",
headers: { authorization: `Bearer ${process.env.LLM_API_KEY}` }, // 客户端看不到这行
body: JSON.stringify({ prompt: text }),
});
return res.json();
}

三段代码对应三种角色:Server Component 负责取数、Client Component 负责状态、Server Function 负责让客户端能「隔着边界」喊一声服务端。官方对 Server Function 的定义逐字是:

Server Functions allow Client Components to call async functions executed on the server.

1.5 两个容易记错的点​

其一,写 "use server" 不等于用了 RSC。 官方专门开了个 Note 纠正这个误解:

There is no directive for Server Components.

A common misunderstanding is that Server Components are denoted by "use server", but there is no directive for Server Components. The "use server" directive is used for Server Functions.

Server Component 是没有指令的(默认形态);"use server" 标记的是函数。

其二,术语变过。 官方原文承认:

Until September 2024, we referred to all Server Functions as "Server Actions".

也就是说「Server Action」是「Server Function」的一个子集(传进 action prop 或从 action 里调用时)。看到老文章里的 Server Actions,指的就是这批东西。

1.6 版本事实:RSC 稳定,但「实现 RSC 的那层」不跟 semver​

这段值得单独记住,因为它直接决定你敢不敢上生产。当天 npm 上 react 是 19.3.0。官方逐字:

While React Server Components in React 19 are stable and will not break between minor versions, the underlying APIs used to implement a React Server Components bundler or framework do not follow semver and may break between minors in React 19.x.

拆开读:组件模型稳定,但打包器/框架用来实现它的那层 API 不遵守 semver。官方给的建议是钉死 React 版本或用 Canary。

实践含义:你说「我用 RSC」,其实是「我用某个框架提供的 RSC」。React 本身只给你组件模型,能不能跑起来取决于 Next.js / React Router / Waku 这些实现方跟得怎么样。选型时该评估的是框架,不是 React 版本号。

二、shadcn/ui:官方原话是「这不是组件库」​

2.1 官方逐字(ui.shadcn.com/docs,2026-09-23 抓取)​

shadcn/ui is a set of beautifully-designed, accessible components and a code distribution platform. Works with your favorite frameworks and AI models. Open Source. Open Code. This is not a component library. It is how you build your component library.

它自己列的三条特性:

Open Code: The top layer of your component code is open for modification.

AI-Ready: Open code for LLMs to read, understand, and improve.

这三句直接呼应了论断 2 里的「可定制性强」——不是「提供了很多主题配置」那种可定制,是「代码拷进你仓库归你」那种可定制。这是两码事,选型时的差别很大:前者升级靠配置,后者升级靠 diff。

2.2 「风格现代」说不清,那就看它今天真实长什么样​

主观判断没法从文档核实。但 shadcn 是「源码分发」,registry 返回的就是要落进你仓库的 .tsx——直接看产物比看宣传页可靠。

我写了第三个探针,扫了 9 个 AI 产品界面里的常客(脚本与输出见 source/rsc-ai-lab/):

组件 use client forwardRef hooks window/document 行数 依赖
button 无 仍在用 无 无 58 @radix-ui/react-slot
badge 无 无 无 无 37 @radix-ui/react-slot
card 无 仍在用 无 无 77 —
input 无 仍在用 无 无 23 —
dialog 有 仍在用 无 无 123 @radix-ui/react-dialog
tabs 有 仍在用 无 无 56 @radix-ui/react-tabs
dropdown-menu 有 仍在用 无 无 202 @radix-ui/react-dropdown-menu
switch 有 仍在用 无 无 30 @radix-ui/react-switch
tooltip 有 仍在用 无 无 33 @radix-ui/react-tooltip

这张表读出三件事:

① 展示型组件可以直接被 Server Component 引用。 button / badge / card / input 既没有 "use client",也没有 hooks、没有 window/document——它们渲染在服务端不会炸。也就是说 <Card> 包着 <Badge> 这种纯展示结构,可以整个留在服务端,不进客户端 bundle。

② 交互型组件的 "use client" 已经替你写好了。 有意思的是这五个组件自己的源码里一个 hook 都没有——状态逻辑全在 Radix 里。shadcn 给你加那行指令,等于替你把客户端边界声明完了,你写业务代码时不用再判断「这里要不要 use client」。

③ forwardRef 还在大面积使用。 9 个里 8 个的源码里仍有 forwardRef。这跟 React 19 把 forwardRef 标为 deprecated 但保持兼容可用是一致的——本仓库另一篇《React 19 弃用 forwardRef》讲的就是这件事。官方 deprecate ≠ 生态当天就改,这份 registry 是很好的佐证。

实测边界说清楚:以上判定是源码文本层面的扫描(grep "use client" / forwardRef / hook 名 / window、document),不是真的跑了一次 RSC 构建。所以它能回答「这份源码里有没有这些东西」,不能替代「把它放进真实 RSC 项目里编译一遍」。另外 forwardRef 的判定是字符串匹配,不排除出现在注释里的可能。要更硬的结论,得搭个 RSC 模板真编一次——这次没做。

2.3 「适合 AI 产品界面」这句,官方自己在这么说​

论断 2 的后半句不用我论证——shadcn 官方文档自己写着「Works with your favorite frameworks and AI models」,还有:

The design of shadcn/ui makes it easy for AI tools to work with your code. Its open code and consistent API allow AI models to read, understand, and even generate new components.

逻辑很顺:源码可读 + API 一致 ⇒ 模型能照着已有组件生成新组件。这也是「Open Code」在 AI 时代比「可配置主题」更值钱的原因。

顺带记一笔:当天抓下来的文档站导航里已经出现了 AI SDK 与 TanStack AI 两个章节。这条只是导航层面的观察,具体内容没细看。

2.4 一个组合时的实操注意​

因为「Server Components 不下发到浏览器」(1.1 节逐字),所以把无 "use client" 的展示型组件放进 Server Component 时,一旦你要给它加 onClick 之类的交互,那段就得单独拆成一个客户端组件。混用不是问题,问题是别指望在服务端组件上挂事件处理器。

三、React Router:嵌套路由没问题,但「7」得改成「8」​

3.1 版本先更正​

论断 3 说的是 React Router 7。查了一下当天 npm:

npm view react-router version → 8.4.0 (latest)
npm view react-router dist-tags → version-7: 7.18.4

v8 早就是大版本了,发布于 2026-06-17。 官方在 changelog 里把它定位成「年度大版本节奏的第一次发布」,并说明了基线要求:

  • Node 22.22.0+(对齐 Node 20 EOL)
  • React 19.2.7+
  • Vite 7+
  • 纯 ESM 发布

注意那个 React 19.2.7+ 的基线:它正好接上了 1.6 节——「用 RSC」这件事确实是被框架绑着走的。

好消息是 v8 的迁移成本被官方刻意压低了(future.v8_* 标志在 v7 里已经消化过),v7 线也仍在维护(7.18.4)。所以:「嵌套路由」这个论断在 v7 和 v8 上都成立,只是别再把 7 当成当前版本。

3.2 官方逐字(docs/start/framework/routing.md,2026-09-23 抓取)​

Nested Routes​

Routes can be nested inside parent routes.

父路径怎么来的:

The path of the parent is automatically included in the child, so this config creates both "/dashboard" and "/dashboard/settings" URLs.

Child routes are rendered through the <Outlet/> in the parent route.

还有一句容易被忽略:

Every route in routes.ts is nested inside the special app/root.tsx module.

另外官方列举了路由模块自带的能力(route-module.md 逐字):

Route modules are the foundation of React Router's framework features, they define:

  • automatic code-splitting
  • data loading
  • actions
  • revalidation
  • error boundaries
  • and more

3.3 实测:匹配链长什么样​

文档说「父路径自动拼接」,那就跑一遍看返回值。第一个探针用 react-router 的 matchRoutes 对一份后台路由配置做匹配(不需要浏览器、不需要打包器),真实输出:

URL 命中链(父 → 子,从上到下渲染)
------------------------------------------------------------------------------
/ root → root.home
/dashboard root → dashboard → dashboard.index
· root 匹配到 / params={}
· dashboard 匹配到 /dashboard params={}
· dashboard.index 匹配到 /dashboard/ params={}
/dashboard/settings root → dashboard → dashboard.settings
/dashboard/users/42 root → dashboard → dashboard.user
· dashboard.user 匹配到 /dashboard/users/42 params={"id":"42"}
/dashboard/settings/profile root → root.notfound

对照表里的「父路径自动拼接」:配置里父路由只写了 path: "dashboard"、子路由只写了 path: "settings",而匹配结果里 dashboard.settings 的 pathname 是完整的 /dashboard/settings。拼接是在匹配阶段完成的,你不用手写全路径。

再看换页时的复用情况,同一份输出里有两行对比:

/dashboard/settings → root → dashboard → dashboard.settings
/dashboard/users/42 → root → dashboard → dashboard.user
共同前缀: root → dashboard (前缀不变 ⇒ 这层布局不会因换页重新挂载)

这就是「适合管理后台」的机制所在(这一步是从匹配结果出发的判断):侧边栏和顶栏长在 dashboard 这一层,内容区通过 Outlet 换。两个子页面之间跳转时,命中链的共同前缀 root → dashboard 不变,变的只有叶子——所以侧边栏的展开状态、滚动位置不用你手动保。

再叠上 3.2 里官方列的「每个路由模块自带 data loading / error boundaries」,就能得到后台的典型收益:某一层炸了只炸那层,而不是整页白屏;数据在哪一层取,也是每层自己声明。

3.4 但「React Router + RSC」目前还是实验性的​

这条得单独说,因为很容易被「RSC + 路由库」的组合冲昏头。React Router 的文档里确实有 RSC 支持,但文档 frontmatter 第一行就写着 unstable: true,正文警告逐字:

React Server Components support is experimental and subject to breaking changes in minor/patch releases. Please use with caution and pay very close attention to release notes for relevant changes.

启用方式也和常规框架模式不同——用的是另一个 Vite 插件:

RSC Framework Mode uses a different Vite plugin than non-RSC Framework Mode, currently exported as unstable_reactRouterRSC.

第二个探针在装好的 8.4.0 上把导出名打出来核对,是真的:

@react-router/dev/vite —— 共 2 个导出,其中含 "RSC" 的 1 个:
unstable_reactRouterRSC

react-router —— 共 131 个导出,其中含 "RSC" 的 4 个:
UNSAFE_FetchersContext
UNSAFE_RSCDefaultRootErrorBoundary
unstable_RSCStaticRouter
unstable_routeRSCServerRequest

其中带 unstable_ / UNSAFE_ 前缀的:5 / 5

5 个相关导出,5 个都带实验性前缀。 它还有个实验性的 peer 依赖 @vitejs/plugin-rsc(当天 npm 上是 0.5.35,仍是 0.x)。

结论:「RR 的嵌套路由」随时可用,「RR 的 RSC」现在只适合尝鲜。 想在生产上用 RSC,得先接受实现层的实验性——这也回到 1.6 节那句话,选型要评估的是框架而不是 React 版本。

四、三者拼起来是什么样​

层由谁负责放什么别放什么
路由与布局React Router嵌套结构、每层的 loader / action / error boundary别在父层塞只属于某个子页的数据
取数与模型调用Server Component / Server Functionprocess.env 里的 key、SDK、直接读库交互状态(useState 用不了)
交互与展示shadcn 组件 + Client Component表单状态、乐观更新、onClick密钥、以及任何想省下的重依赖

一句话版本:React Router 决定「页面怎么分层」,RSC 决定「哪一半跑在服务端」,shadcn 决定「客户端那一半长什么样」。

五、还没解决的问题​

诚实记下三个没结论的:

  1. 「省掉 BFF」在大项目里能不能成立? 官方原话是 without having to build an API,小场景成立。但真做起来,鉴权、限流、多模型路由、成本核算这些迟早要有个中间层——RSC 只是把 BFF 的代码挪进了路由,不等于 BFF 消失了。这是我的怀疑,本次没验证。
  2. shadcn 文档站里的 /docs/components/base/* 是什么? 抓页面时看到这批路径,猜是另一套底层实现(对应的还有 new-york 这种 style 名),但页面正文没抓到足够信息,没核对清楚,不下结论。
  3. RR 的 RSC 什么时候脱掉 unstable_? 官方说了 RSC 支持「Framework 和 Data 两种模式都有」,也给了模板,但没给稳定时间表。跟 1.6 节一样,这取决于实现层,值得按季度回看。

来源与核对​

三条论断的核对材料都在 source/rsc-ai-lab/(抓取日期均为 2026-09-23,抓取方式 curl -sS -L):

引用来源存档
RSC 定义 / 稳定度 / 访问数据层 / 不下发浏览器 / 无指令reactjs/react.dev src/content/reference/rsc/server-components.mdofficial-react-server-components.md
Server Functions / Server Actions 更名react.dev server-functions.mdofficial-react-server-functions.md
嵌套路由三句 + 路由模块能力remix-run/react-router docs/start/framework/routing.md、route-module.mdofficial-rr-routing.md
RSC 实验性警告 / unstable_reactRouterRSCreact-router docs/how-to/react-server-components.md(frontmatter unstable: true)official-rr-rsc-howto.md
「这不是组件库」/ Open Code / AI-Readyui.shadcn.com/docsofficial-shadcn-intro.txt
button 源码与依赖字段ui.shadcn.com/r/styles/new-york/button.jsonofficial-shadcn-button.json

实测脚本 source/rsc-ai-lab/run.sh(三个探针),真实输出见 run.log。环境:node v24.19.0、npm 11.17.0、react 19.3.0、react-router 8.4.0。