1

最近在开发一个长图文预览项目,主要用在手机端浏览(主要在微信端)。这项目其实就是一个手机网页,把数据中的文本和图片等元素渲染出来即可。这样的项目很常见,包括微信内公众号的文章etc.这个项目很简单,但非常头疼的一个问题是对图片的懒加载处理。(下面讨论的加载策略暂且都是针对图片

前置条件:假定项目数据是一个数组,数组元素都是图片,并且指定了图片在屏幕中的left和top。
  • 我们最开始想到的处理方式是:优先考虑首屏体验。
    取得数据后,首屏先呈现Loading状态。通过屏幕的高度H和图片的top,得到首屏的图片,并对其每一个图片的onload和onerror事件绑定回调。当回调全都执行完成之后便将首屏Loading状态移除,呈现首屏的图片。大概的实现是:

    firstScreenPromises = firstScreenImgs.map((img) => {
      return new Promise((resolve, reject) => {
        let image = new Image()
        image.src = img.imgSrc
        image.onload = image.onerror = resolve
      })
    })
    Promise.all(firstScreenPromises).then(data => {
      // 首屏开始显示
    })

接下来首屏后面的图片就全部扔给浏览器去加载了。

  • 上面首屏显示优化自然ok,不过对后面屏幕图片的显示策略自然是不太好的。于是考虑分屏加载,一屏一屏加载图片。

    // 分屏
    screenBox = {}
    H = screen.height
    
    for (let i = 0; i < imgs.length; i += 1) {
      screenNum = Math.floor(imgs.top / H)
      screenBox[screenNum] = screenBox[screenNum] || []
      screenBox[screenNum].push(imgs[i])
    }
    
    indexs = Object.keys(screenBox)
    ----------上边代码块 (X) 继续给下一块代码使用——------------
    loadNext()
    
    // 分屏按序加载
    function loadNext() {
      if (!indexs.length)
        return
    
      screenBox[indexs[0]].map(img=>{
        // 同首屏firstScreenPromises
    
      }).then(data=>{
        // load完后回调该函数继续load下一屏
        indexs.shift()
        loadNext()
      })
    }

上面优化点在于按序分屏加载图片,这适合用户慢慢往下看的情况,但是会有两个弊端。
一是:如果用户突然猛翻到页面较后的位置,此时如果还在加载前面某屏的图片,那用户需要等待。
二是:如果页面图片元素非常多,屏幕数很多,会消耗许多流量,也许用户不想看到最后咧。

  • 于是考虑控制预加载的屏幕数,用户看屏幕所时处的屏幕数为N,预加载N+1,N+2....N+M,最多预加载M屏。同时监听屏幕滚动,通过滚动的高度算出用户所处的屏幕数。

    ....上边代码块(X).....
    loadNext(0) // 初始化load首屏
    window.onscroll = ()=>{
      getViewScreenIndex = function() {
        // 滚动监听求出用户视野所处的屏幕数(若处在a,a+1,取a)
        return ...
      }
      loadNext(getViewScreenIndex())
    }
    // 加载第N屏,加载完继续加载到第N+M屏
    function loadNext(N) {
      hasLoad = 0
      function load() {
        if (hasLoad == M) {
          // 已经加载到N+M屏,停止预加载
          return
        }
    
        hasLoad++
    
        if (indexs.indexOf(N) > -1) {
          screenBox[indexs[0]].map(img=>{
            // 同首屏firstScreenPromises
    
          }).then(data=>{
            // load完后回调该函数继续load下一屏
            indexs.splice(indexs.indexOf(N), 1)
            load()
          })
        } else {
          load()
        }    
      }
    }
        
  • 最后一个优化点:假设正在load第1屏(接下来会预加载第2,3屏),此时屏幕滚动到了第4屏(将会预加载第5,6屏)。此时是否还有必要继续去加载第2,3屏?我觉得是没必要的,用户更有可能会继续往后翻。所以此时我会取消掉2,3屏的预加载(当然,如果此时正在预加载第2屏,那只会取消掉第3屏的加载)。
    这一块的代码,加上一些细节处理,可以到我的github lazyloader看看。

总结:目前能考虑到的上边策略为的是提升用户体验(预加载),同时不会去消耗太多流量(限制预加载的数目)。但我相信还会有更加优化的策略,希望能得到高人的指点,那就真的灰常感激啦!
同时,这里边还会遇到一些兼容上的坑。比如:此处我所用到的滚动监听是window.onscroll。这个监听事件在不同设备上的表现非常不一样,会使得这里的加载策略不一定能使所有的设备都体验不错。
android的(不确定是不是所有)window.onscroll会在手指按着屏幕拖动时触发,以及屏幕滚动停止的时候触发;而ios的则是只在屏幕停止滚动的时候才会触发。这两者,在松开手后屏幕滚动时都不会触发onscroll事件。
目前还没想到比较好的兼容策略,希望有人能提供好的资料和想法借鉴借鉴,感激涕零。


zhoushx3
307 声望27 粉丝