React服务端渲染store和router的同步

本文围绕React服务端渲染展开,介绍了服务端渲染的路由和store同步。路由方面,客户端用BrowserRouter,服务端用StaticRouter。store同步是难点,对于首屏需展示的数据,可通过react-async-bootstrapper库、改造store、获取服务端store并借助ejs模板引擎注入客户端来实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

store采用的是mobx作为状态管理,mobx使用起来确实是比redux简单,没有那么多繁琐的配置。router是react-router4.2。其实实现简单的服务端渲染并不难,react服务端渲染的简单配置这里之前有简单介绍了一下。我个人认为主要的难点是在实现store和router在服务端和客户端的同步。

服务端渲染的路由

在实现服务端渲染的路由比较简单,因为官方都给我们提供了api。

BrowserRouter

在客户端渲染的时候,我们一般会采用BrowserRouter作为前端路由。使用 HTML5 History API(pushState,replaceState 和 popstate 事件)的 <Router> 来保持 UI 与 URL 同步。

StaticRouter

服务器端渲染是一种无状态的渲染。基本的思路是,将<BrowserRouter>替换为无状态的<StaticRouter>。将服务器上接收到的URL传递给路由用来匹配,同时支持传入context特性。

// client
<BrowserRouter>
  <App/>
</BrowserRouter>

// server (not the complete story)
<StaticRouter
  location={req.url}
  context={context}
>
  <App/>
</StaticRouter>

当在浏览器上渲染一个<Redirect>时,浏览器历史记录会改变状态,同时将屏幕更新。在静态的服务器环境中,无法直接更改应用程序的状态。在这种情况下,可以在context特性中标记要渲染的结果。如果出现了context.url,就说明应用程序需要重定向。从服务器端发送一个恰当的重定向链接即可。

            <StaticRouter context={routerContext} location={url}>
                <App />
            </StaticRouter>
        const routerContext = {}
        const stores = createStoreMap()
        const App = createApp(stores, routerContext, req.url) 
        asyncBootstrap(App).then(() => {
            //bootstrap异步方法执行完毕后,执行完余下的渲染方法后,执行此回调。此时的App就是已经插好值的
            if (routerContext.url) {
                res.status(302).setHeader('Location', routerContext.url);
                res.end()
                return
            }

store的同步

我个人认为store的同步是服务端渲染的难点之一。

异步请求

我们在做服务端渲染的时候,有一些服务器请求到的数据是需要在首屏就可以看到的。那么这个请求的操作最好就是在服务端渲染的时候就拿到了,而不是来到客户端渲染的时候才进行请求。

1、使用react-async-bootstrapper这个库,把我们服务端渲染的组件包装起来,先执行异步方法,执行完毕后再进行余下的ssr渲染。我们在组件内部定义一个bootstrap()的异步方法,这个就代表我们先要执行的异步操作。

 class App extends React.Component {
     bootstrap() {
         return new Promise((resolve) => {
             setTimeout(() => {
                 this.props.appState.count = 3
                 resolve(true)
             })
         })
     }
    render(){
        return(
            <div className="app">
             
                    <Link to="/">首页</Link>
                    <Link to="/detail">详情页</Link>
                
                <Routes />
            </div>
        )
    }
}
const asyncBootstrap = require('react-async-bootstrapper')     
   asyncBootstrap(App).then(() => {
            //bootstrap异步方法执行完毕后,执行完余下的渲染方法后,执行此回调。此时的App就是已经插好值的
            if (routerContext.url) {
                res.status(302).setHeader('Location', routerContext.url);
                res.end()
                return
            }
           
            const helmet = Helmet.rewind()
            const state = getStoreState(stores)
            const content = ReactDomServer.renderToString(App);
            //res.send(template.replace('<!--app-->', content))
            // console.log('helmet', new helmet())
            console.log('initialState', state)
            const html = ejs.render(template, {
                appString: content,
                initialState: serialize(state),
                meta: helmet.meta.toString(),
                title: helmet.title.toString(),
                style: helmet.style.toString(),
                link: helmet.link.toString(),

            })
            res.send(html)
            resolve()
        })

2、对store进行改造

App-state.js

export default class AppState {
    constructor({count,name} = {count:0,name:'bb'}){
        this.count=count
        this.name=name
    }
    @observable count       
    @observable name 
    @computed get msg(){
        return `${this.name} say count is ${this.count}`
    }
    @action add() {
        this.count += 1
    }
    @action changeName(name){
        this.name = name
    }

    //此方法用于ssr服务端渲染时调用,获取当前服务端渲染时的store状态,注入到客户端,使得服务端和客户端的store可以同步
    toJson(){
        return {
            count:this.count,
            name:this.name
        }
    }

}
export const AppState = AppStateClass

export default {
    AppState,
}


//此函数专门用于SSR,
export const createStoreMap = () => {
    return {
        appState: new AppState(),
    }
}

把app-state封装成一个类,方便把服务端渲染时的store获取到,注入给客户端。

3、获取服务端渲染后的store

const getStoreState = (stores) => {
    return Object.keys(stores).reduce((result, storeName) => {
        result[storeName] = stores[storeName].toJson()
        return result
    }, {})
}


//bundle是webpack对服务端渲染打包后的代码,再获取里面的createStoreMap获取到当前store实例
 const createStoreMap = bundle.createStoreMap
 const state = getStoreState(stores)

//这样,state获取到就是通过服务端渲染时的store

4、使用模板引擎,把服务端获取到的store注入到客户端,实现同步

这里使用的是ejs模板引擎。

首先,要对客户端的入口文件进行修改,使得客户端是从全局变量当中获取到store的:

const root = document.getElementById('root');
const initialState = window.__INITIAL__STATE__ || {} 

const render = Component => {
    const renderMethod = module.hot ? ReactDom.render : ReactDom.hydrate;
    ReactDom.hydrate(
        <AppContainer>
            <Provider appState={new AppState(initialState.appState)}>
        <BrowserRouter>
          <Component />  
        </BrowserRouter>
        </Provider>
        </AppContainer>
    ,root);
}
render(App);

把服务端渲染后的代码转化为html字符串后,使用ejs模板引擎,把html内容、initialState(就是服务端的store)插入到html模板。

            const state = getStoreState(stores)
            const content = ReactDomServer.renderToString(App);
            const html = ejs.render(template, {
                appString: content,
                initialState: serialize(state),
                meta: helmet.meta.toString(),
                title: helmet.title.toString(),
                style: helmet.style.toString(),
                link: helmet.link.toString(),

            })
            res.send(html)

html模板要改为ejs的模板

server.template.ejs

<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
    <%%- meta %>
    <%%- title %>
    <%%- link %>
    <%%- style %>
</head>

<body>
    <div id="root">
        <%%- appString %>
    </div>
    <script>
        window.__INITIAL__STATE__ = <%%- initialState %>
    </script>
</body>

</html>

 

到此,比较完整的服务端渲染就完成了~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值