如何设计一个好用的 React Image 组件?(1)

前言

本文为笔者阅读 react-image[1] 源码过程中的总结,若有所错漏烦请指出。✨ 仓库传送门[2]

作者:海秋

https://github.com/worldzhao/blog/issues/1

<img />可以说是开发过程中极其常用的标签了。但是很多同学都是<img src="xxx.png" />一把梭,直到 UI 小姐姐来找你谈谈人生理想:

  1. 图片加载太慢,需要展示loading占位符;

  2. 图片加载失败,加载备选图片或展示error占位符。

作为开发者的我们,可能会经历以下几个阶段:

  • 第一阶段:img标签上使用onLoad以及onError进行处理;

  • 第二阶段:写一个较为通用的组件;

  • 第三阶段:抽离 hooks,使用方自定义视图组件(当然也要提供基本组件);

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

preview.gif

useImage


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

对 react-use[3] 熟悉的同学会很容易联想到useAsync

自定义一个 hooks,接收图片链接作为参数,返回调用方需要的三个状态。

基础实现

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;

+ } = {};

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. 从第一张开始加载,若失败则加载第二张,直到某一张成功或全部失败,流程结束。类似于 tapable[4] 的AsyncSeriesBailHook

对入参进行处理:

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

): Promise {

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;

+  [key: string]: Promise;

} = {};

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 类型定义也有变化。

react-image-1

自定义 imgPromise

前面提到过,加载图片过程中,使用方可能会插入自己的逻辑,所以将 imgPromise 方法作为可选参数loadImg传入,若使用者想自定义加载方法,可传入该参数。

function useImage({

+ loadImg = imgPromise,

srcList,

}: {

+ loadImg?: (src: string) => Promise;

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 };

}

最后

我可以将最近整理的前端面试题分享出来,其中包含HTML、CSS、JavaScript、服务端与网络、Vue、浏览器、数据结构与算法等等,还在持续整理更新中,希望大家都能找到心仪的工作。

篇幅有限,仅展示部分截图:

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值