在日常客户端的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
在完成这个方案以前须要改造一下原有的路由,也就是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))
})
}
}
复制代码
根据这个思路,服务端渲染中异步数据的获取功能就完成啦。
其实目前作了这里仍是存在一些细节问题的。好比当我将生命周期钩子里面的异步请求函数注释,如今页面中不会有任何的数据,可是打开网页源代码,却发现:
其实也很好理解。当服务端拿到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框架就搭建的差很少了,可是还有一些内容须要补充,以后会继续更新的。加油吧!