大家好,我是若川(點這里加我微信?ruochuan12,長期交流學習)。今天給大家介紹一下關于h5頁面的列表緩存方案。感謝屏幕前的你一直關注著我。
點擊下方卡片關注我、加個星標,或者查看源碼等系列文章。學習源碼整體架構系列、年度總結、JS基礎系列
前言
在 H5 日常開發中,會經常遇到列表點擊進入詳情頁面然后返回列表的情況,對于電商類平臺尤為常見,像我們平常用的淘寶、京東等電商平臺都是做了緩存,而且不只是列表,很多地方都用到了緩存。但剛才說的都是 App,在原生 App 中,頁面是一層層的 View,蓋在?
LastPage
?上,天然就能夠保存上一個頁面的狀態,而 H5 不同,從詳情返回到列表后,狀態會被清除掉,重新走一遍生命周期,會重新發起請求,會有新的狀態寫入,對于分頁接口,列表很長,當用戶翻了好幾頁后,點擊詳情看看商品詳情后再返回列表,此時頁面回到第一頁,這樣用戶體驗很差,如果在進入詳情的時候將列表數據緩存起來,返回列表的時候用緩存數據,而不是重新請求數據,停留在離開列表頁時的瀏覽位置;或者是能夠像 App 那樣,將頁面一層層堆疊在?LastPage
?上,返回的時候展示對應的頁面,這樣用戶體驗會好很多,本文簡單介紹一下在自己在做列表緩存的時候考慮的幾點,后附簡單實現。
思考
狀態丟失的原因
通常在頁面開發中,我們是通過路由去管理不同的頁面,常用的路由庫也有很多,譬如:React-Router (https://react-guide.github.io/react-router-cn/),Dva-router (https://dvajs.com/api/#dva-router)... 當我們切換路由時,沒有被匹配到的?Component
?也會被整體替換掉,原有的狀態也丟失了。因此,當用戶從詳情頁退回到列表頁時,會重新加載列表頁面組件,重新走一遍生命周期,獲取的就是第一頁的數據,從而回到了列表頂部,下面是常用的路由匹配代碼段。
function?RouterConfig({?history,?app?})?{const?routerData?=?getRouterData(app);return?(<ConnectedRouter?history={history}><Routepath="/"render={(props)?=>?<Layouts?routerData={routerData}?{...props}?/>}redirectPath="/exception/403"/></ConnectedRouter>);
}
//?路由配置說明(你不用加載整個配置,
//?只需加載一個你想要的根路由,
//?也可以延遲加載這個配置)。
React.render((<Router><Route?path="/"?component={App}><Route?path="about"?component={About}/><Route?path="users"?component={Users}><Route?path="/user/:userId"?component={User}/></Route><Route?path="*"?component={NoMatch}/></Route></Router>
),?document.body)
如何解決
原因找到了,那么我們怎么去緩存頁面或者數據呢?一般有兩種解決方式:1. 路由切換時自動保存狀態。2. 手動保存狀態。在?Vue
?中,可以直接使用?keep-alive
?來實現組件緩存,只要使用了?keep-alive
?標簽包裹的組件,在頁面切換的時候會自動緩存?失活
?的組件,使用起來非常方便,簡單例子如下。
<!--?失活的組件將會被緩存!-->
<keep-alive><component?v-bind:is="currentTabComponent"></component>
</keep-alive>
但是,React
?中并沒有?keep-alive
?這種類似的標簽或功能,官方認為這個功能容易造成內存泄漏,暫不考慮支持 (https://github.com/facebook/react/issues/12039)。
所以只能是在路由層做手腳,在路由切換時做對應的緩存操作,之前有開發者提出了一種方案:通過樣式來控制組件的顯示/隱藏 (https://github.com/facebook/react/issues/12039),但是這可能會有問題,例如切換組件的時候無法使用動畫,或者使用?Redux
、Mobx
?這樣的數據流管理工具,還有開發者通過?React.createPortal
?API
?實現了?React
?版本的?React Keep Alive
?(https://github.com/Sam618/react-keep-alive),并且使用起來也比較方便。第二種解決方案就是手動保存狀態,即在頁面卸載時手動將頁面的狀態收集存儲起來,在頁面掛載的時候進行數據恢復,個人采用的就是簡單粗暴的后者,實現上比較簡單。緩存緩存,無外乎就是兩件事,存和取,那么在存、取的過程中需要注意哪些問題呢?
個人認為需要注意的有以下幾點:
存什么?何時存?存在哪?何時取?在哪取?
存什么
首先我們需要關心的是:存什么?既然要緩存,那么我們要存的是什么?是緩存整個?Component
、列表數據還是滾動容器的?scrollTop
。舉個例子,微信公眾號里的文章就做了緩存,任意點擊一篇文章瀏覽,瀏覽到一半后關閉退出,再一次打開該文章時會停留在之前的位置,而且大家可以自行測試一下,再次打開的時候文章數據是重新獲取的,在這種場景下,是緩存了文章詳情滾動容器的滾動高度,在離開頁面的時候存起來,再次進入的時候拿到數據后跳轉到之前的高度,除此之外,還有很多別的緩存的方式,可以緩存整個頁面,緩存?state
?的數據等等,這些都可以達到我們想要的效果,具體用哪一種要看具體的業務場景。
何時存
其次,我們需要考慮的是什么時候存,頁面跳轉時會有多種?action
?導航操作,比如:POP
、PUSH
、REPLACE
?等,當我們結合一些比較通用的路由庫時,action
?會區分的更加細致,對于不同的?action
?在不同的業務場景下處理的方式也不盡相同。還是拿微信公眾號舉例,文章詳情頁面就是無腦存,無論是?PUSH
、POP
?都會存高度數據,所以我們無論跳轉多少次頁面,再次打開總能跳轉到之前離開時的位置,對于商品列表的場景時,就不能無腦存了,因為從?List
?->?Detail
?->?List
?需要緩存沒問題,但是用戶從?List
?返回到其他頁面后再次進入?List
?時,是進入一個新的頁面,從邏輯上來說就不應該在用之前緩存的數據,而是重新獲取數據。正確的方式應該是進行?PUSH
?操作的時候存,POP
?的時候取。
存在哪
持久化緩存。如果是數據持久化可存到?
URL
?或?localStorage
?中,放到?URL
?上有一個很好點在于確定性,易于傳播。但?URL
?可以先?pass
?掉,因為在復雜列表的情況下,需要存的數據比較多,全部放到?URL
?是不現實的,即使可以,也會讓?URL
?顯得極其冗長,顯然不妥。localStorage
?是一種方式,提供的?getItem
、setItem
?等 api 也足夠支持存取操作,最大支持 5M,容量也夠,通過序列化?Serialize
?整合也可以滿足需求,另外?IndexDB
?也不失為一種好的方式,WebSQL
?已廢棄,就不考慮了,詳細可點擊張鑫旭的這篇文章《HTML5 indexedDB前端本地存儲數據庫實例教程》(https://www.zhangxinxu.com/wordpress/2017/07/html5-indexeddb-js-example/)查看對比。內存。對于不需要做持久化的列表或數據來說,放內存可能是一個更好的方式,如果進行頻繁的讀寫操作,放內存中操作 I/O 速度快,方便。因此,可以放到
Redux
?或?Rematch
?等狀態管理工具中,封裝一些通用的存取方法,很方便,對于一般的單頁應用來說,還可以放到全局的?window
?中。
何時取
在進入緩存頁面的時候取,取的時候又有幾種情況
當導航操作為?
POP
?時取,因為每當?PUSH
?時,都算是進入一個新的頁面,這種情況是不應該用緩存數據。無論哪種導航操作都進行取數據,這種情況需要和何時存一起看待。
看具體的業務場景,來判斷取的時機。
在哪取
這個問題很簡單,存在哪就從哪里取。
CacheHoc
?的方案
存什么:列表數據 + 滾動容器的滾動高度
何時存:頁面離開且導航操作為?
PUSH
存在哪:
window
何時取:頁面初始化階段且導航操作為?
POP
?的時候在哪取:
window
CacheHoc
?是一個高階組件,緩存數據統一存到?window
?內,通過?CACHE_STORAGE
?收斂,外部僅需要傳入?CACHE_NAME
,scrollElRefs
?即可,CACHE_NAME
?相當于緩存數據的?key
,而?scrollElRefs
?則是一個包含滾動容器的數組,為啥用數組呢,是考慮到頁面多個滾動容器的情況,在?componentWillUnmount
?生命周期函數中記錄對應滾動容器的?scrollTop
、state
,在?constructor
?內初始化?state
,在?componentDidMount
?中更新?scrollTop
。
簡單使用
import?React?from?'react'
import?{?connect?}?from?'react-redux'
import?cacheHoc?from?'utils/cache_hoc'@connect(mapStateToProps,?mapDispatch)
@cacheHoc
export?default?class?extends?React.Component?{constructor?(...props)?{super(...props)this.props.withRef(this)}//?設置?CACHE_NAMECACHE_NAME?=?`customerList${this.props.index}`;scrollDom?=?nullstate?=?{orderBy:?'2',loading:?false,num:?1,dataSource:?[],keyWord:?undefined}componentDidMount?()?{//?設置滾動容器listthis.scrollElRefs?=?[this.scrollDom]//?請求數據,更新?state}render?()?{const?{?history?}?=?this.propsconst?{?dataSource,?orderBy,?loading?}?=?this.statereturn?(<div?className={gcmc('wrapper')}><MeScrollclassName={gcmc('wrapper')}getMs={ref?=>?(this.scrollDom?=?ref)}loadMore={this.fetchData}refresh={this.refresh}up={{page:?{num:?1,?//?當前頁碼,默認0,回調之前會加1,即callback(page)會從1開始size:?15?//?每頁數據的數量//?time:?null?//?加載第一頁數據服務器返回的時間;?防止用戶翻頁時,后臺新增了數據從而導致下一頁數據重復;}}}down={{?auto:?false?}}>{loading???(<div?className={gcmc('loading-wrapper')}><Loading?/></div>)?:?(dataSource.map(item?=>?(<Cardkey={item.clienteleId}data={item}{...this.props}onClick={()?=>history.push('/detail/id')}/>)))}</MeScroll><div?className={styles['sort']}><div?className={styles['sort-wrapper']}?onClick={this._toSort}><span style={{?marginRight:?3?}}>最近下單時間</span><imgsrc={orderBy?===?'2'???SORT_UP?:?SORT_DOWN}alt='sort'style={{?width:?10,?height:?16?}}/></div></div></div>)}
}
效果如下:
緩存的數據:
代碼
const?storeName?=?'CACHE_STORAGE'
window[storeName]?=?{}export?default?Comp?=>?{return?class?CacheWrapper?extends?Comp?{constructor?(props)?{super(props)//?初始化if?(!window[storeName][this.CACHE_NAME])?{window[storeName][this.CACHE_NAME]?=?{}}const?{?history:?{?action?}?=?{}?}?=?props//?取?stateif?(action?===?'POP')?{const?{?state?=?{}?}?=?window[storeName][this.CACHE_NAME]this.state?=?{...state,}}}async?componentDidMount?()?{if?(super.componentDidMount)?{await?super.componentDidMount()}const?{?history:?{?action?}?=?{}?}?=?this.propsif?(action?!==?'POP')?returnconst?{?scrollTops?=?[]?}?=?window[storeName][this.CACHE_NAME]const?{?scrollElRefs?=?[]?}?=?this//?取?scrollTopscrollElRefs.forEach((el,?index)?=>?{if?(el?&&?el.scrollTop?!==?undefined)?{el.scrollTop?=?scrollTops[index]}})}componentWillUnmount?()?{const?{?history:?{?action?}?=?{}?}?=?this.propsif?(super.componentWillUnmount)?{super.componentWillUnmount()}if?(action?===?'PUSH')?{const?scrollTops?=?[]const?{?scrollElRefs?=?[]?}?=?thisscrollElRefs.forEach(ref?=>?{if?(ref?&&?ref.scrollTop?!==?undefined)?{scrollTops.push(ref.scrollTop)}})window[storeName][this.CACHE_NAME]?=?{state:?{...this.state},scrollTops}}if?(action?===?'POP')?{window[storeName][this.CACHE_NAME]?=?{}}}}
}
總結
以上的?CacheHoc
?只是最簡單的一種實現,還有很多可以改進的地方,譬如:直接存在?window
?中有點粗暴,多頁應用下存到?window
?會丟失數據,可以考慮存到?IndexDB
?或者?localStorage
?中,另外這種方案若不配合上?mescroll
?需要在?componentDidMount
?判斷?state
?內的數據,若有值就不初始化數據,這算是一個?bug
。
緩存方案縱有多種,但需要考慮的問題就以上幾點。另外在講述需要注意的五個點的時候,著重介紹了存什么和存在哪,其實存在哪不太重要,也不需要太關心,找個合適的地方存著就行,比較重要的是存什么、何時存,需要結合實際的應用場景,來選擇合適的方式,可能不同的頁面采用的方式都不同,沒有固定的方案,重要的是分析存取的時機和位置。
最近組建了一個江西人的前端交流群,如果你也是江西人可以加我微信 ruochuan12 拉你進群。
·················?若川出品?·················
今日話題
還有最后一天上班就放五一小長假啦(努力讓內心喜悅不被發現),雖然扣除2天補班,?2天周末,實際天只有1天假期~哈哈,但是能連著休息5天,也還是很不錯哦。趁著小長假可以好好放休息休息,整理一下之前沒及時整理的東西,大家五一都有什么計劃呢?歡迎在下方留言~? 歡迎分享、收藏、點贊、在看我的公眾號文章~
一個愿景是幫助5年內前端人走向前列的公眾號
可加我個人微信?ruochuan12,長期交流學習
推薦閱讀
我在阿里招前端,我該怎么幫你?(現在還能加我進模擬面試群)
若川知乎問答:2年前端經驗,做的項目沒什么技術含量,怎么辦?
點擊上方卡片關注我、加個星標,或者查看源碼等系列文章。
學習源碼整體架構系列、年度總結、JS基礎系列