原理实践 SSR中redux的使用
安装
yarn add @reduxjs/toolkit
@reduxjs/toolkit 主要API
●configureStore(): 包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供的任何 Redux 中间件,thunk中间件默认包含,并启用 Redux DevTools Extension。
●createReducer():这使您可以为 case reducer 函数提供操作类型的查找表,而不是编写 switch 语句。此外,它自动使用该immer库让您使用普通的可变代码编写更简单的不可变更新,例如state.todos[3].completed = true.
●createAction():为给定的动作类型字符串生成动作创建函数。该函数本身已toString()定义,因此可以使用它来代替类型常量。
●createSlice():接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的动作创建者和动作类型。
●createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分派动作类型的 thunk
●createEntityAdapter: 生成一组可重用的 reducer 和 selector 来管理 store 中的规范化数据
快速上手
创建
-
创建目录,按照规范或自我习惯进行进行创建
store ├─ index.js └─ slices ├─ home.js └─ personal.js
-
根据不同的业务创建不同的仓库片端
// store/slices/home import { createSlice,createAsyncThunk } from "@reduxjs/toolkit"; // 初始值 const initialState = { articles:[], age:18 } // 异步的action使用createAsyncThunk // 第一个参数,用于生成额外Redux action常量的字符串,表示异步请求的生命周期: // 第二个参数是一个回调函数,返回值将被作为payload export const fetchHomeData = createAsyncThunk('home/fetch', async (dispath) => { // 模拟假数据 const data = await new Promise((resolve,reject) => { setTimeout(() => { resolve({ articles:[ { id:1, title:'文章标题1', content:'文章内容1' },{ id:2, title:'文章标题2', content:'文章内容2' } ] }) },2000) }) return data }) const homeSlice = createSlice({ name:'homeSlice', // 类似于命名空间,(取个名字) initialState, // 初始状态 reducers:{ changeAge(state,action){ state.age = action.payload } }, // 针对异步action做的监听处理 // 1、fulfilled 成功之后需要做的操作 // 2、pending 加载时需要做的操作 // 3、rejected失败后需要做什么处理 extraReducers:(builder) => { builder.addCase(fetchHomeData.fulfilled, (state,action) => { state.articles = action.payload && action.payload.articles; }) } }) export const {changeAge} = homeSlice.actions export default homeSlice.reducer
-
创建store
// store/index import { configureStore } from '@reduxjs/toolkit' import homeReducer from './slices/home' import personalReducer from "./slices/personal" const store = configureStore({ reducer:{ home:homeReducer, personal:personalReducer } }) export default store
-
嵌套在组件较外层(前后端路由都需嵌套)
// 服务端 const content = ReactDOMServer.renderToString( <Provider store={store}> <StaticRouter location={req.url}> <RoutesList/> </StaticRouter> </Provider> )
// 客户端 hydrateRoot( document.querySelector('#root'), ( <Provider store={store}> <BrowserRouter> <RoutesList/> </BrowserRouter> </Provider> ) )
使用
import React, { useEffect } from "react";
import {useSelector,useDispatch} from "react-redux"
import {fetchHomeData,changeAge} from "../store/slices/home"
const Home = () => {
const dispath = useDispatch()
const homeData = useSelector((state) => state.home) // 获取到home片段的数据
useEffect(() => {
dispath(fetchHomeData()) // 调用异步action
},[])
console.log(homeData)
const handleClick = () => {
dispath(changeAge(5)) // 调用action
console.log('点击')
}
return(
<div>
<h1>首页</h1>
<ul>
{
homeData?.articles.map((article) => {
return(
<li key={article.key}>
<p>{article?.title}</p>
<p>{article?.content}</p>
</li>
)
})
}
</ul>
<button onClick={handleClick}>点我</button>
</div>
)
}
export default Home
redux在SSR中的使用
以上代码有一处异步请求,但是我们通过运行代码发现,这是客户端发起请求后渲染上的,而不是通过SSR渲染后展现在客户端上的
useEffect(() => {
dispath(fetchHomeData()) // 调用异步action
},[])
网页源码:
<html>
<head></head>
<body>
<div id="root">
<div>
<ul>
<li><a href="/">首页</a></li>
<li><a href="/personal">个人中心</a></li>
</ul>
<div>
<h1>首页</h1>
<ul></ul>
<button>点我</button>
</div>
</div>
</div>
<script src="bundle_client.js"></script>
</body>
</html>
我们接下来会将redux与SSR相结合:
分析Next中怎么使用SSR:
我们会在使用到SSR渲染的页面中会暴露出一个异步函数
getServerSideProps
,那么Next判断是否暴露该方法判断服务端是否需要进行异步请求
-
我们给需要ssr渲染的页面添加静态方法用于获取数据,用于判断是否需要数据是否需要SSR渲染
Home.getInitialData = async (store) => { return store.dispath(fetchHomeData()) }
-
导出路由说明
export const routesConfig = [ { path:'/', component:Home }, { path:'/personal', component:Personal } ]
-
服务端处理
app.get('*', (req,res) => { const promises = routesConfig?.map(route => { const component = route?.component if(route.path === req?.url && component?.getInitialData) { return component?.getInitialData(store) } else { return null } }) Promise.all(promises).then(() => { const content = ReactDOMServer.renderToString( <Provider store={store}> <StaticRouter location={req.url}> <RoutesList/> </StaticRouter> </Provider> ) const html = ` <html> <head></head> <body> <div id="root">${content}</div> <script src="bundle_client.js"></script> </body> </html> ` // console.log(html) res.send(html) }) })
此时我们观察网页,有以下几点发现:
-
页面中列表渲染的内容并没有出现
{ homeData?.articles.map((article) => { return( <li key={article.key}> <p>{article?.title}</p> <p>{article?.content}</p> </li> ) }) }
-
控制台中出现了相应的错误信息
Warning: Did not expect server HTML to contain a <li> in <ul>. Error: Hydration failed because the initial UI does not match what was rendered on the server. 注水失败是因为初始的UI和服务器上呈现的UI不匹配
-
查看网页源代码(数据已经渲染且没有错误)
<html> <head> </head> <body> <div id="root"> <div> <ul> <li> <a href="/">首页</a> </li> <li> <a href="/personal">个人中心</a> </li> </ul> <div> <h1>首页</h1> <ul> <li> <p>文章标题1</p> <p>文章内容1</p> </li> <li> <p>文章标题2</p> <p>文章内容2</p> </li> </ul> <button>点我</button> </div> </div> </div> <script src="bundle_client.js"></script> </body> </html>
这是因为客户端和服务端
store
的数据不一致,导致客户端的UI与服务端返回的UI不一致 -
-
修改store的注册方式
// 工厂函数,每次使用都会创建一个新的实例
export default function createStoreInstance(preloadedState = {}) {
return configureStore({
reducer:{
home:homeReducer,
personal:personalReducer
},
preloadedState
})
}
修改服务端中的使用:
import createStoreInstance from './store'
app.get('*', (req,res) => {
// 新添加
const store = createStoreInstance()
const promises = routesConfig?.map(route => {
const component = route?.component
if(route.path === req?.url && component?.getInitialData) {
return component?.getInitialData(store)
} else {
return null
}
})
Promise.all(promises).then(() => {
// 新添加
const preloadedState = store.getState()
const content = ReactDOMServer.renderToString(
<Provider store={store}>
<StaticRouter location={req.url}>
<RoutesList/>
</StaticRouter>
</Provider>
)
const html = `
<html>
<head></head>
<body>
<div id="root">${content}</div>
<script>
<!-- 新添加 -->
window.__PRELOAD_STATE__ = ${JSON.stringify(preloadedState)}
</script>
<script src="bundle_client.js"></script>
</body>
</html>
`
// console.log(html)
res.send(html)
})
})
重要步骤:
- 异步操作结束后,store中的数据都将被更新
const preloadedState = store.getState()
获取更新后的值- 通过
<script>
将数据置入客户端中
修改客户端中的使用:
import React from "react";
import { hydrateRoot } from 'react-dom/client';
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom"
import RoutesList from "./routes";
import createStoreInstance from "./store";
// 获取到最新的store数据
const store = createStoreInstance(window?.__PRELOAD_STATE__)
// 类似render方法,但是基于ssr,只是恢复原本已经存在的DOM节点
hydrateRoot(
document.querySelector('#root'),
(
<Provider store={store}>
<BrowserRouter>
<RoutesList/>
</BrowserRouter>
</Provider>
)
)