Next.js SSR 实战:从零到一,构建服务端渲染应用

引言

今天我们来通过一篇文章掌握服务端渲染框架-nextjs的基本使用,通过本文一步一步实现来完成一个企业级SSR框架的搭建。

能点开这篇文章,相信你对SSR服务端渲染已经有了大概的了解,当然也可以看我前面的一篇文章让你搞清楚服务端渲染的核心原理是什么:搞懂SSR的灵魂-Hydration

咱们学习之前还是需要有一些的react预备知识,比如react16+、hooks、redux、react-redux、redux toolkit,如果是的,那咱们就发车了!

项目创建

java 复制代码
npx create-next-app@latest   //当然你也可以根据需求指定脚手架版本

项目的基本结构如下

python 复制代码
├── assets/             # 文件资源
├── components/         # 可复用组件
├── pages/              # 页面路由
   ├── _app.tsx         # 入口文件
   ├── _document.js     # 文档配置(可在这里定义TDK--sso优化)
   ├── index.tsx        # 首页
   ├── xxxx             # 其他页面
├── store/              # Redux状态管理
├── service/            # API服务层
├── styles/             # 全局样式
└── middleware.ts       # 中间件配置(路由拦截、请求重定向-反向代理...)
└── next.config.js      # 项目配置(网络图片优化配置...)
└── tsconfig.json       # typescript配置(路径别名...)

接下来在编辑器终端执行 npm run dev ,如果打开浏览器看到类似这样的页面就代表第一步已经成功了:

路由系统

项目运行起来了我们可能下一步考虑的是怎么创建路由,关联页面? nextjs具备的一个特点就是:零配置路由

Next.js最大的路由优势就是:不需要手动配置路由,文件系统就是路由系统!

bash 复制代码
# 文件结构自动映射为路由
pages/
├── index.tsx          → 自动映射为 /
├── about.tsx         → 自动映射为 /about
├── profile/
│   ├── index.tsx     → 自动映射为 /profile
│   └── [id].tsx      → 自动映射为 /profile/:id  注意:这里是动态路由,文件命名和参数接收时统一
└── api/
    └── user.ts       → 自动映射为 /api/user

路由跳转可通过下面两种方式

link组件跳转

javascript 复制代码
import Link from "next/link";
import type { AppProps } from "next/app";

export default function App({ Component, ...rest }: AppProps) {
  return (
        <Link href="/">
          <button>Home</button>
        </Link>
        <Link href="/profile?code=123">
          <button>Profile</button>
        </Link>
        <Link href="/profile/123444">
          <button>Profile动态</button>
        </Link>
        <button onClick={getInfo}>api重写</button>
        {/* Component页面占位 ===相当于vue中的 router-view */}
        <Component {...props.pageProps} />
  );
}

编程式导航跳转hook(类似vue3)

javascript 复制代码
import { useRouter } from 'next/router'
import type { AppProps } from "next/app";

export default function App({ Component, ...rest }: AppProps) {

const router = useRouter()
  
  // 基本跳转
  const handleNavigate = () => {
    router.push('/profile') // 跳转到个人资料页
  }
  
  // 带查询参数跳转
  const handleNavigateWithQuery = () => {
    router.push('/profile?code=123&name=john')
  }
  
  // 动态路由跳转
  const handleDynamicRoute = () => {
    router.push('/profile/123') // 跳转到 /profile/123
  }
  
  // 对象形式跳转(更灵活)
  const handleObjectNavigation = () => {
    router.push({
      pathname: '/profile',
      query: { 
        id: '123', 
        tab: 'settings' 
      }
    })
  }
}

路由传递参数的接收

javascript 复制代码
import { useRouter } from 'next/router'
import type { AppProps } from "next/app";

export default function App({ Component, ...rest }: AppProps) {

const router = useRouter()
// 拿到 url中查询字符串 注意:如果是动态路由这里解构的属性名就与文件名必须保持一致
// 动态路由参数都是通过query获取,同名的话路由参数优先级高于查询字符串 
const { id } = router.query; // 拿到 url中查询字符串 注意:如果是动态路由这里结构的属性名就与文件名
  return (
    <div className={styles.detail}>{id}</div>
  )
}

如果要执行路由守卫在中间件配置文件操作

csharp 复制代码
//middleware.ts

  // 路由守卫(示例)
  const token = req.cookies.get("token")?.value;
  if (!token && req.nextUrl.pathname.startsWith("/profile")) {
    return NextResponse.redirect(new URL("/login", req.nextUrl.origin));
  }

上面涵盖了nextjs路由系统的基本使用,接下来我们就来实践下服务端渲染的核心:

SSR数据获取(重点!!!)

我们先来看看页面中如何展示数据

typescript 复制代码
import { useSelector, useDispatch } from "react-redux";
import { fetchUserAction } from "@/store/modules/home";
import wrapper from "@/store/index";

import type { GetServerSideProps } from "next";
import type { FC } from "react";
import type {
  IUser,
} from "@/service/home";

interface IProps {
  users: IUser[];
}

const Home: FC<IProps> = (props) => {
  const {
    product = [], //通过getServerSideProps映射到props中了
  } = props;

  // 从 redux 读取数据
  const { users } = useSelector((rootState: IAppRootState) => {
    return {
      users: rootState.home.users,
    };
  });
  return (
    <>
      <div className={styles.home}>
        {product}
        {users}
      </div>
    </>
  );
};

export default Home;
Home.displayName = "Home";

export const getServerSideProps: GetServerSideProps =
  wrapper.getServerSideProps(function (store) {
    return async (context) => {
      // 1.触发一个异步的action来发起网络请求, 拿到接口数据并存到redex中
      await store.dispatch(fetchUserAction());
      // 2.实际项目中不一定非要在redux进行网络请求获取数据(考虑是否要作为公共数据存储)
      const res = await fetchProduct();
  
      return {
        props: {
          product: res.data.product || [],
        },
      };
    };
  });

看到这里可能会有几个个疑问:getServerSideProps有什么作用? fetchUserAction这个action做了什么?wrapper又是干嘛的?跟着我的节奏,接下来我们围绕这三个问题展开讲解。

首先我们要知道SSR通常是服务端和客户端分工协作的结果:

SSR数据预取(getServerSideProps) :

  • 在服务器端执行,页面首次加载时完成
  • 数据在页面渲染前就已准备好
  • 属于服务端渲染范畴
  • 适用于SEO重要、首次加载必需的数据

客户端数据获取:

  • 在浏览器端执行,页面渲染后触发
  • 通过用户交互(点击、滚动等)触发
  • 属于客户端渲染范畴
  • 适用于动态、用户交互相关的数据

在实际项目中,通常混合使用上面两种方式。 SEO关键数据通过 getServerSideProps 预取,用户交互相关的私密数据则在客户端通过 useEffect 或事件处理函数获取。

接下来我们看看fetchUserAction 在redux中具体实现:

typescript 复制代码
//store\modules\home.ts

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { HYDRATE } from "next-redux-wrapper";
import { fetchUser } from "../../service/home";

const homeSlice = createSlice({
  name: "home",
  initialState: {
    users: {},
  },
  extraReducers: (builder) => {
    // Hydrate 是 Next.js 提供的一个特殊的 action,用于在服务端渲染时将服务器端的状态合并到客户端的状态中。
    builder
      .addCase(HYDRATE, (state, action: any) => {
        return {
          ...state,
          ...action.payload.home,
        };
      })
      .addCase(fetchUserAction.fulfilled, (state, { payload }) => {
        state.users = payload;
      });
  },
});

// 异步的action
export const fetchUserAction = createAsyncThunk(
  "fetchUserAction",
  async () => {
    // 发起网络请求,拿到搜索建议的数据
    const res = await fetchUser();
    return res.data
  }
);
export default homeSlice.reducer;

每个reducer通过redux toolkit是作为一个切片模块,最终需要整合在一起

javascript 复制代码
//store\index.ts

import { configureStore } from "@reduxjs/toolkit";
import { createWrapper } from "next-redux-wrapper";
import homeReducer from "./modules/home";

const store = configureStore({
  reducer: {
    home: homeReducer,
  },
});

const wrapper = createWrapper(() => store);
export default wrapper;

这里我们可以看到createWrapper生成了一个wraper导出,它的主要作用就是使服务端与客户端状态同步,为什么需要同步?

我们再回顾下SSR的核心原理:

  • 服务端状态序列化:在服务器端渲染时,将 Redux store 的状态序列化并注入到 HTML 中
  • 客户端状态水合:在客户端首次加载时,将服务端注入的状态重新水合到客户端的 Redux store
  • 避免状态不一致:防止服务端和客户端渲染结果不一致的问题

最终,我们在入口文件_app.tsx中挂载store要做一点调整

javascript 复制代码
//pages\_app.tsx

import { Provider } from "react-redux";
import wrapper from "../store";
import type { AppProps } from "next/app";

export default function App({ Component, ...rest }: AppProps) {
  //实现服务端和客户端状态的自动同步
  const { store, props } = wrapper.useWrappedStore(rest);
  return (
    <Provider store={store}>
         <Component {...props.pageProps} />
    </Provider>
  );
}

自此,服务端渲染结合redux的核心应用就完成了!通过一张图梳理下个Next.js SSR数据预取与Redux状态同步的完整流程

相关推荐
jay神1 小时前
基于 YOLOv8 的轴承缺陷检测(Web 版)
前端·人工智能·yolo
jay神1 小时前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat1 小时前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss2 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan2 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真2 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment2 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan3 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么3 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技3 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue