案例
我從2020年開始一直使用next.js
做為我的前端SSR框架,使用@reduxjs/toolkit
做為全域性狀態管理器,使用next-redux-wrapper
協助next.js連線和合並redux中store資料並且保持不變,否則會導致資料重複渲染效能問題,但是最近發現一個很奇怪的問題:就是router.push
路由跳轉的時候導致當前頁面重複渲染問題!
換句話說也就是:我從PageA跳轉到PageB,應該是隻有PageB頁面才會渲染,但是現在不但PageB渲染了,PageA也渲染了!!!
分析與操作
經過我使用排除程式碼的方式分析發現,原來每次路由導航的時候都會觸發useSelector
這個方法,而這個方法是react-redux
外掛的。
為什麼會導航的時候會觸發useSelector
呢?我又在網上搜尋相關話題,終於被我找到一篇文章react-redux使用useSelector獲取資料導致元件重複渲染的問題
透過redux中的hooks – useSelector 獲取store中的資料時,只要store中的資料發生了改變,即使元件中並沒有獲取修改的資料,元件也會進行重新渲染。
也就是說造成重複渲染的原因是因為redux中store資料來源變化了導致的。
於是我們使用文中提到的設定useSelector
的第2個引數,相同的時候返回true
會阻止重複渲染,不同的時候返回false
重新渲染的機制,我們使用了lodash
的isEqual
方法來判斷。
注意:react-redux
自帶的shallowEqual
方法是淺比較,所以是陣列物件的情況下比較是有問題的,所以這裡我們使用了lodash
的isEqual
方法來深度比較判斷。
import _ from 'lodash'
const {userInfo, latestNews, likeUrls, reportUrls} = useSelector((state) => state.home, (_old, _new)=>{
console.log('old=',_old,',new=',_new)
return _.isEqual(_old, _new)
});
但是問題是,我只是做了一個路由跳轉為什麼會導致redux中store資料來源前後不一致呢?
於是我加了個列印日誌的程式碼,如下:
發現確實不一樣了,而且主要集中在latestNews, likeUrls, reportUrls
這三個資料來源上,而userInfo
不變。
這是為什麼呢?於是我又看了一下next-redux-wrapper
文件,發現裡面這段程式碼和一段描述:
State reconciliation during hydration
Each time when pages that have getStaticProps or getServerSideProps are opened by user the HYDRATE action will be dispatched. This may happen during initial page load and during regular page navigation. The payload of this action will contain the state at the moment of static generation or server side rendering, so your reducer must merge it with existing client state properly.
翻譯成中文
水合過程中的狀態調節
每次當使用者開啟具有getStaticProps或getServerSideProps的頁面時,都會排程HYDRATE操作。這可能發生在初始頁面載入期間和常規頁面導航期間。此操作的有效負載將包含靜態生成或伺服器端呈現時的狀態,因此您的reducer必須將其與現有客戶端狀態正確合併。
難道我沒有正解合併客戶端資料嗎?
於是我又看了一下我的水合程式碼:
const initialState = {
userInfo: null,
latestNews: [],
likeUrls: [],
reportUrls: []
}
[HYDRATE]: (state, action) => {
console.log('HYDRATE action.payload=',action.payload);
return {
...state,
...action.payload.home,
};
},
感覺沒啥問題啊!於是我又看看PageA的getServerSideProps
方法,發現有個區別:
userInfo
是透過getServerSideProps
Server服務端渲染得到的,而latestNews, likeUrls, reportUrls
是Client客戶端渲染拿的!!!
心機之蛙一直摸你肚子!
結合上面官方給的文件,再加上這個發現,那就是說我沒有正確水合服務端和客戶端資料!
果然,我列印水合資料的時候,action.payload
是拿不到客戶端的資料的,都是空的。
注意:雖然客戶端透過介面獲取的資料儲存到了store中,但是水合的時候是拿不到的,水合的時候只能拿到服務端資料。
接下來我們只要正確水合客戶端和服務端資料就可解決問題,所以我們修改下程式碼:
[HYDRATE]: (state, action) => {
console.log('HYDRATE action.payload=',action.payload);
const _merge = {
...state,
...action.payload.home,
}
console.log('state.latestNews=',state.latestNews)
// 這裡的latestNews、likeUrls、reportUrls都是客戶端資料,所以都要正確的水合到redux store中
if(state.latestNews){
_merge.latestNews = state.latestNews
}
if(state.likeUrls){
_merge.likeUrls = state.likeUrls
}
if(state.reportUrls){
_merge.reportUrls = state.reportUrls
}
return _merge
},
加好之後,我們再跳轉頁面發現水合資料成功了,useSelector
因為進行了深度比較判斷store也是不變的,所以也就不會導致重複渲染了,終於成功了!!!
注意:[HYDRATE]
中state屬性值必須與initialState
做相反判斷,否則會報下面錯。
Error: Hydration failed because the initial UI does not match what was rendered on the server.
解決:
只要if判斷跟initialState
值相反即可,如下程式碼。
const initialState = {
v1: [],
v2: null,
v3: true
}
[HYDRATE]: (state, action) =>{
const _merge = {
...state,
...action.payload.home,
}
// 相反就是length>0
if(state.v1.length > 0){
_merge.v1 = state.v1
}
//相反就是v2不能為空
if(state.v2){
_merge.v2 = state.v2
}
//相反就是v3=false
if(!state.v3){
_merge.v3 = state.v3
}
return _merge
},
總結
1、SSR的水合思想個人覺得理解起來是有點難度的,畢竟之前做前端開發是沒有遇到相同思想的問題
2、next-redux-wrapper文件其實也說明了很清楚,客戶端資料要水合到store中,否則會有問題的。
3、觸發水合的場景有:每次當使用者開啟具有getStaticProps
或getServerSideProps
的頁面時,都會排程HYDRATE
操作。這可能發生在初始頁面載入期間和常規頁面導航期間。
4、當觸發HYDRATE
時,只有服務端資料會儲存到store中,客戶端資料不會自動儲存到store中,所以可以將前置的state的客戶端資料主動合併到store中。
5、useSelector
方法的第2個引數透過判斷是否重新渲染,true
時不重新渲染,false
時重新渲染。
6、next-redux-wrapper
star這麼少,是不是跟它的思想難度(水合)有關???而且好多外網大佬都不建議使用任何一種狀態管理器的。
7、透過分析可知,getServerSideProps服務端獲取的資料,透過水合會儲存到redux Store中,也會儲存到客戶端dom中
8、到目前為止SSR遇到的兩個大問題,1、cookie,2、水合