Next App Router 模式下,如何同步服务端 Redux 初始状态?

news/2024/11/18 0:20:26/文章来源:https://www.cnblogs.com/deerblog/p/18551459

大家的阅读是我发帖的动力,本文首发于我的博客:deerblog.gu-nami.com/,欢迎大家来玩,转载请注明出处

🎈前言

Next.js 是一个广受欢迎的 React 服务端渲染(Server Side Rendering,SSR)框架。Next.js 的页面会先在服务端渲染一次,然后把结果传给浏览器,也就是客户端,再在客户端渲染一遍,并且运行客户端特有的逻辑。如果使用 Redux,一般情况下,在服务端渲染的时候,初始化了的 Redux 全局状态会被创建。然而服务器返回的是只有 HTML 标签的页面,客户端无法获得服务端 Redux 的状态,会引起水合错误,依赖 Redux 的组件渲染异常等影响体验的问题。我们需要同步服务端的 Redux 状态。

在 Page Router 模式下,Redux 同步状态已经有了成熟的解决方案,可以使用 next-redux-wrapper 完成,但是它并不适用于 App Router 模式的应用。这里参考 Redux 文档 的方法,给出一些个人在 Next.js 上同步 Redux 状态的小技巧,也只是一些个人做法,大佬们肯定有更优雅的方法的。

🎀解决思路

先来看createStore的入参:

export declare function createStore<S, A extends Action, Ext, StateExt>(reducer: Reducer<S, A>,preloadedState?: PreloadedState<S>,enhancer?: StoreEnhancer<Ext>
): Store<S & StateExt, A> & Ext

第二个参数preloadedStatestore的初始状态,只要在服务端 / 客户端中,都传入一样的内容,就可以创建两个状态一模一样的store

服务端渲染时,初始状态可在服务端组件(React Server Component,RSC)中直接查询获得。在客户端中,如果走网络请求,则在首次渲染中是拿不到服务端状态的。我们不妨把状态写入 HTML 中,带往客户端,然后客户端就可以同步服务端 Redux 的状态了。

我们先来以我博客的统计数据为例,这是目前的效果:

🎉服务端渲染阶段初始化 Redux

先写一个创建 Redux 的代码(代码很大一部分是从老项目中迁移的,当时并没有用上 Redux Toolkit,请见谅):

import { thunk } from 'redux-thunk'
import { compose, createStore, applyMiddleware, StoreEnhancer, Store, EmptyObject } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'import rootReducer, { combinedStateType } from './rootReducers'let storeEnhancers: StoreEnhancer
if (process.env.NODE_ENV === 'production') {storeEnhancers = compose(applyMiddleware(thunk))
} else {storeEnhancers = compose(composeWithDevTools(applyMiddleware(thunk)))
}export type reduxStoreType = Store<EmptyObject & combinedStateType>export const configureStore = (initialState = {}) => {return createStore(rootReducer, initialState, storeEnhancers)
}

我们可以在Provider中完成初始化,毕竟 RSC 需要以组件的形式组织代码,而且后面useSelectoruseDispatch之类的钩子也需要它。这里ReduxProvider接收data参数作为初始状态。

import { configureStore, reduxStoreType } from "./index"
export default function ReduxProvider ({children, data
}: {children: React.ReactNode, data: any
}) {const storeRef = useRef<reduxStoreType | null>(null)const initialState = dataif (!storeRef.current) {storeRef.current = configureStore(initialState)}return <Provide store={storeRef.current}>{children}</Provider>
}

导出一个获取方法,以便 React 组件外的代码可以使用 Redux。

// ...
let reduxStore: reduxStoreType | null = null
export default function ReduxProvider (/* ... */) {// ...storeRef.current = reduxStore = configureStore(initialState)// ...
}
export const getStore = () => reduxStore

在 src/app/layout.tsx 中使用这个ReduxProvider。到这里,其实服务端初始化 Redux 已经完成了。

import { getArticleData } from "@/request/ssr/article";
import RootLayoutInner from "./layoutInner";
import ReduxProvider from "@/redux/reduxProvider";export default async function RootLayout ({children,
}: Readonly<{children: React.ReactNode;
}>) {const data = {article: await getArticleData()}return <html lang="en"><head>{/* ... */}</head><body><ReduxProvider data={data}><RootLayoutInner >{ children }</RootLayoutInner></ReduxProvider></body></html>
}

getArticleData函数获取服务端的数据传进了ReduxProvider中,成为了 Redux 的初始状态,服务端已经可以渲染出具有 Redux 初始状态的页面了。

来看看现在的效果,emmmmmm... 似乎并不太好:

看看控制台... 给了我们几个水合错误。

Uncaught Error: Text content does not match server-rendered HTML.
Warning: Text content did not match. Server: "53" Client: "0"

可以发现服务端传回来的 HTML 是有数据的,但是客户端渲染时并没有拿到数据。接下来客户端就需要同步这个状态了。

🎨客户端同步 Redux 状态

我们可以通过<script>标签,在客户端把初始状态挂到window.DATA_TO_SYNC_REDUX上。在客户端环境中,直接从这里取初始化的值。

// ...
const id = 'redux-initializer-json-data'
export default function ReduxProvider (/* ... */) {children: React.ReactNode, data: any
}) {const storeRef = useRef<reduxStoreType | null>(null)let initialStateif (!BROWSER_ENV) {initialState = data} else {try {// @ts-ignoreinitialState = JSON.parse(window.DATA_TO_SYNC_REDUX)} catch (error) {logger.log(error)initialState = {}}}if (!storeRef.current) {storeRef.current = reduxStore = configureStore(initialState)}const text = `window.DATA_TO_SYNC_REDUX=\`${(JSON.stringify(data))}\``BROWSER_ENV && setTimeout(() => {document.getElementById(id)?.remove()}, 100)return [<script key={id} id={id}>{text}</script>,<Provider key='redux-provider' store={storeRef.current}>{children}</Provider>]
}

看起来到这里已经完成了,页面正常运行,但是一打开控制台,马上就给了我们一大堆报错,(虽然不管也行):

Uncaught Error: Text content does not match server-rendered HTML.
Warning: Text content did not match.
Server: "window.DATA_TO_SYNC_REDUX=`{&quot;article&quot;:{...}}`"
Client: "window.DATA_TO_SYNC_REDUX=`{"article":{...}}`"

看起来我们<script>的代码不知道为什么在服务端被转码了,在客户端第一次渲染渲染时又被转了回来,造成了水合错误。这里搜了一下也没有发现什么优雅的解决方法,我们就手动转码一下,绕开 HTML 的特殊字符。

const tokens: Record<string, string> = {'!lt;': '<','!gt;': '>','!nbsp;': ' ','!amp;': '&','!quot;': '"'
}
const invTokens: Record<string, string> = {'<': '!lt;','>': '!gt;',' ': '!nbsp;','&': '!amp;','"': '!quot;'
}
export function pseudoHtml2Escape (htmlString: string) {return htmlString.replace(/(!(lt|gt|nbsp|amp|quot);)/ig, function (t: string) {return tokens[t]})
}
export function escape2PseudoHtml (escapeString: string) {const res = escapeString.replace(/(<|>| |&|")/g, function (_, t: string) {return invTokens[t]})return res
}export default function ReduxProvider (/* ... */) {// ...initialState = JSON.parse(pseudoHtml2Escape(window.DATA_TO_SYNC_REDUX))// ...const text = `window.DATA_TO_SYNC_REDUX=\`${escape2PseudoHtml(JSON.stringify(data))}\``// ...
}

到这里同步服务端状态已经完成了。来看看最终效果:

🎁结语

本文简单地实现了 Next.js App Router 下客户端同步服务端 Redux 状态的方法。其中状态的传递主要通过 HTML 代码来进行,总感觉是不是不太优雅。大体流程如下所示:

graph LR subgraph "服务端渲染"RootLayout -->|传入数据|B[ReduxProvider]-->|初始化|ReduxB -->|传入初始值| script end subgraph "浏览器"script -->|记录初始值| windowReduxProvider --> |获取数据|windowReduxProvider-->|初始化|A[Redux] end

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/835606.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Git版本管理系统快速上手指南

作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任。 目录一.git命令初体验1.搭建Golang开发环境2.git init项目初始化3.git status查看工作目录状态4.git add将文件从工作区提交到暂存区5.git config配置git个人信息6.git commit提交代码到本地仓库7.git log查看…

Air201模组入门:掌握SPI读写外部Flash的技巧

今天带领大家通过Air201+扩展板读写外部flash的演示,教你使用SPI示例,可根据实际需求灵活应用。今天带领大家通过Air201+扩展板读写外部flash的演示,教你使用SPI示例,可根据实际需求灵活应用。 我们先了解一些相关基础知识: SPI(Serial Peripheral Interface)——是一种…

第七章习题

学号后四位:3018 7.3:点击查看代码 import numpy as np import matplotlib.pyplot as plt from scipy.interpolate import interp1d, CubicSplineT = np.array([700, 720, 740, 760, 780]) V = np.array([0.0977, 0.1218, 0.1406, 0.1551, 0.1664])# 线性插值 linear_interp = …

6502 指令译码器

引言 CPU 要执行指令需要先识别指令,弄清楚要执行的指令是什么类型、需要几个周期、操作数在哪里、目的地在哪里等信息,才能在后续的指令执行过程中打开对应的数据通路。“识别指令”的过程叫译码,完成指令识别功能的机构,叫译码器。 两个译码器 因为 6502 CPU 有一个两级流…

随笔5

这个作业属于哪个课程 计科22级34班这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13234这个作业的目标 完成昨天的计划总结:问题 答案昨天完成的工作 完成个人中心和发表博客页面今天计划完成的工作 完成分类模块以及登录部分的接口对接遇到…

随笔4

这个作业属于哪个课程 计科22级34班这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13234这个作业的目标 完成昨天的计划总结:问题 答案昨天完成的工作 完成首页的搭建今天计划完成的工作 完成个人中心和发表博客页面遇到的困难 发表页面需要…

Nukem pg walkthrough Intermediate

nmap 扫描 ┌──(root㉿kali)-[~] └─# nmap -p- -A 192.168.157.105 Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-17 12:32 UTC Nmap scan report for 192.168.157.105 Host is up (0.071s latency). Not shown: 65529 filtered tcp ports (no-response) PORT …

Scrum冲刺-Day4

Scrum冲刺-Day4 1站立式会议 1.1站立会议照片1.2会议纪要 - Day4成员姓名 昨天已完成的工作 今天计划完成的工作 工作中遇到的困难张易欣 订单模块接口的开发 实现对用户的请求和响应进行处理 需要解决前端和后端之间的数据传递和同步问题苏清仪 分类模块接口的开发 业务逻辑处…

第七天 项目冲刺

情况 前端提出浏览器访问出现错误的问题 定位发现是跨域问题先有前端通过配置代理也能通过,但还是决定再后端也配置一下解决跨域问题由于使用了springSecurity,所有还要在security中开启这个功能 前后端沟通时发现他对我的分类的理解完全不一样,而是类似于这样的于是按照他的…

Scrum冲刺-Day3

1.站立式会议 1.1站立会议照片1.2会议纪要成员姓名 昨天已完成的工作 今天计划完成的工作 工作中遇到的困难张易欣 管理员模块接口的开发 订单模块接口的开发 暂无苏清仪 商品模块接口开发 分类模块接口的开发 暂无李心怡 登录注册 购物车功能的逻辑设计与ui图 暂无郑梦翰 确定…

Scrum冲刺-Day2

1站立式会议 1.1站立会议照片1.2会议纪要成员姓名 昨天已完成的工作 今天计划完成的工作 工作中遇到的困难张易欣 后端用户模块 管理员模块接口开发 需要更深入学习数据库知识苏清仪 项目环境搭建 商品模块接口开发 暂无李心怡 前端接口设计 登录注册 暂无郑梦翰 前端接口设计 …