本文爲筆者閱讀 react-image 源碼過程當中的總結,如有所錯漏煩請指出。
<img />
能夠說是開發過程當中極其經常使用的標籤了。然而不少同窗都是<img src="xxx.png" />
一把梭,直到 UI 小姐姐來找你談談人生理想:html
loading
佔位符;error
佔位符。做爲開發者的咱們,可能會經歷如下幾個階段:react
img
標籤上使用onLoad
以及onError
進行處理;hooks
,使用方自定義視圖組件(固然也要提供基本組件);如今讓咱們直接從第三階段開始,看看如何使用少許代碼打造一個易用性、封裝性以及擴展性俱佳的image
組件。git
本文倉庫github
首先分析可複用的邏輯,能夠發現使用者須要關注三個狀態:loading
、error
以及src
,畢竟加載圖片也是異步請求嘛。數組
對
react-use 熟悉的同窗會很容易聯想到
useAsync
。
自定義一個 hooks,接收圖片連接做爲參數,返回調用方須要的三個狀態。promise
import * as React from 'react'; // 將圖片加載轉爲promise調用形式 function imgPromise(src: string) { return new Promise((resolve, reject) => { const i = new Image(); i.onload = () => resolve(); i.onerror = reject; i.src = src; }); } function useImage({ src, }: { src: string; }): { src: string | undefined; isLoading: boolean; error: any } { const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [value, setValue] = React.useState<string | undefined>(undefined); React.useEffect(() => { imgPromise(src) .then(() => { // 加載成功 setLoading(false); setValue(src); }) .catch((error) => { // 加載失敗 setLoading(false); setError(error); }); }, [src]); return { isLoading: loading, src: value, error: error }; }
咱們已經完成了最基礎的實現,接下來慢慢優化。緩存
對於同一張圖片來說,在組件 A 加載過的圖片,組件 B 不用再走一遍new Image()
的流程,直接返回上一次結果便可。性能優化
+ const cache: { + [key: string]: Promise<void>; + } = {}; function useImage({ src, }: { src: string; }): { src: string | undefined; isLoading: boolean; error: any } { const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [value, setValue] = React.useState<string | undefined>(undefined); React.useEffect(() => { + if (!cache[src]) { + cache[src] = imgPromise(src); + } - imgPromise(src) + cache[src] .then(() => { setLoading(false); setValue(src); }) .catch(error => { setLoading(false); setError(error); }); }, [src]); return { isLoading: loading, src: value, error: error }; }
優化了一丟丟性能。異步
上文提到過一點:圖片加載失敗,加載備選圖片或展現error
佔位符。性能
展現error
佔位符能夠經過error
狀態去控制,但加載備選圖片的功能還未完成。
主要思路以下:
src
改成srcList
,值爲圖片url
或圖片(含備選圖片)的url
數組;對入參進行處理:
const removeBlankArrayElements = (a: string[]) => a.filter((x) => x); const stringToArray = (x: string | string[]) => (Array.isArray(x) ? x : [x]); function useImage({ srcList, }: { srcList: string | string[]; }): { src: string | undefined; loading: boolean; error: any } { // 獲取url數組 const sourceList = removeBlankArrayElements(stringToArray(srcList)); // 獲取用於緩存的鍵名 const sourceKey = sourceList.join(''); }
接下來就是重要的加載流程啦,定義promiseFind
方法,用於完成以上加載圖片的邏輯。
/** * 注意 此處將imgPromise做爲參數傳入,而沒有直接使用imgPromise * 主要是爲了擴展性 * 後面會將imgPromise方法做爲一個參數由使用者傳入,使得使用者加載圖片的操做空間更大 * 固然若使用者不傳該參數,就是用默認的imgPromise方法 */ function promiseFind( sourceList: string[], imgPromise: (src: string) => Promise<void> ): Promise<string> { let done = false; // 從新使用Promise包一層 return new Promise((resolve, reject) => { const queueNext = (src: string) => { return imgPromise(src).then(() => { done = true; // 加載成功 resolve resolve(src); }); }; const firstPromise = queueNext(sourceList.shift() || ''); // 生成一條promise鏈[隊列],每個promise都跟着catch方法處理當前promise的失敗 // 從而繼續下一個promise的處理 sourceList .reduce((p, src) => { // 若是加載失敗 繼續加載 return p.catch(() => { if (!done) return queueNext(src); return; }); }, firstPromise) // 全都掛了 reject .catch(reject); }); }
再來改動useImage
。
const cache: { - [key: string]: Promise<void>; + [key: string]: Promise<string>; } = {}; function useImage({ - src, + srcList, }: { - src: string; + srcList: string | string[]; }): { src: string | undefined; loading: boolean; error: any } { const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [value, setValue] = React.useState<string | undefined>(undefined); // 圖片連接數組 + const sourceList = removeBlankArrayElements(stringToArray(srcList)); // cache惟一鍵名 + const sourceKey = sourceList.join(''); React.useEffect(() => { - if (!cache[src]) { - cache[src] = imgPromise(src); - } + if (!cache[sourceKey]) { + cache[sourceKey] = promiseFind(sourceList, imgPromise); + } - cache[src] - .then(() => { + cache[sourceKey] + .then((src) => { setLoading(false); setValue(src); }) .catch(error => { setLoading(false); setError(error); }); }, [src]); return { isLoading: loading, src: value, error: error }; }
須要注意的一點:如今傳入的圖片連接可能不是單個src
,最終設置的value
爲promiseFind
找到的src
,因此 cache
類型定義也有變化。
前面提到過,加載圖片過程當中,使用方可能會插入本身的邏輯,因此將 imgPromise
方法做爲可選參數loadImg
傳入,若使用者想自定義加載方法,可傳入該參數。
function useImage({ + loadImg = imgPromise, srcList, }: { + loadImg?: (src: string) => Promise<void>; srcList: string | string[]; }): { src: string | undefined; loading: boolean; error: any } { const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [value, setValue] = React.useState<string | undefined>(undefined); const sourceList = removeBlankArrayElements(stringToArray(srcList)); const sourceKey = sourceList.join(''); React.useEffect(() => { if (!cache[sourceKey]) { - cache[sourceKey] = promiseFind(sourceList, imgPromise); + cache[sourceKey] = promiseFind(sourceList, loadImg); } cache[sourceKey] .then(src => { setLoading(false); setValue(src); }) .catch(error => { setLoading(false); setError(error); }); }, [sourceKey]); return { loading: loading, src: value, error: error }; }
完成useImage
後,咱們就能夠基於其實現 Img
組件了。
預先定義好相關 API:
屬性 | 說明 | 類型 | 默認值 |
---|---|---|---|
src | 圖片連接 | string / string[] | - |
loader | 可選,加載過程佔位元素 | ReactNode | null |
unloader | 可選,加載失敗佔位元素 | ReactNode | null |
loadImg | 可選,圖片加載方法,返回一個 Promise | (src:string)=>Promise<void> | imgPromise |
固然,除了以上 API,還有<img />
標籤原生屬性。編寫類型聲明文件以下:
export type ImgProps = Omit< React.DetailedHTMLProps< React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement >, 'src' > & Omit<useImageParams, 'srcList'> & { src: useImageParams['srcList']; loader?: JSX.Element | null; unloader?: JSX.Element | null; };
實現以下:
export default ({ src: srcList, loadImg, loader = null, unloader = null, ...imgProps }: ImgProps) => { const { src, loading, error } = useImage({ srcList, loadImg, }); if (src) return <img src={src} {...imgProps} />; if (loading) return loader; if (error) return unloader; return null; };
測試效果以下:
比較簡單,就很少說啦。
本文倉庫,歡迎 star😝。
值得注意的是,本文遵循 react-image
大致思路,但部份內容暫未實現(因此代碼可讀性要好一點)。其它特性,如:
有興趣的同窗能夠看看下面這些文章: