javascript如何实现服务端渲染_Next.js是如何处理这个过程的?

服务端渲染在JavaScript中的本质是代码在Node.js中执行并生成HTML字符串;Next.js的getServerSideProps每次请求时服务端执行并注入props;SSR与hydration断连主因是服务端与客户端渲染不一致;App Router中SSR变为隐式async组件执行,fetch默认缓存。

服务端渲染在 JavaScript 中的本质是什么?

服务端渲染(SSR)不是框架的专利,而是指 JavaScript 代码在 Node.js 环境中执行并生成 HTML 字符串,再返回给浏览器。关键在于:DOM 操作不能依赖浏览器全局对象(如 windowdocument),否则会抛出 ReferenceError

纯手写 SSR 需要手动处理:

  • 入口文件需兼容服务端(无浏览器 API 调用)
  • 组件状态必须可序列化,且首次渲染结果能被 React.renderToStringVue.renderToString 捕获
  • 数据获取逻辑必须在服务端触发,并注入到 HTML 中(如通过 window.__INITIAL_STATE__
  • 客户端 hydrate 时需复用服务端生成的 DOM,避免重复渲染闪烁

Next.js 的 getServerSideProps 是怎么工作的?

这是 Next.js 实现 SSR 的核心约定函数,每次请求都会在服务端执行,返回的数据会作为 props 注入页面组件。它不是“预渲染”,而是真正意义上的请求时服务端执行。

注意点:

  • 只在页面组件(pages/xxx.jsapp/xxx/page.js 的旧版 pages 目录下)中有效
  • 不能在普通组件中使用,也不能在 app 目录的默认 Server Components 中直接调用(新版用 async 组件 + fetch 替代)
  • 函数内可调用数据库、API、fs.readFile 等 Node.js API,但不能访问 window
  • 返回值必须是 { props: { ... } } 对象,否则页面会 500
export async function getServerSideProps(context) {
  const res = await fetch('https://api.example.com/user')
  const user = await res.json()
  return {
    props: { user } // 这个对象会变成组件的 props
  }
}

Next.js 的 SSR 和客户端 hydration 之间容易断连的点

最常见问题是服务端渲染内容与客户端初次 render 结果不一致,导致 React 报错 Hydration failed because the initial UI does not match what was rendered on the server

典型诱因:

  • 组件内部使用了 Math.random()Date.now() 等非确定性值,服务端和客户端结果不同
  • 条件渲染依赖了 typeof window !== 'undefined',服务端为 true(因为 window 不存在),客户端为 false,导致 DOM 结构不一致
  • useEffect 内修改了服务端已渲染的 DOM(比如动态插入广告位),但服务端没做对应输出
  • 样式库(如 styled-components)未正确启用 StyleSheetManager 或未同步服务端的 class name 生成逻辑

App Router(app 目录)下 SSR 的实际形态变了

Next.js 13+ 的 app 目录默认使用 Server Components,不再需要 getServerSideProps。SSR 变成隐式行为:所有 async 组件函数都在服务端执行,await fetch 自动被拦截并去重、缓存。

但这也带来新约束:

  • Server Components 中不能使用 useStateuseEffect、浏览器 API
  • 交互逻辑必须下沉到 "use client" 标记的 Client Components 中
  • fetch 默认开启 cache: 'force-cache',要实现真正的 SSR(每次请求都发新请求),得显式写 fetch(url, { cache: 'no-store' })
  • 服务端生成的 HTML 不再包含完整 JS bundle,hydration 更轻量,但也意味着部分逻辑无法在服务端“提前执行”

真实项目里,很多人卡在“为什么 app 目录下接口没重发”,其实只是 fetch 缓存策略默认变了。