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状态同步的完整流程

相关推荐
萌狼蓝天1 小时前
[Vue]性能优化:动态首行与动态列的匹配,表格数据格式处理性能优化
前端·javascript·vue.js·性能优化·ecmascript
一 乐1 小时前
宠物管理宠物医院管理|基于Java+vue的宠物医院管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·宠物
一 乐1 小时前
学习辅导系统|数学辅导小程序|基于java+小程序的数学辅导小程序设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·学习·小程序
徐同保1 小时前
react+antd Input回车输入生成tag组件
前端·react.js·前端框架
YL有搞头1 小时前
webpack的构建流程以及loader和plugin
前端·webpack·node.js
2503_928411561 小时前
11.20 vue项目搭建-单页面应用
前端·javascript·vue.js
BUG创建者1 小时前
项目中使用script-ext-html-webpack-plugin
前端·webpack·html
极光代码工作室2 小时前
基于SpringBoot的校园招聘信息管理系统的设计与实现
java·前端·spring
打小就很皮...2 小时前
React VideoPlay 组件封装与使用指南
前端·react.js·video