最近專案上有一個需求,在顯示圖片的時候,需要傳遞自定義的頭部就行認證,google了一番之后,發現沒有現成的組件庫可以使用【也可能是我沒找到】,所以請求圖片只能采用xhr方式來異步加載,下面就是在做這個組件庫時的一些筆記,主要關注以下兩個點:
- 圖片的等比例縮放處理
- 在請求圖片的程序中,由于是異步加載,如果后加載的一個圖片太小,而前一個圖片過大,就會有圖片顯示不正確的問題
圖片的縮放處理
最開始想到的是使用CSS 屬性background來顯示圖片,后來發現使用CSS的background-size實作按照比例縮放圖片好像有點困難,具體如下:
- 如果圖片原始的尺寸小于外層容器的尺寸,我希望它居中顯示
- 如果圖片原始尺寸大于外層尺寸
- 如果ratio > 1 (imageWidth / imageHeight),圖片應該按照寬度來進行縮放
- 如果ratio = 1, 圖片等比例縮放
- 如果ratio < 1, 圖片按照高度來縮放
因為要取到圖片的原始尺寸,使用img標簽顯示也會有點問題,所以最終采用的是new Image()這個Web Api來創建的圖片,具體代碼如下:
export const getImage = (src: string) => (
new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(new Error(NETWORK_ERROR));
image.src = https://www.cnblogs.com/rynxiao/p/src;
image.crossOrigin ='';
return image;
})
);
同時,圖片縮放的部分代碼如下:
if (ratio > 1) {
if (imageWidth > wrapperWidth) {
displayWidth = wrapperWidth;
displayHeight = parseInt(`${(1 / ratio) * wrapperWidth}`, 10);
}
} else if (ratio === 1) {
if (imageWidth > wrapperWidth) {
displayWidth = wrapperWidth;
displayHeight = wrapperWidth;
} else {
displayWidth = wrapperHeight;
displayHeight = wrapperHeight;
}
} else if (imageHeight > wrapperHeight) {
displayWidth = parseInt(`${ratio * wrapperHeight}`, 10);
displayHeight = wrapperHeight;
}
圖片的覆寫問題
因為需要進行頭部的認證,所以請求圖片的方式統一使用了XHR的方式來進行請求,然后就會造成圖片覆寫的問題,造成這個原因是,當出現了圖片地址替換的時候,比如類似下面的代碼:
const [src, setSrc] = useState(src1);
useEffect(() => { setTimeout(() => setSrc(src2)); }, [src]);
return (
<div className="App">
<Image width={50} height={100} src=https://www.cnblogs.com/rynxiao/p/{src} errorMessage="something bad happen" />
</div>
);
上述代碼中的src2會后被加載,如果src1的加載速度比src2的加載速度快倒沒有什么問題,但是反之,就會出現后加載的圖片反而被先加載的圖片進行覆寫,那么,怎么解決這個問題:
想到的辦法是,當開始加載后一個圖片時,首先進行判斷是否存在上一個加載圖片的請求,如果存在,則直接abort,類似于debounce的做法,具體的做法如下:
-
宣告一個圖片請求的類,專門用來作圖片請求
export default class ImageRequest { xmlHttpRequest: XMLHttpRequest; url: string; headers: XMLHttpRequestHeaders; setHeaders() { if (this.headers) { const keys = Object.keys(this.headers); keys.forEach((key: string) => { this.xmlHttpRequest.setRequestHeader(key, this.headers[key]); }); } } request(url: string, headers: XMLHttpRequestHeaders) { this.url = url; this.headers = headers; if (this.xmlHttpRequest) { this.xmlHttpRequest.abort(); } this.xmlHttpRequest = new XMLHttpRequest(); this.xmlHttpRequest.open('GET', this.url); this.xmlHttpRequest.responseType = 'blob'; this.setHeaders(); this.xmlHttpRequest.send(); return new Promise((resolve, reject) => { this.xmlHttpRequest.onload = () => { this.xmlHttpRequest = null; if (this.xmlHttpRequest.status === 200) { resolve(this.xmlHttpRequest.response); } else { reject(new Error(`${IMAGE_LOAD_ERROR}${this.xmlHttpRequest.statusText}`)); } }; this.xmlHttpRequest.onerror = () => { reject(new Error(NETWORK_ERROR)); }; }); } }在每個實體中維持一個
XMLHttpRequest的參考,每當進行請求的時候,首先判斷當前參考是否存在,如果存在,則直接abort,否則,則進行圖片的請求,同時在組件中,需要創建一個實體// 記住,不能在組件外部宣告實體,需要保存在每一個組件中,確保每一個組件都有一個新的請求實體 // const imageRequest: ImageRequest = new ImageRequest(); const Image: React.FC<Props> = (props) => { const [request] = useState<ImageRequest>(new ImageRequest()); useEffect(() => { if (src) { setState(LOADING_STATE.LOADING); loadImage(request, src, headers).then((img: HTMLImageElement) => { const { displayWidth, displayHeight } = getDisplayImageSize(img, width, height); const displayImage = img; displayImage.width = displayWidth; displayImage.height = displayHeight; setState({ ...LOADING_STATE.SUCCESS, image: displayImage }); }).catch(() => setState(LOADING_STATE.FAIL)); } }, [loadImage, src]); // ... };注意,這里的
ImageRequest實體只能保存在組件的state中,因為如果在組件開始使用const引入,如果一個頁面中存在多個相同組件時,就會導致多個組件共享一個request實體中的xmlHttpRequest參考,就會出現前面的圖片全部都會被abort掉的情況,
總結
看是簡單的問題,做起來也會比較復雜,口說的沒用,做起來才行,
最后,專案地址:https://github.com/Rynxiao/react-image,npm包地址:https://www.npmjs.com/package/rt-image,歡迎留言和star
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/124764.html
標籤:JavaScript
上一篇:React 元素渲染簡單演示
下一篇:React JSX簡單案例演示
