React服務端渲染探祕:4.異步數據的服務端渲染方案(數據注水與脫水)

1、問題引入

在日常客戶端的React開發中,咱們通常在組件的componentDidMount生命週期函數進行異步數據的獲取。可是,在服務端渲染中卻出現了問題。javascript

如今我在componentDidMount鉤子函數中進行Ajax請求:html

import { getHomeList } from './store/actions'
  //......
  componentDidMount() {
    this.props.getList();
  }
  //......
  const mapDispatchToProps = dispatch => ({
    getList() {
      dispatch(getHomeList());
    }
})
複製代碼
//actions.js
import { CHANGE_LIST } from "./constants";
import axios from 'axios'

const changeList = list => ({
  type: CHANGE_LIST,
  list
})

export const getHomeList = () => {
  return dispatch => {
    //另外起的本地的後端服務
    return axiosInstance.get('localhost:4000/api/news.json')
      .then((res) => {
        const list = res.data.data;
        dispatch(changeList(list))
      })
  }
}
//reducer.js
import { CHANGE_LIST } from "./constants";

const defaultState = {
  name: 'sanyuan',
  list: []
}

export default (state = defaultState, action) => {
  switch(action.type) {
    case CHANGE_LIST:
      const newState = {
        ...state,
        list: action.list
      }
      return newState
    default:
      return state;
  }
}
複製代碼

好,如今啓動服務。java

如今頁面可以正常渲染,可是打開網頁源代碼。

源代碼裏面並無這些列表數據啊!那這是爲何呢?

讓咱們來分析一下客戶端和服務端的運行流程,當瀏覽器發送請求時,服務器接受到請求,這時候服務器和客戶端的store都是空的,緊接着客戶端執行componentDidMount生命週期中的函數,獲取到數據並渲染到頁面,然而服務器端始終不會執行componentDidMount,所以不會拿到數據,這也致使服務器端的store始終是空的。換而言之,關於異步數據的操做始終只是客戶端渲染。react

如今的工做就是讓服務端將得到數據的操做執行一遍,以達到真正的服務端渲染的效果。ios

2、改造路由

在完成這個方案以前須要改造一下原有的路由,也就是routes.jsjson

import Home from './containers/Home';
import Login from './containers/Login';

export default [
{
  path: "/",
  component: Home,
  exact: true,
  loadData: Home.loadData,//服務端獲取異步數據的函數
  key: 'home'
},
{
  path: '/login',
  component: Login,
  exact: true,
  key: 'login'
}
}];
複製代碼

此時客戶端和服務端中編寫的JSX代碼也發生了相應變化redux

//客戶端
//如下的routes變量均指routes.js導出的數組
<Provider store={store}>
  <BrowserRouter> <div> { routers.map(route => { <Route {...route} /> }) } </div> </BrowserRouter> </Provider>
複製代碼
//服務端
<Provider store={store}>
  <StaticRouter> <div> { routers.map(route => { <Route {...route} /> }) } </div> </StaticRouter> </Provider>
複製代碼

其中配置了一個loadData參數,這個參數表明了服務端獲取數據的函數。每次渲染一個組件獲取異步數據時,都會調用相應組件的這個函數。所以,在編寫這個函數具體的代碼以前,咱們有必要想清楚如何來針對不一樣的路由來匹配不一樣的loadData函數。axios

在server/utils.js中加入如下邏輯後端

import { matchRoutes } from 'react-router-config';
  //調用matchRoutes用來匹配當前路由(支持多級路由)
  const matchedRoutes = matchRoutes(routes, req.path)
  //promise對象數組
  const promises = [];
  matchedRoutes.forEach(item => {
    //若是這個路由對應的組件有loadData方法
    if (item.route.loadData) {
      //那麼就執行一次,並將store傳進去
      //注意loadData函數調用後須要返回Promise對象
      promises.push(item.route.loadData(store))
    }
  })
  Promise.all(promises).then(() => {
      //此時該有的數據都已經到store裏面去了
      //執行渲染的過程(res.send操做)
  }
  )
複製代碼

如今就能夠安心的寫咱們的loadData函數,其實前面的鋪墊工做作好後,這個函數是至關容易的。api

import { getHomeList } from './store/actions'

Home.loadData = (store) => {
    return store.dispatch(getHomeList())
}
複製代碼
//actions.js
export const getHomeList = () => {
  return dispatch => {
    return axios.get('xxxx')
      .then((res) => {
        const list = res.data.data;
        dispatch(changeList(list))
      })
  }
}
複製代碼

根據這個思路,服務端渲染中異步數據的獲取功能就完成啦。

3、數據的注水和脫水

其實目前作了這裏仍是存在一些細節問題的。好比當我將生命週期鉤子裏面的異步請求函數註釋,如今頁面中不會有任何的數據,可是打開網頁源代碼,卻發現:

數據已經掛載到了服務端返回的HTML代碼中。那這就說明服務端和客戶端的store不一樣步的問題。

其實也很好理解。當服務端拿到store並獲取數據後,客戶端的js代碼又執行一遍,在客戶端代碼執行的時候又建立了一個空的store,兩個store的數據不能同步。

那如何才能讓這兩個store的數據同步變化呢?

首先,在服務端獲取獲取以後,在返回的html代碼中加入這樣一個script標籤:

<script> window.context = { state: ${JSON.stringify(store.getState())} } </script>
複製代碼

這叫作數據的「注水」操做,即把服務端的store數據注入到window全局環境中。 接下來是「脫水」處理,換句話說也就是把window上綁定的數據給到客戶端的store,能夠在客戶端store產生的源頭進行,即在全局的store/index.js中進行。

//store/index.js
import {createStore, applyMiddleware, combineReducers} from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '../containers/Home/store';

const reducer = combineReducers({
  home: homeReducer
})
//服務端的store建立函數
export const getStore = () => {
  return createStore(reducer, applyMiddleware(thunk));
}
//客戶端的store建立函數
export const getClientStore = () => {
  const defaultState = window.context ? window.context.state : {};
  return createStore(reducer, defaultState, applyMiddleware(thunk));
}
複製代碼

至此,數據的脫水和注水操做完成。可是仍是有一些瑕疵,其實當服務端獲取數據以後,客戶端並不須要再發送Ajax請求了,而客戶端的React代碼仍然存在這樣的浪費性能的代碼。怎麼辦呢?

仍是在Home組件中,作以下的修改:

componentDidMount() {
  //判斷當前的數據是否已經從服務端獲取
  //要知道,若是是首次渲染的時候就渲染了這個組件,則不會重複發請求
  //若首次渲染頁面的時候未將這個組件渲染出來,則必定要執行異步請求的代碼
  //這兩種狀況對於同一組件是都是有可能發生的
  if (!this.props.list.length) {
    this.props.getHomeList()
  }
}
複製代碼

一路作下來,異步數據的服務端渲染仍是比較複雜的,可是難度並非很大,須要耐心地理清思路。

至此一個比較完整的SSR框架就搭建的差很少了,可是還有一些內容須要補充,以後會繼續更新的。加油吧!

相關文章
相關標籤/搜索