H5 頁面列表緩存方案
大家好,我是若川(點這裏加我微信 ruochuan12,長期交流學習)。今天給大家介紹一下關於 h5 頁面的列表緩存方案。感謝屏幕前的你一直關注着我。
前言
在 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}>
<Route
path="/"
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_NAME
CACHE_NAME = `customerList${this.props.index}`;
scrollDom = null
state = {
orderBy: '2',
loading: false,
num: 1,
dataSource: [],
keyWord: undefined
}
componentDidMount () {
// 設置滾動容器list
this.scrollElRefs = [this.scrollDom]
// 請求數據,更新 state
}
render () {
const { history } = this.props
const { dataSource, orderBy, loading } = this.state
return (
<div className={gcmc('wrapper')}>
<MeScroll
className={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 => (
<Card
key={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>
<img
src={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
// 取 state
if (action === 'POP') {
const { state = {} } = window[storeName][this.CACHE_NAME]
this.state = {
...state,
}
}
}
async componentDidMount () {
if (super.componentDidMount) {
await super.componentDidMount()
}
const { history: { action } = {} } = this.props
if (action !== 'POP') return
const { scrollTops = [] } = window[storeName][this.CACHE_NAME]
const { scrollElRefs = [] } = this
// 取 scrollTop
scrollElRefs.forEach((el, index) => {
if (el && el.scrollTop !== undefined) {
el.scrollTop = scrollTops[index]
}
})
}
componentWillUnmount () {
const { history: { action } = {} } = this.props
if (super.componentWillUnmount) {
super.componentWillUnmount()
}
if (action === 'PUSH') {
const scrollTops = []
const { scrollElRefs = [] } = this
scrollElRefs.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
。
緩存方案縱有多種,但需要考慮的問題就以上幾點。另外在講述需要注意的五個點的時候,着重介紹了存什麼和存在哪,其實存在哪不太重要,也不需要太關心,找個合適的地方存着就行,比較重要的是存什麼、何時存,需要結合實際的應用場景,來選擇合適的方式,可能不同的頁面採用的方式都不同,沒有固定的方案,重要的是分析存取的時機和位置。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/wwSd9xXt7-aWncoRBGy-9w