设计一款简单实用的 image 组件

前言

本文为笔者阅读 react-image 源码过程当中的总结,如有所错漏烦请指出。html

<img />能够说是开发过程当中极其经常使用的标签了。可是不少同窗都是<img src="xxx.png" />一把梭,直到 UI 小姐姐来找你谈谈人生理想:react

  1. 图片加载太慢,须要展现loading占位符;
  2. 图片加载失败,加载备选图片或展现error占位符。

做为开发者的咱们,可能会经历如下几个阶段:webpack

  • 第一阶段:img标签上使用onLoad以及onError进行处理;
  • 第二阶段:写一个较为通用的组件;
  • 第三阶段:抽离 hooks,使用方自定义视图组件(固然也要提供基本组件);

如今让咱们直接从第三阶段开始,看看如何使用少许代码打造一个易用性、封装性以及扩展性俱佳的image组件。git

Img.gif
本文仓库

若是帮助到了你请给我一颗 ✨,谢谢。github

useImage

首先分析可复用的逻辑,能够发现使用者须要关注三个状态:loadingerror以及src,毕竟加载图片也是异步请求嘛。web

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 };
}
复制代码

优化了一丢丢性能。

支持 srcList

上文提到过一点:图片加载失败,加载备选图片或展现error占位符。

展现error占位符咱们能够经过error状态去控制,可是加载备选图片的功能尚未完成。

主要思路以下:

  1. 将入参src改成srcList,值为图片url或图片(含备选图片)的url数组;
  2. 从第一张开始加载,若失败则加载第二张,直到某一张成功或所有失败,流程结束。相似于 tapableAsyncSeriesBailHook

对入参进行处理:

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,最终设置的valuepromiseFind找到的src,因此 cache 类型定义也有变化。

useImage-srcList

自定义 imgPromise

前面提到过,加载图片过程当中,使用方可能会插入本身的逻辑,因此将 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 };
}
复制代码

实现 Img 组件

完成useImage后,咱们就能够基于其实现 Img 组件了。

预先定义好相关 API:

属性 说明 类型 默认值
src 图片连接 string / string[] -
loader 可选,加载过程占位元素 ReactNode null
unloader 可选,加载失败占位元素 ReactNode null
loadImg 可选,图片加载方法,返回一个 Promise (src:string)=>Promise 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; }; 复制代码

测试效果以下:

Img.gif

结语

值得注意的是,本文遵循 react-image 大致思路,但部份内容暂未实现(因此代码可读性要好一点)。其它特性,如:

  1. 支持 Suspense 形式调用;
  2. 默认在渲染图片前会进行 decode,避免页面卡顿或者闪烁。

有兴趣的同窗能够看看下面这些文章:

本文仓库,欢迎 star😝。

相关文章
相关标签/搜索