Neverending Programming Mistakes

    react-img-worker
    TypeScript icon, indicating that this package has built-in type declarations

    1.1.5 • Public • Published

    图片加载占据了一个 web 应用的较多资源。

    这篇文章中,我们实现一个 ImgWorker 组件,满足以下功能:

    • image 可选择的 worker 加载,并且做好优雅降级
    • decode 解码,并且做好优雅降级
    • miniSrc 和 src 竞速加载

    注意,在性能较低的机器,开启 worker 进程下载的开销大于直接使用主进程下载的开销; 使用 worker 需要主动传递 worker 属性为 true 开启

    组件代码

    先上代码,后续对关键代码做一些解释:

    src/react-img-worker.js :

    import * as React from 'react';
     
    const blobUrl = new Blob(
      [
        `
    self.addEventListener('message', event => {
      const [url, type] = event.data;
      fetch(url, {
          method: 'GET',
          mode: 'no-cors',
          cache: 'default'
      }).then(response => {
          return response.blob();
      }).then(_ => postMessage([url, type])).catch(console.error);
    })
    `,
      ],
      { type: 'application/javascript' }
    );
     
    interface IImgWorkerProps
      extends React.DetailedHTMLProps<
        React.ImgHTMLAttributes<HTMLImageElement>,
        HTMLImageElement
      > {
      boxProps?: React.DetailedHTMLProps<
        React.ImgHTMLAttributes<HTMLImageElement>,
        HTMLImageElement
      >;
      miniSrc?: string;
      objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
      renderLoading?: any;
      worker?: boolean;
    }
     
    interface IImgWorkerState {
      isLoading: boolean;
      src: string;
    }
     
    export class ImgWorker extends React.Component<
      IImgWorkerProps,
      IImgWorkerState
    > {
      public static defaultProps = {
        objectFit: 'cover',
      };
     
      public div: any = null;
      public image: HTMLImageElement = new Image();
      public isLoadedSrcLock = false;
      public state = {
        isLoading: true,
        src: '',
      };
      public worker: Worker = null as any;
      public constructor(props: IImgWorkerProps) {
        super(props);
        this.image.style.width = '100%';
        this.image.style.height = '100%';
        this.image.style.display = 'none';
        this.image.style.objectFit = this.props.objectFit!;
     
        // 如果使用 worker 并且浏览器支持 worker
        if (this.props.worker && typeof Worker !== 'undefined') {
          this.worker = new Worker(URL.createObjectURL(blobUrl));
          this.worker.addEventListener('message', event => {
            const [url, type] = event.data;
     
            this.loadImage(url, type);
          });
        }
      }
     
      public componentDidMount() {
        this.div.appendChild(this.image);
        this.postMessage(this.props);
      }
     
      public componentWillReceiveProps(nextProps: IImgWorkerProps) {
        if (nextProps.objectFit !== this.props.objectFit) {
          this.image.style.objectFit = nextProps.objectFit!;
        }
        let isPostMessage = false;
        if (nextProps.miniSrc !== this.props.miniSrc) {
          isPostMessage = true;
        }
        if (nextProps.src !== this.props.src) {
          isPostMessage = true;
        }
     
        // 如果 src 或 miniSrc 更新,重新请求
        if (isPostMessage) {
          this.isLoadedSrcLock = false;
          this.postMessage(nextProps);
        }
      }
     
      public componentWillUnmount() {
        if (this.image) {
          this.image.onload = null;
          this.image.onerror = null;
        }
        if (this.worker) {
          this.worker.terminate();
        }
      }
     
      public loadImage = (url: string, type: string) => {
        // 如果 src 已经被设置,拦截后续的更新
        if (this.isLoadedSrcLock) {
          return;
        }
        if (type === 'src') {
          this.isLoadedSrcLock = true;
        }
     
        this.image.src = url;
        this.image.decoding = 'async';
        this.image.decode !== undefined
          ? this.image
              .decode()
              .then(this.onLoad)
              .catch(this.onLoad)
          : (this.image.onload = this.onLoad);
      };
     
      public onLoad = () => {
        this.image.style.display = 'block';
        this.setState({
          src: this.image.src,
          isLoading: false,
        });
      };
     
      public postMessage = (props: IImgWorkerProps) => {
        if (props.miniSrc) {
          if (this.worker) {
            this.worker.postMessage([props.miniSrc, 'miniSrc']);
          } else {
            this.loadImage(props.miniSrc, 'miniSrc');
          }
        }
     
        if (props.src) {
          if (this.worker) {
            this.worker.postMessage(this.worker.postMessage([props.src, 'src']));
          } else {
            this.loadImage(props.src, 'miniSrc');
          }
        }
      };
     
      public render() {
        const { boxProps, renderLoading: Loading, src: _src, ...rest } = this.props;
        const { isLoading } = this.state;
     
        return (
          <div ref={r => (this.div = r)} {...rest}>
            {Loading && isLoading && (
              <Loading key="img-worker-loading" isLoaing={isLoading} />
            )}
          </div>
        );
      }
    }

    代码是 typescript 编写的,这是为了组件发版可以更简便的生成.d.ts 文件,内容很简单,其中关键在于两处:

    1. 创建一个 worker
    this.worker = new Worker(URL.createObjectURL(blobUrl));
    1. 使用 decode

    decode 的功能网上有许多文章,这里不展开

    const image = new Image();
    image.src = url;
    image.decoding = 'async';
    image.decode !== undefined
      ? image
          .decode()
          .then(this.onLoad)
          .catch(this.onLoad)
      : (image.onload = this.onLoad);

    使用

    使用默认 image loader, ImgWorker 保留了 <img /> 标签原有的所有 api

    import { ImgWorker } from './react-img-worker';
     
    export default () => {
      return <ImgWorker src="http://example.png" />;
    };

    使用 worker loader:

    import { ImgWorker } from './react-img-worker';
     
    export default () => {
      return <ImgWorker worker src="http://example.png" />;
    };

    支持 mini 图片,如果 设置了 miniSrc 会并行加载 miniSrc 和 src;若 miniSrc 优先加载完,显示 miniSrc;若 src 优先加载完会拦截此次后续的图片更新。

    使用 worker + miniSrc + src:

    import { ImgWorker } from './react-img-worker';
     
    export default () => {
      return (
        <ImgWorker
          worker
          miniSrc="http://example.mini.png"
          src="http://example.png"
        />
      );
    };

    直接使用 react-img-worker 库

    为了方便使用,以上代码已发布至 npm,可以直接使用

    $ yarn add react-img-worker

    希望有对君有帮助:)

    Keywords

    none

    Install

    npm i react-img-worker

    DownloadsWeekly Downloads

    1

    Version

    1.1.5

    License

    MIT

    Unpacked Size

    11 kB

    Total Files

    4

    Last publish

    Collaborators

    • ymblender