H5 頁面列表緩存方案

這是第 95 篇不摻水的原創,想獲取更多原創好文,請搜索公衆號關注咱們吧~ 本文首發於政採雲前端博客: H5 頁面列表緩存方案

前言

在 H5 平常開發中,會常常遇到列表點擊進入詳情頁面而後返回列表的狀況,對於電商類平臺尤其常見,像咱們日常用的淘寶、京東等電商平臺都是作了緩存,並且不僅是列表,不少地方都用到了緩存。但剛纔說的都是 App,在原生 App 中,頁面是一層層的 View,蓋在 LastPage 上,自然就可以保存上一個頁面的狀態,而 H5 不一樣,從詳情返回到列表後,狀態會被清除掉,從新走一遍生命週期,會從新發起請求,會有新的狀態寫入,對於分頁接口,列表很長,當用戶翻了好幾頁後,點擊詳情看看商品詳情後再返回列表,此時頁面回到第一頁,這樣用戶體驗不好,若是在進入詳情的時候將列表數據緩存起來,返回列表的時候用緩存數據,而不是從新請求數據,停留在離開列表頁時的瀏覽位置;或者是可以像 App 那樣,將頁面一層層堆疊在 LastPage 上,返回的時候展現對應的頁面,這樣用戶體驗會好不少,本文簡單介紹一下在本身在作列表緩存的時候考慮的幾點,後附簡單實現。

思考

狀態丟失的緣由

一般在頁面開發中,咱們是經過路由去管理不一樣的頁面,經常使用的路由庫也有不少,譬如:React-RouterDva-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 這種相似的標籤或功能,官方認爲這個功能容易形成內存泄漏,暫不考慮支持
前端

因此只能是在路由層作手腳,在路由切換時作對應的緩存操做,以前有開發者提出了一種方案:經過樣式來控制組件的顯示/隱藏,可是這可能會有問題,例如切換組件的時候沒法使用動畫,或者使用 ReduxMobx 這樣的數據流管理工具,還有開發者經過 React.createPortal API 實現了 React 版本的 React Keep Alive,而且使用起來也比較方便。第二種解決方案就是手動保存狀態,即在頁面卸載時手動將頁面的狀態收集存儲起來,在頁面掛載的時候進行數據恢復,我的採用的就是簡單粗暴的後者,實現上比較簡單。緩存緩存,無外乎就是兩件事,存和取,那麼在存、取的過程當中須要注意哪些問題呢?html5

我的認爲須要注意的有如下幾點:java

存什麼?什麼時候存?存在哪?什麼時候取?在哪取?

存什麼

首先咱們須要關心的是: 存什麼?既然要緩存,那麼咱們要存的是什麼?是緩存整個 Component 、列表數據仍是滾動容器的 scrollTop 。舉個例子,微信公衆號裏的文章就作了緩存,任意點擊一篇文章瀏覽,瀏覽到一半後關閉退出,再一次打開該文章時會停留在以前的位置,並且你們能夠自行測試一下,再次打開的時候文章數據是從新獲取的,在這種場景下,是緩存了文章詳情滾動容器的滾動高度,在離開頁面的時候存起來,再次進入的時候拿到數據後跳轉到以前的高度,除此以外,還有不少別的緩存的方式,能夠緩存整個頁面,緩存 state 的數據等等,這些均可以達到咱們想要的效果,具體用哪種要看具體的業務場景。react

什麼時候存

其次,咱們須要考慮的是何時存,頁面跳轉時會有多種 action 導航操做,好比:POPPUSHREPLACE 等,當咱們結合一些比較通用的路由庫時,action 會區分的更加細緻,對於不一樣的 action 在不一樣的業務場景下處理的方式也不盡相同,仍是拿微信公衆號舉例,文章詳情頁面就是無腦存,不管是 PUSHPOP 都會存高度數據,因此咱們不管跳轉多少次頁面,再次打開總能跳轉到以前離開時的位置,對於商品列表的場景時,就不能無腦存了,由於從 List -> Detail -> List 須要緩存沒問題,可是用戶從 List 返回到其餘頁面後再次進入 List 時,是進入一個新的頁面,從邏輯上來講就不該該在用以前緩存的數據,而是從新獲取數據。正確的方式應該是進行 PUSH 操做的時候存, POP 的時候取。git

存在哪

  1. 持久化緩存。 若是是數據持久化可存到 URLlocalStorage 中,放到 URL 上有一個很好點在於肯定性,易於傳播。但 URL 能夠先 pass 掉,由於在複雜列表的狀況下,須要存的數據比較多,所有放到 URL 是不現實的,即便能夠,也會讓 URL 顯得極其冗長,顯然不妥。 localStorage 是一種方式,提供的 getItemsetItem 等 api 也足夠支持存取操做,最大支持 5M,容量也夠,經過序列化 Serialize 整合也能夠知足需求,另外 IndexDB 也不失爲一種好的方式,WebSQL 已廢棄,就不考慮了,詳細可點擊張鑫旭的這篇文章《HTML5 indexedDB前端本地存儲數據庫實例教程》查看對比。
  2. 內存。 對於不須要作持久化的列表或數據來講,放內存多是一個更好的方式,若是進行頻繁的讀寫操做,放內存中操做 I/O 速度快,方便。所以,能夠放到 reduxrematch 等狀態管理工具中,封裝一些通用的存取方法,很方便,對於通常的單頁應用來講,還能夠放到全局的 window 中。

什麼時候取

在進入緩存頁面的時候取,取的時候又有幾種狀況github

  1. 當導航操做爲 POP 時取, 由於每當 PUSH 時,都算是進入一個新的頁面,這種狀況是不該該用緩存數據。
  2. 不管哪一種導航操做都進行取數據,這種狀況須要和什麼時候存一塊兒看待。
  3. 看具體的業務場景,來判斷取的時機。

在哪取

這個問題很簡單,存在哪就從哪裏取。數據庫

CacheHoc 的方案

  • 存什麼: 列表數據 + 滾動容器的滾動高度
  • 什麼時候存: 頁面離開且導航操做爲 PUSH
  • 存在哪: window
  • 什麼時候取: 頁面初始化階段且導航操做爲 POP 的時候
  • 在哪取: window

CacheHoc 是一個高階組件,緩存數據統一存到 window 內,經過 CACHE_STORAGE 收斂,外部僅須要傳入 CACHE_NAMEscrollElRefs 便可,CACHE_NAME 至關於緩存數據的 key,而 scrollElRefs 則是一個包含滾動容器的數組,爲啥用數組呢,是考慮到頁面多個滾動容器的狀況,在 componentWillUnmount 生命週期函數中記錄對應滾動容器的 scrollTopstate,在 constructor 內初始化 state,在 componentDidMount 中更新 scrollTopredux

簡單使用

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

相關文章
相關標籤/搜索