在日常客戶端的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框架就搭建的差很少了,可是還有一些內容須要補充,以後會繼續更新的。加油吧!