關於圖片加載,你需要學習一下
作者:田同學 2001
https://juejin.cn/post/7134706181863374878
😊 從何而來
這篇文章,出自我自己的開源組件庫 fighting-design[1] 中的 Avatar 頭像 [2] 組件的 load-image[3] 類。
相比於其它的靜態組件,像圖片加載這種的組件,內部我做了很多的優化,對於圖片的加載和錯誤的處理,我都儘可能的將每種可能出現的結果都考慮到,針對每種不確定的結果做出相應的提示,以便於提升用戶體驗。
🥕 設計思路
我的設計想法是:通過一個加載類,傳入 dom
元素、 props
和 emit
。先創建出一個虛擬的 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
賦值並加載。監聽上面的 error
和 load
事件,即可監聽到圖片是否加載成功,以便做出不同的狀態。
🚩 成功和失敗
對於成功或失敗的處理,我新增了 onerror
和 onload
方法,來處理加載成功和失敗之後的不同處理狀態
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)
}
}
但是上面代碼存在兩個問題:
- 首先我們發現,在
onload
加載成功的方法中,將真實dom
始終賦值的始終 是src
:
onload = (evt) => {
// 始終賦值爲 props.src
this.node.src = this.props.src
}
但是 src
並不是始終可以加載成功的,所以還是需要動態的去將真正加載成功的 src
傳給 onload
方法,那麼真正加載成功的 src
也就是在 load
方法中。並且還要加入成功的 emit
。
- 其次,在處理加載失敗的
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