原理实践 SSR中redux的使用

原理实践 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 中的规范化数据

快速上手

创建

  1. 创建目录,按照规范或自我习惯进行进行创建

    store
    ├─ index.js
    └─ slices
           ├─ home.js
           └─ personal.js
    
  2. 根据不同的业务创建不同的仓库片端

    // 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
    
  3. 创建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
    
  4. 嵌套在组件较外层(前后端路由都需嵌套)

    // 服务端
        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判断是否暴露该方法判断服务端是否需要进行异步请求

  1. 我们给需要ssr渲染的页面添加静态方法用于获取数据,用于判断是否需要数据是否需要SSR渲染

    Home.getInitialData = async (store) => {
        return store.dispath(fetchHomeData())
    }
    
  2. 导出路由说明

    export const routesConfig = [
        {
            path:'/',
            component:Home
        },
        {
            path:'/personal',
            component:Personal
        }
    ]
    
  3. 服务端处理

    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不一致

  4. 修改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)
    })

})

重要步骤:

  1. 异步操作结束后,store中的数据都将被更新
  2. const preloadedState = store.getState()获取更新后的值
  3. 通过<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>

    )
)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值