react服務端渲染實踐2: 數據預渲染

前言

上一章節,咱們瞭解了react 服務端渲染的工程化配置,這一章節講解如何進行數據預渲染,這是服務端渲染的核心目的。javascript

如下示例中的代碼有部分刪減,具體以 git 倉庫中的代碼爲準php

redux

不管是 react 的服務端渲染數據預取,仍是 vue 的服務端渲染數據預取,都離不開狀態管理工具,這裏使用到的是 redux,若是不懂 redux 的使用,建議先學習後再閱讀此文章,本篇不具體講解 redux 的使用。css

首先創建 store 目錄,用於存放 redux 相關文件,目錄結構以下:html

image

使用了兩個入口區分服務端以及客戶端 store,其中文件內容以下:前端

// index.ts

import { combineReducers } from 'redux';
import indexPage from './modules/index'
import classificationsPage from './modules/classifications'
import goodsDetailPage from './modules/goods_detail'

export const rootReducer = combineReducers({
    indexPage,
    classificationsPage,
    goodsDetailPage
})
複製代碼

index.ts 文件將 redux 模塊進行了合併。vue

// client.ts

import { createStore } from 'redux'
import { rootReducer } from './index'

export default createStore(rootReducer, window.INIT_REDUX_STORE || {})
複製代碼

client.ts 返回了一個 store 對象,其中初始化的數據來自 window.INIT_REDUX_STORE,這個後文會詳細講解。java

// server.ts

import { createStore } from 'redux'
import { rootReducer } from './index'

export function createServerStore() {
    return createStore(rootReducer)
}
複製代碼

server.ts 返回了一個建立store的工廠函數,由於每次請求都須要一個全新的 store 存儲數據,不能被以前的請求污染。react

入口文件的變化

而後修改以前的入口文件以下:git

// app.tsx

import * as React from 'react'
import { Switch } from 'react-router-dom'
import { renderRoutes } from 'react-router-config'
import Login from './src/view/login'
import Main from './src/view/main/router'
import GoodsDetail from './src/view/goods_detail/goods_detail'
import NotFoundPage from './src/view/404/404'

export const routes = [
    Main, 
{
    path: '/app/login',
    component: Login,
    exact: true
}, {
    path: '/app/goodsDetail/:id',
    component: GoodsDetail,
    exact: true
}, {
    component: NotFoundPage
}]

interface componentProps {
}

interface componentStates {
}

class App extends React.Component<componentProps, componentStates> {
    constructor(props) {
        super(props)
    }

    render() {
        return (
            <Switch> {renderRoutes(routes)} </Switch>
        )
    } 
}

export default App

複製代碼

這裏單獨將 routes 導出是由於服務端渲染須要用到這個 routes 配置,其他與以前相比無變化。github

// client.entry.tsx

import * as React from 'react'
import { hydrate } from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import App from './app'
// import JssProvider from 'react-jss/lib/JssProvider';
import { Provider as ReduxProvider } from 'react-redux'
import store from '@src/store/client'

hydrate(<ReduxProvider store={store}> <BrowserRouter> <App/> </BrowserRouter> </ReduxProvider>, document.getElementById('root'))
複製代碼

客戶端入口與以前相比變化不大,添加了 ReduxProvider

// server.entry.tsx

import * as React from 'react'
const { renderToString } = require("react-dom/server")
import { matchRoutes } from 'react-router-config'
import { StaticRouter } from "react-router"
import { Provider as ReduxProvider } from 'react-redux'
import { createServerStore } from '@src/store/server'
import App, { routes } from './app'


export default async (req, context = {}) => {
    const store = createServerStore()
    const matchedRoute = matchRoutes(routes, req.url)
    return await Promise.all(
        matchedRoute
            .filter(value => (value.route.component as any).getAsyncData)
            .map(value => (value.route.component as any).getAsyncData(store, req))
    ).then(() => {
        const sheets = new ServerStyleSheets();
        return {
            store: store.getState(),
            html: renderToString(
                sheets.collect(
                    <ReduxProvider store={store}> <StaticRouter location={req.url} context={context}> <App/> </StaticRouter> </ReduxProvider>,
                  ),
            )
        }
    })
}
複製代碼

服務端入口代碼修改較多,這裏詳細進行講解。

路由配置

服務端渲染由於涉及到數據預取,因此沒法像客戶端渲染那樣使用動態路由,只能使用靜態路由進行配置,因此須要使用 react-router-config 配置一個路由對象:

// app.tsx
import Main from './src/view/main'
import Index from './src/view/index/index'
import Login from './src/view/login'
import Classifications from './src/view/classifications/classifications'
import Cart from './src/view/cart/cart'
import GoodsDetail from './src/view/goods_detail/goods_detail'
import NotFoundPage from './src/view/404/404'

export const routes = [
{
    path: '/app/main',
    component: Main,
    routes: [{
        path: '/app/main/index',
        component: Index
    }, {
        path: '/app/main/classifications',
        component: Classifications
    }, {
        path: '/app/main/cart',
        component: Cart
    }]
}, 
{
    path: '/app/login',
    component: Login,
    exact: true
}, {
    path: '/app/goodsDetail/:id',
    component: GoodsDetail,
    exact: true
}, {
    component: NotFoundPage
}]
複製代碼

在組件中須要使用 renderRoutes 函數渲染路由:

import { renderRoutes } from 'react-router-config'

render() {
    return (
        <div> {renderRoutes(this.props.route.routes)} </div>
    )
} 
複製代碼

請求數據

當靜態路由配置完成,每次服務端接收到請求,均可以經過請求的url找到須要渲染的頁面,這裏用到的是 matchRoutes 函數,示例以下:

import { routes } from 'app.tsx'
import { matchRoutes } from 'react-router-config'
export default async (req, context = {}) => {
    const matchedRoute = matchRoutes(routes, req.url)
}
複製代碼

既然知道了哪一個頁面須要渲染,那麼指定數據的獲取就好辦了。

在路由組件中定義靜態函數 getAsyncData, 接收一個 store 對象,這個 store 就是 redux 的 store。返回一個 promise 函數,用於數據的請求,以及後續步驟的處理,在數據請求完成後,向 store 派發更新,將獲取到的數據存儲在 store 中。

class Classifications extends React.Component<IProps & RouteComponentProps, IStates> {
    static getAsyncData(store) {
        return fetch.getClassifications({
            page: 0,
            limit: 10000
        }, ` id name products { id name thumbnail intro price } `).then(data => {
            store.dispatch({
                type: 'SET_CLASSIFICATIONS_DATA',
                value: data.classifications.rows
            })
        }).catch(e => {

        })
    }
}
複製代碼

那麼在接收到請求時,是如何處理的呢?接下來將如下代碼拆分進行講解。

// server.entry.tsx

import * as React from 'react'
const { renderToString } = require("react-dom/server")
import { matchRoutes } from 'react-router-config'
import { StaticRouter } from "react-router"
import { Provider as ReduxProvider } from 'react-redux'
import { createServerStore } from '@src/store/server'
import App, { routes } from './app'


export default async (req, context = {}) => {
    const store = createServerStore()
    const matchedRoute = matchRoutes(routes, req.url)
    return await Promise.all(
        matchedRoute
            .filter(value => (value.route.component as any).getAsyncData)
            .map(value => (value.route.component as any).getAsyncData(store, req))
    ).then(() => {
        return {
            store: store.getState(),
            html: renderToString(
                    <ReduxProvider store={store}> <StaticRouter location={req.url} context={context}> <App/> </StaticRouter> </ReduxProvider>,
                  ),
        }
    })
}
複製代碼

首先經過 matchRoutes 找到與 url 匹配的路由組件。

const matchedRoute = matchRoutes(routes, req.url)
複製代碼

其次對於匹配的組件進行過濾,而後調用組件類的靜態函數, 將建立的 store 做爲參數傳入 getAsyncData 函數中,用於 store 的更新。

const store = createServerStore()
Promise.all(
    matchedRoute
        .filter(value => (value.route.component as any).getAsyncData)
        .map(value => (value.route.component as any).getAsyncData(store, req))
)
複製代碼

當 Promise.all 中的請求都完成後,store 中的數據已經更新成須要渲染的數據。最終將 store 傳入 ReduxProvider 進行渲染,獲得填充完數據的 html 字符串返回給客戶端。

return {
    store: store.getState(),
    html: renderToString(
            <ReduxProvider store={store}> <StaticRouter location={req.url} context={context}> <App/> </StaticRouter> </ReduxProvider>,
            ),
}
複製代碼

至於組件中如何使用 store 數據這裏不細說。

最後再看看服務端的代碼:

const path = require('path')
const serverEntryBuild = require('./dist/server.entry.js').default
const ejs = require('ejs')
const express = require('express')

module.exports = function(app) {
    app.use(express.static(path.resolve(__dirname, './static')));
    app.use(async (req, res, next) => {
        const reactRenderResult = await serverEntryBuild(req)
        ejs.renderFile(path.resolve(process.cwd(), './app/server/dist/index.ejs'), {
            store: JSON.stringify(reactRenderResult.store),
            html: reactRenderResult.html
        }, {
            delimiter: '?',
            strict: false
        }, function(err, str){
            if (err) {
                console.log(err)
                res.status(500)
                res.send('渲染錯誤')
                return
            }
            res.end(str, 'utf-8')
        })
    });
}
複製代碼

這裏在收到http請求後,調用服務端渲染函數,傳入當前 request 對象,服務端入口根據 request url渲染對應頁面,返回一個填充了數據的 html 字符串,以及 store 中的 state,接着使用 ejs 渲染這兩個數據返回到客戶端,完成一次服務端渲染,ejs模版 內容以下:

<!DOCTYPE html>
<html lang="zh">
    <head>
        <title>My App</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no">
        <script> window.INIT_REDUX_STORE = <?- store ?> </script>
        <link href="./static/main.e61e8228d12dd0cba063.css" rel="stylesheet"></head>
        <body>
        <div id="root"><?- html ?></div>
        <script type="text/javascript" src="./static/commons.d50e3adb1df381cdc21b.js"></script>
        <script type="text/javascript" src="./static/bundle.e61e8228d12dd0cba063.js"></script></body>
</html>
複製代碼

須要注意的是將 store 中的 state 也返回給了前端,在前端初始化時,將這份數據填充進客戶端的 store 中供客戶端使用:

<script> window.INIT_REDUX_STORE = <?- store ?> </script>
複製代碼
// 客戶端 store 入口
import { createStore } from 'redux'
import { rootReducer } from './index'

export default createStore(rootReducer, window.INIT_REDUX_STORE || {})

複製代碼

目的在於前端能夠根據這些數據判斷服務端渲染是否成功,以及是否須要再次請求數據,具體邏輯以下:

componentDidMount() {
    if (this.props.classifications.length === 0) { // 若是 store 中不存在相應的數據,那麼請求數據,不然不請求
        this.getClassificationsData()
    }
}
複製代碼

到這裏,react ssr 數據預渲染就完成了,如下倉庫提供完整代碼以供參考。

倉庫地址: github.com/Richard-Cho…

相關文章
相關標籤/搜索