作一个单纯的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

相关文章
相关标签/搜索