Next.js 教程系列(七)服务端渲染 (SSR) 深度探究:`getServerSideProps`

前言

大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!


第七章:服务端渲染 (SSR) 深度探究:getServerSideProps

一、理论讲解

1.1 什么是服务端渲染(SSR)?

服务端渲染(Server Side Rendering,简称 SSR)是指在服务器端生成 HTML 内容并返回给客户端浏览器的技术。与传统的客户端渲染(CSR)相比,SSR 可以显著提升首屏加载速度、SEO 友好性和内容可访问性。

  • SSR 优势
    • 首屏渲染更快,用户体验更好
    • 更利于 SEO,爬虫可直接抓取内容
    • 适合内容型、营销型、首屏要求高的应用
  • SSR 劣势
    • 服务端压力大,扩展性需关注
    • 复杂交互仍需客户端 JS 支持

1.2 Next.js SSR 的实现方式

Next.js 提供了多种数据获取方式:

  • getStaticProps(静态生成,SSG)
  • getServerSideProps(服务端渲染,SSR)
  • getInitialProps(早期方案,已不推荐)

本章重点聚焦于 getServerSideProps,它让我们可以在每次请求时都在服务端动态获取数据。

1.3 SSR 适用场景

  • 需要实时数据的页面(如用户仪表盘、新闻列表)
  • 需要权限校验的页面
  • SEO 要求高的内容页
  • 需要根据请求头、Cookie、Session 动态渲染的页面

二、getServerSideProps 详解

2.1 基本用法

getServerSideProps 是 Next.js Pages Router 下的服务端数据获取函数。它只能用于 pages 目录下的页面组件,并且每次请求都会在服务端执行。

tsx 复制代码
// pages/news.tsx
import React from 'react';
import type { GetServerSideProps, NextPage } from 'next';

interface NewsProps {
  newsList: { id: number; title: string; content: string }[];
}

const News: NextPage<NewsProps> = ({ newsList }) => (
  <div>
    <h1>最新新闻</h1>
    <ul>
      {newsList.map((item) => (
        <li key={item.id}>
          <h2>{item.title}</h2>
          <p>{item.content}</p>
        </li>
      ))}
    </ul>
  </div>
);

export const getServerSideProps: GetServerSideProps = async (context) => {
  // 这里可以访问 context.req, context.res, query, params, cookies 等
  const res = await fetch('https://api.example.com/news');
  const newsList = await res.json();
  return {
    props: { newsList },
  };
};

export default News;
2.2 参数详解
  • context.req / context.res:Node.js 原生请求/响应对象
  • context.query:URL 查询参数
  • context.params:动态路由参数
  • context.locale:当前语言环境
  • context.resolvedUrl:解析后的 URL
2.3 返回值说明
  • props:传递给页面组件的 props
  • redirect:重定向到其他页面
  • notFound:返回 404 页面
ts 复制代码
return {
  redirect: {
    destination: '/login',
    permanent: false,
  },
};

三、实战项目:SSR 新闻列表

3.1 需求分析

  • 实现一个新闻列表页,数据实时获取,支持移动端适配
  • 未登录用户跳转到登录页
  • 支持国际化(中英文切换)
  • 错误处理与性能优化

3.2 目录结构

bash 复制代码
/pages
  ├── news.tsx
  ├── login.tsx
  └── _app.tsx
/components
  └── NewsList.tsx
/utils
  └── auth.ts

3.3 代码实现

3.3.1 登录校验工具
ts 复制代码
// utils/auth.ts
export function checkLogin(req: any) {
  // 假设通过 Cookie 判断登录
  return Boolean(req.cookies?.token);
}
3.3.2 SSR 页面实现
tsx 复制代码
// pages/news.tsx
import React from 'react';
import { GetServerSideProps } from 'next';
import { checkLogin } from '../utils/auth';
import NewsList from '../components/NewsList';

const NewsPage = ({ newsList, locale }: any) => (
  <div>
    <h1>{locale === 'en' ? 'Latest News' : '最新新闻'}</h1>
    <NewsList newsList={newsList} />
  </div>
);

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { req, locale } = context;
  if (!checkLogin(req)) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    };
  }
  try {
    const res = await fetch('https://api.example.com/news');
    const newsList = await res.json();
    return {
      props: { newsList, locale },
    };
  } catch (error) {
    return {
      props: { newsList: [], locale },
    };
  }
};

export default NewsPage;
3.3.3 组件拆分与样式
tsx 复制代码
// components/NewsList.tsx
import React from 'react';

export default function NewsList({ newsList }: { newsList: any[] }) {
  if (!newsList.length) return <div>暂无新闻</div>;
  return (
    <ul>
      {newsList.map((item) => (
        <li key={item.id}>
          <h2>{item.title}</h2>
          <p>{item.content}</p>
        </li>
      ))}
    </ul>
  );
}
3.3.4 移动端适配与样式优化
css 复制代码
/* styles/news.module.css */
ul {
  padding: 0;
  list-style: none;
}
li {
  margin-bottom: 16px;
  border-bottom: 1px solid #eee;
  padding-bottom: 8px;
}
@media (max-width: 600px) {
  h2 {
    font-size: 18px;
  }
  p {
    font-size: 14px;
  }
}
3.3.5 国际化支持

Next.js 支持内置国际化,可通过 context.locale 获取当前语言。

3.3.6 错误处理与性能优化
  • 使用 try/catch 捕获服务端异常,避免页面崩溃
  • 可结合 SWR、React Query 等客户端缓存方案提升体验
  • 服务端可设置缓存头(Cache-Control)优化接口性能

四、企业级最佳实践

4.1 代码组织与团队协作

4.1.1 目录结构与分层

在企业级项目中,推荐采用分层结构:

  • pages/ 只负责路由和页面入口,页面只做数据拼装和渲染
  • components/ 存放通用 UI 组件
  • modules/features/ 目录按业务模块拆分,聚合相关页面、组件、hooks、service
  • services/ 统一管理 API 请求、服务端数据获取逻辑
  • utils/ 存放工具函数
  • middlewares/ 存放 SSR 中间件(如鉴权、国际化、日志等)
4.1.2 SSR 中间件模式

可将 SSR 逻辑拆分为中间件链式调用,提升复用性和可维护性。

ts 复制代码
// middlewares/withAuth.ts
import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
export function withAuth<T>(gssp: (ctx: GetServerSidePropsContext) => Promise<GetServerSidePropsResult<T>>) {
  return async (ctx: GetServerSidePropsContext) => {
    const { req } = ctx;
    if (!req.cookies?.token) {
      return {
        redirect: {
          destination: '/login',
          permanent: false,
        },
      };
    }
    return await gssp(ctx);
  };
}
ts 复制代码
// pages/dashboard.tsx
import { withAuth } from '../middlewares/withAuth';
export const getServerSideProps = withAuth(async (ctx) => {
  // 业务数据获取
  return { props: { /* ... */ } };
});
4.1.3 团队协作与代码规范
  • 统一 TypeScript 类型定义,提升开发效率和可维护性
  • 结合 ESLint、Prettier、Husky 保障代码风格一致
  • 采用 Git Flow 或 trunk-based workflow 管理分支
  • 代码评审流程自动化(如 GitHub Actions)

4.2 安全与权限

4.2.1 SSR 鉴权与权限控制
  • getServerSideProps 中校验用户身份、角色权限
  • 敏感接口仅在服务端调用,避免暴露 token、密钥
  • 对于多角色系统,结合 RBAC(基于角色的访问控制)
ts 复制代码
// middlewares/withRole.ts
export function withRole(roles: string[], gssp) {
  return async (ctx) => {
    const user = getUserFromCookie(ctx.req);
    if (!user || !roles.includes(user.role)) {
      return { redirect: { destination: '/403', permanent: false } };
    }
    return await gssp(ctx);
  };
}
4.2.2 防御 SSR 注入与 XSS
  • 服务端渲染时,所有输出内容需转义
  • 严格校验和过滤外部输入,防止 SQL 注入、命令注入
  • 使用 Helmet 等库设置 HTTP 安全头
4.2.3 CSRF 防护
  • SSR 场景下,表单/接口需校验 CSRF Token
  • 可结合 NextAuth.js、csrf 包实现

4.3 性能优化

4.3.1 SSR 缓存与分布式
  • 利用 HTTP 缓存头(Cache-Control)提升页面响应速度
  • 结合 Redis、Memcached 等缓存热点数据,减轻数据库压力
  • CDN 边缘缓存 SSR 页面(如 Vercel Edge、Cloudflare)
ts 复制代码
// pages/news.tsx
export const getServerSideProps = async (ctx) => {
  ctx.res.setHeader('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=30');
  // ...数据获取
  return { props: { /* ... */ } };
};
4.3.2 数据预取与并发优化
  • 服务端可并发请求多个数据源,提升整体渲染速度
  • 合理拆分页面,避免 SSR 过重
ts 复制代码
// 并发获取多接口数据
const [userRes, newsRes] = await Promise.all([
  fetch('https://api.example.com/user'),
  fetch('https://api.example.com/news'),
]);
4.3.3 图片与静态资源优化
  • 使用 next/image 组件自动优化图片
  • 静态资源走 CDN,减少主站压力
4.3.4 SSR 性能监控
  • 接入 APM(如 Datadog、NewRelic)监控 SSR 响应时间
  • 记录慢查询、异常日志,定期分析优化

4.4 移动端适配

4.4.1 响应式设计
  • 使用 CSS-in-JS、Tailwind CSS、styled-components 等方案实现响应式
  • SSR 阶段可根据 UA 判断设备类型,动态渲染不同布局
ts 复制代码
// 判断移动端 UA
const isMobile = /mobile/i.test(ctx.req.headers['user-agent'] || '');
4.4.2 移动端首屏优化
  • SSR 阶段输出精简 HTML,减少首屏体积
  • 关键 CSS 内联,提升渲染速度

4.5 自动化测试与监控

4.5.1 SSR 单元测试
  • 使用 Jest + Testing Library 测试 SSR 逻辑
  • Mock fetch、数据库等外部依赖
ts 复制代码
// __tests__/news.test.ts
import { getServerSideProps } from '../pages/news';
test('SSR 正常返回数据', async () => {
  global.fetch = jest.fn(() => Promise.resolve({ json: () => [{ id: 1, title: 'test', content: 'c' }] })) as any;
  const ctx = { req: { cookies: { token: '123' } }, locale: 'zh' } as any;
  const result = await getServerSideProps(ctx);
  expect(result).toHaveProperty('props.newsList');
});
4.5.2 E2E 测试
  • 使用 Playwright/Cypress 模拟用户端到端访问,验证 SSR 页面渲染、跳转、权限
4.5.3 监控与告警
  • 接入 Sentry、LogRocket 等监控 SSR 错误
  • SSR 关键路径设置告警,及时发现问题

4.6 常见问题与解决方案

4.6.1 SSR 页面数据闪烁/丢失
  • 客户端 hydration 需与服务端数据一致,避免"内容闪烁"
  • 建议 SSR 返回的数据结构与客户端保持一致
4.6.2 SEO 优化细节
  • SSR 页面需设置完整 meta、title、OG 标签
  • 动态内容建议服务端渲染,提升爬虫抓取率
4.6.3 SSR 性能瓶颈与扩展
  • 高并发场景下,建议前置缓存、分布式部署
  • SSR 服务可横向扩展,结合负载均衡
4.6.4 SSR 与客户端状态同步
  • SSR 获取的数据通过 props 传递,客户端可用 React Context、Redux、Zustand 等管理全局状态
  • 结合 SWR/React Query 实现客户端数据自动同步与缓存
4.6.5 SSR 错误处理
  • 服务端异常需捕获并友好展示,避免 500 页面直出
  • 可自定义 _error.tsx 页面统一处理错误
tsx 复制代码
// pages/_error.tsx
function Error({ statusCode }) {
  return (
    <div>
      <h1>出错啦!</h1>
      <p>错误码:{statusCode}</p>
      <a href="/">返回首页</a>
    </div>
  );
}
Error.getInitialProps = ({ res, err }) => {
  const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
  return { statusCode };
};
export default Error;

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!

相关推荐
foxhuli22918 分钟前
禁止ifrmare标签上的文件,实现自动下载功能,并且隐藏工具栏
前端
青皮桔1 小时前
CSS实现百分比水柱图
前端·css
影子信息1 小时前
vue 前端动态导入文件 import.meta.glob
前端·javascript·vue.js
青阳流月1 小时前
1.vue权衡的艺术
前端·vue.js·开源
样子20181 小时前
Vue3 之dialog弹框简单制作
前端·javascript·vue.js·前端框架·ecmascript
kevin_水滴石穿1 小时前
Vue 中报错 TypeError: crypto$2.getRandomValues is not a function
前端·javascript·vue.js
孤水寒月2 小时前
给自己网站增加一个免费的AI助手,纯HTML
前端·人工智能·html
CoderLiu2 小时前
用这个MCP,只给大模型一个figma链接就能直接导出图片,还能自动压缩上传?
前端·llm·mcp
伍哥的传说2 小时前
鸿蒙系统(HarmonyOS)应用开发之实现电子签名效果
开发语言·前端·华为·harmonyos·鸿蒙·鸿蒙系统
海的诗篇_3 小时前
前端开发面试题总结-原生小程序部分
前端·javascript·面试·小程序·vue·html