關於圖片加載,你需要學習一下

作者:田同學 2001

https://juejin.cn/post/7134706181863374878

😊 從何而來

這篇文章,出自我自己的開源組件庫 fighting-design[1] 中的 Avatar 頭像 [2] 組件的 load-image[3] 類。

相比於其它的靜態組件,像圖片加載這種的組件,內部我做了很多的優化,對於圖片的加載和錯誤的處理,我都儘可能的將每種可能出現的結果都考慮到,針對每種不確定的結果做出相應的提示,以便於提升用戶體驗。

🥕 設計思路

我的設計想法是:通過一個加載類,傳入 dom 元素、 propsemit。先創建出一個虛擬的 image 元素進行嘗試加載,加載成功獲失敗都會進入下一步的函數,做出對應從處理邏輯。

🍭 初步設計

首先類中先有一個加載的方法 loadCreateImg,代碼如下:

class Load {
  constructor(node, props, emit) {
    this.node = node
    this.props = props
    this.emit = emit
  }
  // 加載 src
  loadCreateImg = () ={
    const newImg = new Image() // 新建一個虛擬的 img

    newImg.src = this.props.src // 將傳入的 src 賦值給虛擬節點

    // src 加載失敗
    newImg.addEventListener('error'(evt) ={
      // 加載失敗的處理
    })

    // src 加載成功
    newImg.addEventListener('load'(evt) ={
      // 加載成功的處理
    })
  }
}

首先我創建了一個 Load 的加載類,需要傳入 node 參數作爲最終需要渲染的 dom 節點,props 是傳入的組件內部的 props 參數,內部包含圖片需要加載的 src 路徑,emit 包括一些回調參數。

類的內部有個 loadCreateImg 的方法,調用可創建一個虛擬的 Image 元素,直接將傳入的 props.src 賦值並加載。監聽上面的 errorload 事件,即可監聽到圖片是否加載成功,以便做出不同的狀態。

🚩 成功和失敗

對於成功或失敗的處理,我新增了 onerroronload 方法,來處理加載成功和失敗之後的不同處理狀態

class Load {
  constructor(node, props, emit) {
    this.node = node
    this.props = props
    this.emit = emit
  }
  loadCreateImg = () ={
    const newImg = new Image()

    newImg.src = this.props.src

    newImg.addEventListener('error'(evt) ={
      this.onerror(evt) // 新增
    })

    newImg.addEventListener('load'(evt) ={
      this.onload(evt) // 新增
    })
  }
  // 加載成功
  onload = (evt) ={
    this.node.src = this.props.src
  }
  // 加載失敗
  onerror = (evt) ={
    // ……
  }
}

對於加載成功,處理方式是,將傳入的真是的 dom 節點直接賦值給傳入的 props.src 即可完成加載。

🚧 加載失敗

對於加載失敗的處理,Fighting Design 內部做了很多處理,比如可以傳入 err-src 的備用路徑加載,在 src 加載失敗之後,如果 err-src 存在的話,那麼就需要加載 err-src 。接下來繼續完善類方法:

首先要在 onerror 方法中判斷是否存在 err-src,如果有 err-src 那麼就需要重新調用 loadCreateImg 重新加載,但是現在的代碼顯然不能滿足需要,所以 loadCreateImg 需要接收一個可選的參數爲 errSrc,因爲只有在加載失敗之後才需要再次調用該方法傳入 err-src,所以方法內部就可以根據 err-src 是否存在,來做出不同的處理:

class Load {
  constructor(node, props, emit) {
    this.node = node
    this.props = props
    this.emit = emit
  }
  loadCreateImg = (errSrc?: string) ={
    const newImg = new Image()

    // 如果 errSrc 存在 就嘗試加載 errSrc
    if (errSrc) {
      newImg.src = errSrc
    } else {
      newImg.src = this.props.src
    }

    newImg.addEventListener('error'(evt) ={
      this.onerror(evt)
    })

    newImg.addEventListener('load'(evt) ={
      this.onload(evt)
    })
  }
  onload = (evt) ={
    this.node.src = this.props.src
  }
  // 加載失敗
  onerror = (evt) ={
    // 如果存在 errSrc 則繼續嘗試加載
    if (this.props.errSrc) {
      // 將 errSrc 傳給 loadCreateImg 方法
      return this.loadCreateImg(this.props.errSrc)
    }

    // 否則返回失敗回調
    this.emit('error', evt)
  }
}

但是上面代碼存在兩個問題:

  1. 首先我們發現,在 onload 加載成功的方法中,將真實 dom 始終賦值的始終 是 src
onload = (evt) ={
  // 始終賦值爲 props.src
  this.node.src = this.props.src
}

但是 src 並不是始終可以加載成功的,所以還是需要動態的去將真正加載成功的 src 傳給 onload 方法,那麼真正加載成功的 src 也就是在 load 方法中。並且還要加入成功的 emit

  1. 其次,在處理加載失敗的 onerror 方法中,因爲判斷了如果存在 errSrc 就繼續調用 loadCreateImg 加載方法重新加載。問題是,如果傳入了 errSrc 那麼 if (this.props.errSrc) 其實是始終爲真的,這也就導致了死循環,會重複調用加載函數。
onerror = (evt) ={
  // 判斷始終爲真
  if (this.props.errSrc) {
    return this.loadCreateImg(this.props.errSrc)
  }

  // 否則返回失敗回調
  this.emit('error', evt)
}

所以就需要給它一個可以變爲假的時機,那麼修復方法爲:在傳給 loadCreateImg 方法之後,將 errSrc 清空,這樣加載一次之後就可以判斷爲假了,所以完整代碼爲:

class Load {
  constructor(node, props, emit) {
    this.node = node
    this.props = props
    this.emit = emit
  }
  loadCreateImg = (errSrc?: string) ={
    const newImg = new Image()

    // 如果 errSrc 存在 就嘗試加載 errSrc
    if (errSrc) {
      newImg.src = errSrc
    } else {
      newImg.src = this.props.src
    }

    newImg.addEventListener('error'(evt) ={
      this.onerror(evt)
    })

    newImg.addEventListener('load'(evt) ={
      this.onload(evt, newImg.src) // 將加載成功的 src 傳給 onload 函數
    })
  }
  // 新增 src 屬性
  onload = (evt, src: string) ={
    this.node.src = src // 將真實 dom 的 src 賦值給傳入的 src
    this.emit('load', evt) // 新增
  }
  onerror = (evt) ={
    if (this.props.errSrc) {
      this.loadCreateImg(this.props.errSrc)
      this.props.errSrc = '' // 清空 errSrc 避免重複調用死循環
      return
    }

    this.emit('error', evt)
  }
}

🐬 回調函數

有些時候,我們還需要通過一個布爾值來判斷圖片是否加載成功,或者進行其它判斷。

Fighting Design 內部對圖片加載失敗做了特殊的樣式處理來提示用戶,所以需要一個布爾值和 v-if 來展示不同的狀態,這裏就涉及到了類的第四個參數,也就是一個可選的回調函數

這樣就可以在加載成功和加載失敗的時候通過回調函數來返回一個布爾值判斷是否加載成功,代碼如下:

class Load {
  constructor(node, props, emit, callback) {
    this.node = node
    this.props = props
    this.emit = emit
    this.callback = callback // 新增 callback 參數
  }
  loadCreateImg = (errSrc?: string) ={
    const newImg = new Image()

    if (errSrc) {
      newImg.src = errSrc
    } else {
      newImg.src = this.props.src
    }

    newImg.addEventListener('error'(evt) ={
      this.onerror(evt)
    })

    newImg.addEventListener('load'(evt) ={
      this.onload(evt, newImg.src)
    })
  }
  onload = (evt, src: string) ={
    this.node.src = src
    this.emit('load', evt)

    // 如果 callback 存在,在加載成功的時候返回 true
    if (this.callback) {
      this.callback(true)
    }
  }
  onerror = (evt) ={
    if (this.props.errSrc) {
      this.loadCreateImg(this.props.errSrc)
      this.props.errSrc = ''
      return
    }

    this.emit('error', evt)
    // 如果 callback 存在,在加載失敗的時候返回 false
    if (this.callback) {
      this.callback(false)
    }
  }
}

上面代碼即可實現判斷是否加載成功的需求。

當然,回調函數你可以盡情的發揮想象做出更多的事情,這裏僅提供部分用法。

⌛ 懶加載

圖片的懶加載,也是一個圖片加載必備的功能了,這裏我使用的是內置的 IntersectionObserver[4] 接口,對於這個方法,這裏不過多描述,各位可以通過 MDN[5] 進行學習。

對於懶加載,因爲這是一個可選的屬性,並不是每次都需要,所以我將懶加載單獨抽離出來的一個 Lazy 類進行實現,再將 Lazy 類繼承到 Load 類,代碼如下:

class Lazy extends Load {
  constructor(img, props, emit, callback) {
    // super 關鍵字調用
    super(img, props, emit, callback)
  }
  observer = () ={
    const observer = new IntersectionObserver(
      (arr)void ={
        // 如果進入可視區域
        if (arr[0].isIntersecting) {
          // 開始加載圖片 調用父類
          this.loadCreateImg()
          observer.unobserve(this.node)
        }
      },
      /**
       * rootMargin 爲觸發懶加載的距離 通過 props 傳入
       * https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin
       */
      { rootMargin: this.props.rootMargin }
    )
    return observer
  }
  // 執行 懶加載
  lazyCreateImg = ()void ={
    // IntersectionObserver 內部方法,需要將 dom 節點傳入
    this.observer().observe(this.node)
  }
}

IntersectionObserver 接口可以判斷 dom 元素是否進入可視區域,通過內置方法判斷進入可視區域之後,執行父類的 loadCreateImg 方法進行加載,從而實現懶加載。

🚥 對外接口

對於 Load 類和 Lazy 類,Fighting Design 並沒有直接暴露出去提供使用,而是暴露出了一個全新的 loadImage 函數,讓它去根據是否爲懶加載而實例化不同的類,再調用加載方法:

// 導出對外接口
export const loadImage = (node, prop, emit, callback) ={
  /**
   * 如果傳入了 lazy 則執行懶加載類
   * 否則執行正常加載類
   */
  if (prop.lazy) {
    const lazy = new Lazy(node, prop, emit, callback)
    return lazy.lazyCreateImg()
  }
  const load = new Load(node, prop, emit, callback)
  load.loadCreateImg()
}

🚩 測試使用

寫好的函數測試一下看看:

<script lang="ts" setup>
  import { loadImage } from '../../packages/fighting-design/_utils'
  import { ref, onMounted } from 'vue'

  const myImg = ref(null as unknown as HTMLImageElement)

  // 模擬 props
  const props = {
    src: 'https://tianyuha2o.cn/images/auto/my.jpg',
    errSrc: 'https://tianyuhao.cn/images/auto/4.jpg',
    lazy: true
  }

  onMounted(() => {
    loadImage(myImg.value, props)
  })
</script>

<template>
  <img ref="myImg" src="" />
</template>

可以看到,是成功執行的。

🏆 完整代碼

完整代碼可參考 load-image[6]

參考資料

[1]

fighting-design: https://github.com/FightingDesign/fighting-design

[2]

Avatar 頭像: https://fighting.tianyuhao.cn/components/avatar.html

[3]

load-image: https://github.com/FightingDesign/fighting-design/blob/master/packages/fighting-design/_utils/load-image.ts

[4]

IntersectionObserver: https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver

[5]

MDN: https://developer.mozilla.org/zh-CN/

[6]

load-image: https://github.com/FightingDesign/fighting-design/blob/master/packages/fighting-design/_utils/load-image.ts

關注「大前端技術之路」加星標,提升前端技能

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/XFCdS0hLHJZNq_qvaMx5MQ