作一個單純的react-image顯示組件[with headers]

最近項目上有一個需求,在顯示圖片的時候,須要傳遞自定義的頭部就行認證。google了一番以後,發現沒有現成的組件庫能夠使用【也多是我沒找到】,因此請求圖片只能採用xhr方式來異步加載。下面就是在作這個組件庫時的一些筆記,主要關注如下兩個點:javascript

  • 圖片的等比例縮放處理
  • 在請求圖片的過程當中,因爲是異步加載,若是後加載的一個圖片過小,而前一個圖片過大,就會有圖片顯示不正確的問題

圖片的縮放處理

最開始想到的是使用CSS 屬性background來顯示圖片,後來發現使用CSS的background-size實現按照比例縮放圖片好像有點困難,具體以下:java

  • 若是圖片原始的尺寸小於外層容器的尺寸,我但願它居中顯示
  • 若是圖片原始尺寸大於外層尺寸
    • 若是ratio > 1 (imageWidth / imageHeight),圖片應該按照寬度來進行縮放
    • 若是ratio = 1, 圖片等比例縮放
    • 若是ratio < 1, 圖片按照高度來縮放

由於要取到圖片的原始尺寸,使用img標籤顯示也會有點問題。因此最終採用的是new Image()這個Web Api來建立的圖片。具體代碼以下:react

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 = src;
    image.crossOrigin = '';
    return image;
  })
);
複製代碼

同時,圖片縮放的部分代碼以下:git

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的方式來進行請求,而後就會形成圖片覆蓋的問題。形成這個緣由是,當出現了圖片地址替換的時候,好比相似下面的代碼:github

const [src, setSrc] = useState(src1);
useEffect(() => { setTimeout(() => setSrc(src2)); }, [src]);

return (
  <div className="App"> <Image width={50} height={100} src={src} errorMessage="something bad happen" /> </div> ); 複製代碼

上述代碼中的src2會後被加載,若是src1的加載速度比src2的加載速度快倒沒有什麼問題,可是反之,就會出現後加載的圖片反而被先加載的圖片進行覆蓋。那麼,怎麼解決這個問題:npm

想到的辦法是,當開始加載後一個圖片時,首先進行判斷是否存在上一個加載圖片的請求,若是存在,則直接abort,相似於debounce的作法。具體的作法以下:markdown

  • 聲明一個圖片請求的類,專門用來做圖片請求app

    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,不然,則進行圖片的請求。異步

  • 在組件中,建立一個request實例,同時將其維護在state中oop

    // 記住,不能在組件外部聲明實例,須要保存在每個組件中,確保每個組件都有一個新的請求實例
    // 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掉的狀況。

總結

看是簡單的問題,作起來也會比較複雜,口說的沒用,作起來才行。

最後,項目地址:github.com/Rynxiao/rea…,npm包地址:www.npmjs.com/package/rt-…,歡迎留言和star

相關文章
相關標籤/搜索