這是第 95 篇不摻水的原創,想獲取更多原創好文,請搜索公衆號關注咱們吧~ 本文首發於政採雲前端博客: H5 頁面列表緩存方案
在 H5 平常開發中,會常常遇到列表點擊進入詳情頁面而後返回列表的狀況,對於電商類平臺尤其常見,像咱們日常用的淘寶、京東等電商平臺都是作了緩存,並且不僅是列表,不少地方都用到了緩存。但剛纔說的都是 App,在原生 App 中,頁面是一層層的 View,蓋在LastPage
上,自然就可以保存上一個頁面的狀態,而 H5 不一樣,從詳情返回到列表後,狀態會被清除掉,從新走一遍生命週期,會從新發起請求,會有新的狀態寫入,對於分頁接口,列表很長,當用戶翻了好幾頁後,點擊詳情看看商品詳情後再返回列表,此時頁面回到第一頁,這樣用戶體驗不好,若是在進入詳情的時候將列表數據緩存起來,返回列表的時候用緩存數據,而不是從新請求數據,停留在離開列表頁時的瀏覽位置;或者是可以像 App 那樣,將頁面一層層堆疊在LastPage
上,返回的時候展現對應的頁面,這樣用戶體驗會好不少,本文簡單介紹一下在本身在作列表緩存的時候考慮的幾點,後附簡單實現。
一般在頁面開發中,咱們是經過路由去管理不一樣的頁面,經常使用的路由庫也有不少,譬如:React-Router,Dva-router...... 當咱們切換路由時,沒有被匹配到的 Component
也會被總體替換掉,原有的狀態也丟失了,所以,當用戶從詳情頁退回到列表頁時,會從新加載列表頁面組件,從新走一遍生命週期,獲取的就是第一頁的數據,從而回到了列表頂部,下面是經常使用的路由匹配代碼段。javascript
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
標籤包裹的組件,在頁面切換的時候會自動緩存 失活
的組件,使用起來很是方便,簡單例子以下。html
<!-- 失活的組件將會被緩存!--> <keep-alive> <component v-bind:is="currentTabComponent"></component> </keep-alive>
可是,React
中並無 keep-alive
這種相似的標籤或功能,官方認爲這個功能容易形成內存泄漏,暫不考慮支持。前端
因此只能是在路由層作手腳,在路由切換時作對應的緩存操做,以前有開發者提出了一種方案:經過樣式來控制組件的顯示/隱藏,可是這可能會有問題,例如切換組件的時候沒法使用動畫,或者使用 Redux
、Mobx
這樣的數據流管理工具,還有開發者經過 React.createPortal
API
實現了 React
版本的 React Keep Alive
,而且使用起來也比較方便。第二種解決方案就是手動保存狀態,即在頁面卸載時手動將頁面的狀態收集存儲起來,在頁面掛載的時候進行數據恢復,我的採用的就是簡單粗暴的後者,實現上比較簡單。緩存緩存,無外乎就是兩件事,存和取,那麼在存、取的過程當中須要注意哪些問題呢?html5
我的認爲須要注意的有如下幾點:java
首先咱們須要關心的是: 存什麼?既然要緩存,那麼咱們要存的是什麼?是緩存整個 Component
、列表數據仍是滾動容器的 scrollTop
。舉個例子,微信公衆號裏的文章就作了緩存,任意點擊一篇文章瀏覽,瀏覽到一半後關閉退出,再一次打開該文章時會停留在以前的位置,並且你們能夠自行測試一下,再次打開的時候文章數據是從新獲取的,在這種場景下,是緩存了文章詳情滾動容器的滾動高度,在離開頁面的時候存起來,再次進入的時候拿到數據後跳轉到以前的高度,除此以外,還有不少別的緩存的方式,能夠緩存整個頁面,緩存 state
的數據等等,這些均可以達到咱們想要的效果,具體用哪種要看具體的業務場景。react
其次,咱們須要考慮的是何時存,頁面跳轉時會有多種 action
導航操做,好比:POP
、PUSH
、REPLACE
等,當咱們結合一些比較通用的路由庫時,action
會區分的更加細緻,對於不一樣的 action
在不一樣的業務場景下處理的方式也不盡相同,仍是拿微信公衆號舉例,文章詳情頁面就是無腦存,不管是 PUSH
、POP
都會存高度數據,因此咱們不管跳轉多少次頁面,再次打開總能跳轉到以前離開時的位置,對於商品列表的場景時,就不能無腦存了,由於從 List
-> Detail
-> List
須要緩存沒問題,可是用戶從 List
返回到其餘頁面後再次進入 List
時,是進入一個新的頁面,從邏輯上來講就不該該在用以前緩存的數據,而是從新獲取數據。正確的方式應該是進行 PUSH
操做的時候存, POP
的時候取。git
URL
或 localStorage
中,放到 URL
上有一個很好點在於肯定性,易於傳播。但 URL
能夠先 pass
掉,由於在複雜列表的狀況下,須要存的數據比較多,所有放到 URL
是不現實的,即便能夠,也會讓 URL
顯得極其冗長,顯然不妥。 localStorage
是一種方式,提供的 getItem
、setItem
等 api 也足夠支持存取操做,最大支持 5M,容量也夠,經過序列化 Serialize
整合也能夠知足需求,另外 IndexDB
也不失爲一種好的方式,WebSQL
已廢棄,就不考慮了,詳細可點擊張鑫旭的這篇文章《HTML5 indexedDB前端本地存儲數據庫實例教程》查看對比。redux
或 rematch
等狀態管理工具中,封裝一些通用的存取方法,很方便,對於通常的單頁應用來講,還能夠放到全局的 window
中。在進入緩存頁面的時候取,取的時候又有幾種狀況github
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
。redux
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
只是最簡單的一種實現,還有不少能夠改進的地方,譬如:1. 直接存在 window
中有點粗暴,多頁應用下存到 window
會丟失數據,能夠考慮存到 IndexDB
或者 localStorage
中,另外這種方案若不配合上 mescroll
須要在 componentDidMount
判斷 state
內的數據,如有值就不初始化數據,這算是一個 bug
。
緩存方案縱有多種,但須要考慮的問題就以上幾點。另外在講述須要注意的五個點的時候,着重介紹了存什麼和存在哪,其實存在哪不過重要,也不須要太關心,找個合適的地方存着就行,比較重要的是存什麼、什麼時候存,須要結合實際的應用場景,來選擇合適的方式,可能不一樣的頁面採用的方式都不一樣,沒有固定的方案,重要的是分析存取的時機和位置。
政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 40 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。
若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com