Next.js 里写 SSR 组件的正确姿势:参数与注意点一次讲清

编程狮(w3cschool.cn) 2026-10-24 07:02:52 浏览数 (27)
反馈

先给结论:Next.js 里写服务端渲染组件,默认就已经在服务端跑了,不要再套一层取数据的副作用。App Router 里把组件写成 async 函数直接等数据,Pages Router 里导出 getServerSideProps 把数据作为属性传进去,两种写法都能在返回给浏览器时带上已经渲染好的内容。

服务端渲染组件到底该怎么写?

新手最容易犯的错,是把浏览器端的写法原样搬过来:在组件里挂一个副作用去请求接口,结果服务端渲染出来的页面里没有数据,搜索引擎抓到的还是空壳。

本文给出两套可直接对照的最小示例,说明各自的写法边界、哪些浏览器接口不能用、以及三种常见报错的判断方式。示例按 Next.js 13 之后引入的 App Router 为主,Pages Router 的写法作为对照保留。

先看结论

  • 服务端渲染组件默认就在服务端执行:不需要、也不应该再用副作用取首屏数据。
  • 浏览器接口一律不可用:凡是依赖窗口对象或生命周期的东西,都要挪到客户端组件。
  • 两种路由写法不通用:App Router 用 async 组件,Pages Router 用导出的取数函数,混写会直接报错。

一、最小场景:两种路由的写法

把问题定死:有一个商品详情页,需要在返回页面之前把商品标题查出来。

App Router 的写法是把组件直接声明成 async 函数:

// app/product/[id]/page.jsx
async function getProduct(id) {
  const res = await fetch(`https://example.com/api/product/${id}`, {
    cache: 'no-store'
  });
  if (!res.ok) {
    throw new Error('商品不存在');
  }
  return res.json();
}

export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  return <h1>{product.title}</h1>;
}

这段做的是在组件内部直接等待数据返回,再把结果渲染出去。预期结果(未在本机执行):查看页面源代码能看到标题已经写在返回的标记里。

Pages Router 的写法则是把取数逻辑挪到导出的函数里:

// pages/product/[id].js
export async function getServerSideProps(context) {
  const { id } = context.params;
  const res = await fetch(`https://example.com/api/product/${id}`);
  if (!res.status === 200) {
    return { notFound: true };
  }
  const product = await res.json();
  return { props: { product } };
}

export default function ProductPage({ product }) {
  return <h1>{product.title}</h1>;
}

这段做的是由框架先调用取数函数,把结果作为属性传给组件。两种做法都属于数据预取,区别只在于取数发生在组件内部还是独立的导出函数里;但两者不能互相套用:App Router 里导出 getServerSideProps 会被直接忽略,Pages Router 里把组件写成 async 函数则会报错。

二、参数与注意点

下面几条是写服务端渲染组件时真正会卡住人的地方。异步语法记不牢时,翻 JavaScript 速查手册 对照一眼最快。

2.1 服务端渲染组件里没有浏览器环境

窗口对象、本地存储、以及在挂载后才执行的生命周期钩子,在服务端渲染组件里都不存在。写了会在构建或请求阶段直接抛"未定义"。

判断标准很直接:只要这段代码在 Node.js 里跑不通,它就不该出现在服务端渲染组件里。拿不准某个接口属于浏览器还是运行时,可以对照 Node.js 教程 的运行时章节确认。

2.2 什么时候必须标客户端组件

只有三种情况需要加客户端组件声明:用到状态或副作用、绑定事件、以及依赖浏览器接口。其余一律留在服务端,能少传一大堆脚本到浏览器。

服务端组件与客户端组件的划分决策

一个实用的划分办法:把交互的那部分单独抽成小组件,只给它加声明,页面主体继续保持服务端渲染。

2.3 缓存与请求去重

默认的请求会带缓存,同一个地址在一次渲染里的重复请求会被自动合并。需要每次都拿最新数据时,显式声明不缓存;数据基本不变时则保留默认,省掉一轮网络往返。

这是性能取舍的关键处:把不常变的数据留给缓存,只对真正实时的接口关掉缓存,全关会让服务端压力翻倍。

三、常见失败分支

3.1 报"窗口未定义"

组件里用到了浏览器接口,但没标客户端组件。要么把用到的那几行挪进客户端组件,要么改成在挂载后再读取。

3.2 报"水合不一致"

服务端和客户端算出了不同的内容,常见于渲染时用了时间戳、随机数或本地存储。修复方式是把这类值的读取推迟到挂载之后,保证两侧首次渲染结果一致。

3.3 页面源码里没有数据

说明数据是在客户端才取到的。检查是不是把取数写进了副作用,或者组件根本没有被服务端渲染。

3.4 验证清单

  • 检查:禁用浏览器脚本后刷新页面,内容是否仍在
  • 运行:查看页面源代码,确认关键数据已经写在标记里
  • 预期:首屏源码包含数据,控制台无水合警告
  • 失败:报未定义就回到浏览器接口清单逐项排查
  • 修复:把交互部分抽成独立的客户端组件
  • 验证:分别在开发模式与生产构建下各跑一次

两种路由写法与三类常见报错的对照

总结

Next.js 里写服务端渲染组件,核心是把"取数发生在哪里"想清楚:App Router 直接在组件里等,Pages Router 交给导出的取数函数,两者都不需要副作用。

三条要点带走:

  • 服务端渲染组件里没有浏览器环境,窗口与本地存储一律不可用;
  • 只有状态、事件与浏览器接口这三类才需要标客户端组件;
  • 缓存默认开启,只对真正实时的数据关掉它。

下一步建议把"页面主体保持服务端、交互抽成小组件"作为默认结构。语言细节可以对照 JavaScript 教程 里的异步章节再确认一遍。

常见问题

Q:服务端渲染组件里能用状态钩子吗?

A:不能。状态依赖组件的生命周期,只有标了客户端组件的组件才能使用。需要交互就把那一小块单独抽出去。

Q:两种路由的取数函数能混着用吗?

A:不能。App Router 不认识 Pages Router 的取数导出,反过来也一样。一个项目里选定一种路由写法再统一改造。

Q:为什么本地正常,构建时报错?

A:构建阶段会在没有浏览器的环境里执行组件。凡是直接读取窗口对象的代码都会在这一步暴露出来,把这类读取挪走即可。

Q:数据每次都要最新,怎么关掉缓存?

A:在请求选项里显式声明不缓存,或者把该路由标记为动态。注意只对确实实时的数据这么做。

Q:服务端渲染组件会影响首屏速度吗?

A:数据查询慢会直接拖慢首屏,因为要等数据返回才输出页面。慢接口建议加缓存或拆分到客户端按需加载。

延伸学习

想把这个知识点补扎实,可以按这个顺序来:

  1. 想把语言基础补系统,JavaScript 原生模块 可以跟一遍;
  2. 读一篇相关笔记 服务端渲染提升性能与 SEO,看别人踩过的坑;
  3. 想随手验证一段代码,用 JavaScript 在线运行 跑一遍最快。

0 人点赞