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框架就搭建的差很少了,可是还有一些内容须要补充,以后会继续更新的。加油吧!

相关文章
相关标签/搜索