Nuxt 4 Server Components 从入门到理解:不写 API 的前端长什么样 一、引言传统 SSR 的两头编痛点你在用 Vue 3 写一个文章列表页产品经理要求 SEO 友好 首屏秒开。你选了 SSR服务端渲染然后你的日常变成了在server/api/写一个接口连数据库查文章列表在页面setup里用useFetch调这个接口处理好 loading、error、空数据三种状态写完发现明明数据就在服务端为什么还要绕一圈 HTTP 调用这就是传统 SSR 的两头编困境——同一份渲染逻辑你既要在服务端跑一遍生成 HTML又要在客户端再跑一遍激活交互。API 层成了绕不开的中间商而大部分列表展示类组件其实根本不需要客户端交互。Nuxt 4 给出的答案是Server Components——让组件自己决定我在哪运行。二、什么是 Server Components一句话定义Server Component 是只在服务端运行的组件它的 JavaScript 代码永远不会发送到浏览器。和传统 SSR 的区别对比维度传统 SSRServer ComponentJS 是否下发到浏览器✅ 是需要 hydration❌ 否零 JS Bundle服务端角色生成 HTML 快照原地完成渲染能否直接访问数据库需要走 API 层✅ 直接在组件里查能否有交互事件✅ 可以❌ 不能需嵌套客户端组件客户端工作量重新执行一遍渲染只接收 HTML无渲染逻辑打个比方传统 SSR 像外卖——厨房做好菜服务端渲染但你还是得自己摆盘、倒饮料客户端 hydration。Server Component 像堂食——菜端上来就能吃你不需要知道厨房怎么做的。为什么需要它考虑一个常见场景文章详情页。页面 90% 是静态内容标题、正文、作者信息只有 10% 需要交互点赞按钮、收藏按钮。传统 SSR整个页面的 JS 都下发到浏览器包括那些永远不变的渲染逻辑。Server Component静态部分在服务端跑完只下发 HTML交互部分用客户端组件JS 照常下发。结果用户下载的 JS 体积大幅减少首屏加载更快。三、Nuxt 4 怎么做的Nuxt 4 的 Server Component 实现核心依赖两个机制文件命名约定和NuxtIsland组件。从 4.0 开始experimental.componentIslands默认开启你不需要做任何配置。3.1.server.vue一个后缀搞定创建 Server Component 最简单的方式给组件文件加上.server.vue后缀。!-- components/ArticleList.server.vue -- script setup langts // ✅ 这个组件运行在服务端可以直接查数据库 // ✅ 这里可以 import 服务端依赖如 ORM、fs不会打包到客户端 interface Article { id: number title: string summary: string createdAt: string } // useFetch 在服务端直接执行不走浏览器网络请求 const { data: articles, pending, error } await useFetchArticle[](/api/articles, { // 服务端渲染时 key 用于缓存去重 key: article-list, }) // 服务端可以直接用 process.server 做条件判断 if (process.server) { console.log(这段日志只出现在服务端终端浏览器控制台看不到) } /script template div classarticle-list div v-ifpending加载中.../div div v-else-iferror出错了{{ error.message }}/div article v-foritem in articles :keyitem.id classarticle-card h3{{ item.title }}/h3 p{{ item.summary }}/p time{{ item.createdAt }}/time /article /div /templateNuxt 在构建时识别.server.vue后缀自动把这个组件标记为 server-only。它在服务端渲染成纯 HTML 后直接输出不打包任何 JS 到客户端。3.2 更进一步组件内直接查数据库既然运行在服务端完全可以在组件里直接操作数据库省掉 API 中间层!-- components/RecentArticles.server.vue -- script setup langts // ⚠️ 仅在服务端执行这段代码不出现在客户端 bundle import { eq, desc } from drizzle-orm import { useDrizzle } from ~/server/utils/drizzle const db useDrizzle() const articles await db.query.articles.findMany({ orderBy: [desc(articles.createdAt)], limit: 10, }) /script template ul li v-forarticle in articles :keyarticle.id NuxtLink :to/article/${article.id}{{ article.title }}/NuxtLink /li /ul /template注意这种方式适合内网应用或内容站。对外部服务暴露数据库连接需要评估安全风险——Nuxt 的 server component 在服务端执行数据库凭证不会泄露到客户端但 DB 查询的负载会直接打到数据库。3.3NuxtIsland手动控制渲染时机除了文件约定Nuxt 4 还提供了NuxtIsland组件让你在父组件中精确控制岛屿的加载行为!-- pages/index.vue -- template div h1首页/h1 !-- 默认行为阻塞渲染等 island 返回再显示 -- NuxtIsland nameRecentArticles / !-- 懒加载不阻塞页面渲染先显示 fallback -- NuxtIsland nameRecentArticles lazy :props{ limit: 5 } template #fallback div classskeleton正在加载文章列表.../div /template /NuxtIsland /div /template属性类型说明namestring组件名对应components/islands/下的文件名lazybooleantrue时不阻塞页面渲染propsobject传递给 island 组件的参数sourcestring从远端加载 island高级用法当lazy为true时页面先渲染骨架屏随后向/__nuxt_island/端点发起请求获取 island 的 HTML 后替换进去——整个过程用户无感知。3.4 如何刷新 Server Component组件渲染后如果需要更新数据比如用户提交了新评论可以调用refresh()script setup langts const articleComments ref() function handleCommentPosted() { // 触发 island 重新向服务端请求最新数据 articleComments.value?.refresh() } /script template NuxtIsland refarticleComments nameArticleComments :props{ articleId: 123 } / CommentForm submittedhandleCommentPosted / /template四、一个完整示例服务端列表 客户端交互下面通过一个实际场景——“文章列表 点赞”——展示 Server Component 和 Client Component 如何配合。目录结构components/ ├── ArticleFeed.server.vue # 服务端渲染文章列表.server.vue 后缀 └── LikeButton.client.vue # 客户端点赞按钮.client.vue 后缀服务端组件负责数据获取和渲染!-- components/ArticleFeed.server.vue -- script setup langts interface Article { id: number title: string summary: string likes: number createdAt: string } const { data: articles } await useFetchArticle[](/api/articles, { key: home-article-feed, }) /script template div classfeed !-- 服务端渲染的纯 HTML文章卡片的结构和样式都在服务端完成。 每个卡片内嵌一个 LikeButton 客户端组件来处理点赞交互。 -- div v-forarticle in articles :keyarticle.id classarticle-card h3{{ article.title }}/h3 p{{ article.summary }}/p div classcard-footer time{{ article.createdAt }}/time !-- LikeButton 是 .client.vueNuxt 识别后按传统 SSR 处理 下发 JS 并在客户端激活保持点击交互能力。 -- LikeButton :article-idarticle.id :initial-likesarticle.likes / /div /div /div /template客户端组件仅负责交互!-- components/LikeButton.client.vue -- script setup langts const props defineProps{ articleId: number initialLikes: number }() const likes ref(props.initialLikes) const loading ref(false) async function handleLike() { if (loading.value) return loading.value true try { const res await $fetch(/api/articles/${props.articleId}/like, { method: POST, }) likes.value res.likes } catch (e) { console.error(点赞失败, e) } finally { loading.value false } } /script template button :disabledloading classlike-btn clickhandleLike {{ likes }} /button /template发生了什么最终效果文章列表部分零 JS Bundle点赞按钮的 JS 只有几 KB。对比传统 SSR 整个页面的几十 KB JS体积缩小了 70% 以上。五、踩坑清单Server Component 虽好用但有几个容易踩的坑坑一服务端组件不能有交互事件!-- ❌ 错误server component 里写 click -- template button clickhandleClick点我/button /template现象点击没反应或控制台报错。原因Server Component 不下发 JSclick处理器根本不存在于浏览器端。解决把需要交互的部分拆成.client.vue组件通过 slot 或 prop 嵌套。坑二服务端组件不要使用浏览器 API!-- ❌ 错误 -- script setup langts const width ref(window.innerWidth) // window 在服务端不存在 const stored localStorage.getItem(token) // localStorage 也不存在 /script现象服务端渲染报错window is not defined。解决用process.client守卫或把这些逻辑放到onMounted里但 server component 的onMounted也不会执行。更好的做法是拆成客户端组件。坑三多个NuxtIsland的性能代价每个NuxtIsland在服务端都会启动一个独立的渲染上下文——相当于跑一次迷你 Nuxt 应用。如果一个页面放了 20 个 island服务端负载会显著增加。建议一个页面控制在 3–5 个 island 以内合并可以合并的数据请求。坑四server component 的useFetchkey 去重script setup langts // ⚠️ 如果同一个 URL 在多个 island 中出现需要手动管理 key const { data } await useFetch(/api/articles, { key: unique-key-per-component, // 避免请求重复 }) /scriptNuxt 4 默认对useFetch做请求去重但如果多个 server component 请求同一个接口且没指定 key可能出现缓存交叉污染。始终给 key 赋值。坑五注意 Nuxt 4 版本2025 年 Nuxt 4 经历了几个安全漏洞CVE-2026-47200、CVE-2026-46342涉及.server.vue的 middleware 绕过和 island 缓存投毒问题。这些问题在Nuxt 4.4.6已修复。生产环境务必使用 4.4.6 或更高版本。检查当前版本npx nuxi info升级到最新npx nuxi upgrade六、总结Server Component 不是要取代 SSR而是给 SSR 提供了一种取舍的自由度静态内容列表、详情、导航→ Server Component零 JS极速渲染交互组件按钮、表单、弹窗→ Client Component保持完整交互能力混合场景大部分真实业务都是这样→ Server Component 包 Client Component各取所需如果你是从 Vue 3 SPA 起步的前端上手 Nuxt 4 Server Component 的学习曲线并不陡——你不需要重写整个应用可以一个新模块一个新模块地迁移。先从某个纯展示的列表页下手把它改成.server.vue看看页面加载速度的变化。与其继续在server/api 写接口 → 页面调接口这个循环里兜圈子不如让组件自己决定在哪里运行。