所有源代碼、文檔和圖片都在 github 的倉庫裏,點擊進入倉庫
相關閲讀
- React服務端渲染之路01——項目基礎架構搭建
- React服務端渲染之路02——最簡單的服務端渲染
- React服務端渲染之路03——路由
- React服務端渲染之路04——redux-01
- React服務端渲染之路05——redux-02
- React服務端渲染之路06——優化
- React服務端渲染之路07——添加CSS樣式
- React服務端渲染之路08——404和重定向
- React服務端渲染之路09——SEO優化
1. redux 與路由優化
- 到目前我們已經實現了服務端的異步獲取數據,但是現在依然還有幾個問題
- 第一,多級路由與路由精確匹配,我們並沒有實現多級路由,而且對路由的校驗也比較簡單,沒有深層次的校驗
- 第二,Promise.all 這個方法,要求的是,裏邊如果有一個方法失敗了,那麼整個 Promise.all 就是失敗的。但是這樣是有問題的,我們是要先通過 Promise.all 獲取到數據並修改 store,然後才進行頁面渲染的。如果 Promise.all 失敗了,那麼就不再往下執行,頁面就一直處於 loading 的狀態,渲染不出頁面,這樣明顯不是我們想要的。我們想要的是,不管 Promise.all 裏邊有幾個失敗請求,都不會影響到我們客户端渲染,同時,如果服務端請求失敗了,store 裏沒有拿到我們想要的值,那麼客户端還可以繼續獲取數據,不影響用户使用,這樣相當於是雙保險
- 那麼接下來,我們就要開始解決這兩個問題
1.1 建立 App 組件
- 在解決這兩個問題之前,我們先一個 App 組件,就像是客户端渲染時的 App 組件,主要是為了做整體頁面的一個入口,就是説,我們進入到每一個頁面及組件的時候,都要通過 App 組件,這樣有助於我們建立多級路由
- 同時,這樣我們還可以把 Header 組件從 src/server/render.js 裏拿出來,放在 App 組件裏,這樣便於代碼的管理
- src/App.js
import React, { Component } from 'react';
import Header from './components/Header';
class App extends Component {
render() {
return (
<div>
<Header />
<div className="container">
</div>
</div>
);
}
}
export default App;
- 那麼我們可以想兩個問題,第一個問題是, container 裏應該放什麼,肯定是路由,因為 Header 是導航,那麼導航下邊肯定要放路由,這樣才能把內容顯示出來
- 第二個問題是,路由怎麼放,我們要建立多級路由,肯定不能像之前那樣採用 routes.map() 這樣的方法顯示路由,那麼該怎麼做,要解決這個問題,我們先修改路由文件
1.2 配置多級路由
- 多級路由的根入口,就是 App 組件,那麼也就是説,根路由就是 App 組件,剩下的 Home 和 News 組件都是子路由,儘管 Home 頁面顯示的也是根路由,但是我們依然把它作為一個子路由
- 接下來,我們修改路由文件
- src/routes.js
// src/routes.js
import React from 'react';
import {Route} from 'react-router-dom';
import App from './App';
import Home from './containers/Home';
import News from './containers/News';
export default [
{
path: '/',
component: App,
key: 'app',
routes: [
{
path: '/',
component: Home,
loadData: Home.loadData,
exact: true,
key: '/'
},
{
path: '/news',
component: News,
exact: true,
key: '/news'
}
]
}
];
1.3 服務端使用多級路由
- 配置完成了,但是我們該怎麼使用呢,這是一個問題,react-router-dom 裏有一個 matchPath 的方法,這個方法可以匹配路由,但是它只能匹配單級路由,多級路由不支持,所以我們不能使用 matchPath
- 有一個庫叫做 react-router-config,這個庫裏邊有兩個方法,一個是 matchRoutes ,另一個是 renderRoutes,這兩個方法一個是匹配路由,一個是渲染路由,剛好可以滿足我們的需要。但是順序我們要先搞清楚,肯定是先匹配路由,匹配完了之後,然後才開始渲染路由
- 下載依賴
npm i react-router-config -S -
首先在服務端匹配多級路由
- 就這一句話,routes 就是在 src/routes.js 配置的新的路由,req.path 就是服務的請求路由,匹配完之後得到的是一個數組對象,每個對象都是所匹配到的路由
- 有一個不一樣的地方就是,匹配完之後得到的 matchedRoutes 裏的結構體不一樣了,loadData 已經不在是屬於單個 item 的,而是 item.route.loadData,其他的沒有什麼需要改變的
// src/server/render.js
import { matchRoutes } from 'react-router-config';
let matchedRoutes = matchRoutes(routes, req.path);
let promises = [];
matchedRoutes.forEach(item => {
let loadData = item.route.loadData;
if (loadData) {
const promise = loadData(store);
promises.push(promise);
}
});
- 然後服務端開始渲染路由,渲染路由就比較簡單,直接調用 renderRoutes 方法,傳入 routes 參數就行
// src/server/render.js
import { renderRoutes } from 'react-router-config';
let domContent = renderToString(
<Provider store={store}>
<StaticRouter context={context} location={req.path}>
{
renderRoutes(routes)
}
</StaticRouter>
</Provider>
);
1.4 客户端使用多級路由
- 客户端使用的多級路由,和服務端區別不大, 不一樣的地方就是客户端不需要匹配路由,直接渲染就可以
import { renderRoutes } from 'react-router-config';
hydrate(<Provider store={store}>
<BrowserRouter>
{renderRoutes(routes)}
</BrowserRouter>
</Provider>, window.root);
1.5 App 組件使用多級路由
- App 組件是客户端和服務端都要使用到的組件,所以這個組件就不太一樣,渲染路由需要傳遞參數
- renderRoutes 這個方法比較有意思,在服務端使用的時候,直接就獲取到了所有匹配到的路由,同時,把匹配到的路由作為屬性值保留在組件的 props 裏,所以,在服務端 {renderRoutes(routes)} 的時候,就已經把匹配到的信息保留了下來。那麼在 App 裏使用的時候,直接調用 props 裏的屬性值就可以了
- 我們先修改代碼,src/App.js
// src/App.js
import React, { Component } from 'react';
import { renderRoutes } from 'react-router-config';
import Header from './components/Header';
class App extends Component {
render() {
console.log(this.props);
return (
<div>
<Header />
<div className="container">
{
renderRoutes(this.props.route.routes)
}
</div>
</div>
);
}
}
export default App;
- 我們在控制枱查看一下 this.props 的值是什麼
- 可以看到,props 裏有路由的三個屬性,history,location 和 match,還有一個靜態屬性 staticContext,這個我們後邊再説。最重要的是 route 屬性,route.routes 裏就是我們定義的路由文件裏的 routes 屬性,所以我們直接使用這個 routes 屬性渲染路由
- 此時我們再進行頁面的切換,服務端異步獲取數據,客户端同步修改數據,都可以正常操作,沒有任何的問題
1.6 解決 Promise.all 的問題
- Promise.all 的問題實際上就是每一個 promise 的問題,而每一個 promise 的問題就是這個 promise 的狀態可能會失敗,那麼我們需要解決的就是,如何把 promise 失敗的狀態,也改為成功的狀態。就算失敗了,不修改 store 的值也沒關係,客户端可以修改,最重要的是不能引起頁面一直 loading 而不渲染頁面
- 所以我們修改 promise 的狀態,把失敗的狀態也改為成功的狀態,這樣就解決了 promise 失敗的狀態,可以嘗試一下把接口改為一個不存在的接口,然後調用接口,看一下頁面是否會正常渲染,同時查看一下 store 裏的值
- src/server/render.js
// src/server/render.js
matchedRoutes.forEach(item => {
let loadData = item.route.loadData;
if (loadData) {
const promise = new Promise((resolve) => {
loadData(store).then(resolve).catch(resolve);
});
promises.push(promise);
}
});
- 這裏還需要再進行一步操作,就是在有 loadData 屬性的組件裏,我們要在 componentDidMount 或者 componentWillMount 生命週期方法裏去判斷 store 裏是否有我們想要的值,如果沒有,在這兩個生命週期方法裏進行調用,這樣就不會因為注水失敗,而導致頁面沒有數據
- 我們修改 Home 組件裏的代碼,src/containers/Home/index.js
// src/containers/Home/index.js
componentDidMount () {
if (!this.props.user.schoolList.length) {
this.props.propGetSchoolList();
}
}
- 但是,如果服務端沒有獲取到數據,客户端也沒有數據,那就是出 bug 了,這個需要測試接口,還需要測試前端的代碼
2. 服務端代理轉發
- 我們開發的時候,因為有安全問題,所以儘量不讓客户端去直接調用第三方接口。但是我們服務端渲染的時候,不提供第三方的接口,這該怎麼辦?我們可以採用代理
- 代理,跟代購(海淘)很像,我們要買一雙空軍一號耐克鞋,但是由於國內粉絲熱情,一鞋難求,而且還有黃牛囤貨,價錢太貴。國內沒有空軍一號,但是美國有呀,我們可以去美國買呀。這下就有問題了,去美國就得買機票辦簽證定酒店,而且還有拒籤的風險,再説,也不可能去美國專門買一雙鞋回來的呀。這成本加起來,別説買一雙了,買 10 雙都夠了,何必呢,所以,找代購,代購才多花多少錢,花不了多少錢,比起簽證機票酒店便宜了的海了去了,所以,代購,就是我們最好的選擇
- 我們把我們需要購買的物品告訴代購,代購去美國購買,購買完之後,再把物品帶回來給我。這個過程,代購就是一箇中間人的過程,跟代理是一模一樣的,我們的客户端是建立在 Node 服務器上的,客户端發送請求給 Node 服務器,Node 服務器把請求信息轉發給第三方服務器,比如 Java,那麼 Node 服務器去請求 Java 服務器,Java 服務器把數據返回給 Node 服務器,Node 服務端再把數據返回給客户端。這裏的 Node 服務器,就是中間層代理
- 我們採用 express-http-proxy 這個庫來做轉發,但是轉發之前我們要考慮一下,哪些需要轉發?哪些不需要轉發?肯定不是所有的都轉發,我們要知道第三方服務是做數據提供的,不是做頁面渲染和資源提供的,所以只需要轉發 api 數據接口就行,如果有需要,也可以轉發需要的請求頭,比如 cookie 信息,其他的不需要轉發,所以,我們統一把以 api 開頭的接口,轉發給 Java 服務器
- src/server/index.js
import proxy from 'express-http-proxy';
app.use('/api', proxy('http://localhost:8757', {
proxyReqPathResolver(req) {
return `/api${req.url}`;
}
}));
- 所以,我們實際上僅僅是把以 api 開頭的接口進行了轉發,其他的都沒有修改
3. axios 的請求優化
3.1 為什麼要做 axios 的請求優化
- 既然服務端已經把請求的接口進行了轉發,那麼我們的客户端在請求的時候,就不需要直接請求第三方服務,直接去請求服務端就可以,因為服務端已經幫助我們做了一層代理
- 還有一個問題,客户端請求服務端要轉發,那麼服務端請求的話,服務端就不需要轉發,因為服務端直接請求的就是第三方服務,換句話説,客户端和服務端請求的路徑,還是有一些不一樣的,我們可以對不同的端請求進行不同的處理
3.2 如何對 axios 進行優化
- 我們通過 axios.create 方法創建一個實例,這個實例本質上與 axios 是一樣的,只不過是説,創建出來的做個實例,我們可以對請求頭和響應頭做統一的處理,這樣更加方便。
- 在 src/client/ 創建一個 request.js 文件,供客户端請求使用
import axios from 'axios';
export default axios.create({
baseURL: `/`
});
- 在 src/server/ 創建一個 request.js 文件,供服務端請求使用
import axios from 'axios';
const serverAxios = axios.create({
baseURL: 'http://localhost:8757'
});
export default serverAxios;
3.3 如何使用 axios 實例
- 定義 axios 實例之後,我們需要考慮如何去使用
- 我們知道,redux 的 action 返回的是一個對象,通過 redux-thunk 可以返回一個方法。redux-thunk 還有一個 withExtraArgument 的屬性方法,我們可以把 axios 的實例作為 withExtraArgument 的參數進行傳遞,在 action 中直接使用這個參數
- 把 axios 的實例傳遞到 store 裏,src/store/index.js
// src/store/index.js
import clientAxios from '../client/request';
import serverAxios from '../server/request';
export const getServerStore = (req) => createStore(
reducers,
composeWithDevTools(applyMiddleware(thunk.withExtraArgument(serverAxios(req)), logger))
);
export const getClientStore = () => {
let initState = window.context.state;
return createStore(
reducers,
initState,
composeWithDevTools(applyMiddleware(thunk.withExtraArgument(clientAxios), logger))
)
};
- 在 action 中使用,src/store/user/createActions.js
// src/store/user/createActions.js
export const getSchoolList = () => {
return (dispatch, getState, axiosInstance) => {
return axiosInstance.get('http://localhost:8758/api/getSchoolList').then(res => {
if (res.status === 200) {
let schoolList = res.data.schoolList;
dispatch({
type: Types.GET_SCHOOL_LIST,
payload: schoolList
});
}
});
}
}
- 這個時候,我們就實現了針對客户端和服務端不一樣,請求的方式也不一樣,也方便我們後期對不同的端的 axios 請求做一些擴展
3.4 關於 cookie
- 我們知道,我們現在的瀏覽器向服務器發送請求的時候,是有 cookie 信息的,然而現在服務端在向 Java 服務器請求的時候,是沒有 cookie 信息的,但是我們經常需要做登錄校驗的判斷,所以我們還需要把 cookie 轉發給 Java 服務器
- 我們修改一下服務端的 axios 實例
- src/server/render.js
// src/server/render.js
let store = getServerStore(req);
- src/store/index.js
export const getServerStore = (req) => createStore(
reducers,
composeWithDevTools(applyMiddleware(thunk.withExtraArgument(serverAxios(req)), logger))
);
- src/server/request.js
// src/server/request.js
const serverAxios = axios.create({
baseURL: 'http://localhost:8757',
headers: {
cookie: req.get('cookie') || ''
}
});
- 實際上就是,我們把請求信息全部通過 getServerStore 傳遞給 serverAxios 實例,然後在 serverAxios 裏修改請求頭信息,服務端在向 Java 服務器發送請求的時候,就可以把 cookie 攜帶上,也可以攜帶其他的信息,比如請求的其他參數之類的
相關閲讀
- React服務端渲染之路01——項目基礎架構搭建
- React服務端渲染之路02——最簡單的服務端渲染
- React服務端渲染之路03——路由
- React服務端渲染之路04——redux-01
- React服務端渲染之路05——redux-02
- React服務端渲染之路06——優化
- React服務端渲染之路07——添加CSS樣式
- React服務端渲染之路08——404和重定向
- React服務端渲染之路09——SEO優化