前言
项目遇到一个需要优化的点。就是公司的网要么都比较差,在点击 Image 查看预览时,会加载很久,并且没有loading态。而且来回切换时,每次都重新请求,没有缓存。我们的图片是存在七牛的,而七牛并没有缓存,所以前端通过 base64 来实现缓存
思路
请先看原本的代码:
import { IImgDTO } from "@/service/base/v1/organization/get-page";
import { IObj } from "@/utils/interface";
import { classNames } from "@/utils/tools";
import { GetProps, Image, Skeleton } from "antd";
import { useEffect, useRef, useState } from "react";
type PreviewGroupType = GetProps<typeof Image.PreviewGroup>;
export interface ITXImagesRenderProps extends PreviewGroupType {
imgs: IImgDTO[];
showCount?: number;
width?: number;
height?: number;
className?: string;
/** @param 仅展示第一张,剩余的预览查看 */
albumFlag?: boolean;
/** @param 更多图标的类名 */
moreClassName?: string;
}
export const TXImagesRender = function TXImagesRender_(
props: ITXImagesRenderProps
) {
const {
imgs = [],
showCount = 2,
className = "",
width = 32,
height = width,
albumFlag = false,
moreClassName = "",
...reset
} = props;
const [visible, setVisible] = useState(false);
const [current, setCurrent] = useState(0);
if (!imgs.length) {
return "-";
}
let showImgs = imgs;
let num = 0;
if (showImgs.length > showCount) {
num = showImgs.length - showCount + 1;
showImgs = imgs.slice(0, showCount - 1);
}
if (albumFlag) {
num = 0;
showImgs = imgs.slice(0, 1);
}
return (
<Image.PreviewGroup
{...reset}
items={imgs?.map?.((r) => r.fullPath)}
preview={{
visible,
onVisibleChange: setVisible,
current,
onChange: (index) => setCurrent(index),
}}
>
<div className={classNames({ "flex gap-1": true, [className]: true })}>
{showImgs.map((r, rIndx) => {
return (
<Image
key={rIndx}
width={width}
height={height}
src={r.fullThumbnailPath}
onClick={() => {
setCurrent(rIndx);
}}
/>
);
})}
<div
className={classNames({
"bg-gray-400/30 cursor-pointer flex items-center justify-center rounded":
true,
hidden: !num,
[moreClassName]: true,
})}
onClick={() => {
setCurrent(showCount - 1);
setVisible(true);
}}
style={{
height: `${height}px`,
width: `${width}px`,
}}
>
+{num}
</div>
</div>
</Image.PreviewGroup>
);
};
通过base64手动缓存图片来实现优化方案
新的代码:
import { IImgDTO } from "@/service/base/v1/organization/get-page";
import { IObj } from "@/utils/interface";
import { classNames } from "@/utils/tools";
import { GetProps, Image, Skeleton } from "antd";
import { useEffect, useRef, useState } from "react";
type PreviewGroupType = GetProps<typeof Image.PreviewGroup>;
export interface ITXImagesRenderProps extends PreviewGroupType {
imgs: IImgDTO[];
showCount?: number;
width?: number;
height?: number;
className?: string;
/** @param 仅展示第一张,剩余的预览查看 */
albumFlag?: boolean;
/** @param 更多图标的类名 */
moreClassName?: string;
}
export const TXImagesRender = function TXImagesRender_(
props: ITXImagesRenderProps
) {
const {
imgs = [],
showCount = 2,
className = "",
width = 32,
height = width,
albumFlag = false,
moreClassName = "",
...reset
} = props;
const [visible, setVisible] = useState(false);
const [current, setCurrent] = useState(0);
const imgRef = useRef<HTMLImageElement | null>(null);
const [base64Obj, setBase64Obj] = useState<IObj>({});
const [items, setItems] = useState<string[]>(imgs?.map?.((r) => r.fullPath));
const handleImageLoaded = (url: string) => {
if (base64Obj[url]) {
return;
}
try {
const img = imgRef.current;
if (!img) return;
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.drawImage(img, 0, 0);
const dataURL = canvas.toDataURL("image/png");
setBase64Obj((prev) => ({
...prev,
[url]: dataURL,
}));
setItems((o) => {
const list = [...o];
list[current] = dataURL;
return list;
});
} catch (err) {
console.log(err);
console.warn("无法生成Base64(可能因跨域图片)", err);
}
};
useEffect(() => {
setItems(
imgs.map((r) => {
return base64Obj[r.fullPath] || '';
})
);
}, imgs);
if (!imgs.length) {
return "-";
}
let showImgs = imgs;
let num = 0;
if (showImgs.length > showCount) {
num = showImgs.length - showCount + 1;
showImgs = imgs.slice(0, showCount - 1);
}
if (albumFlag) {
num = 0;
showImgs = imgs.slice(0, 1);
}
return (
<Image.PreviewGroup
{...reset}
items={items}
preview={{
imageRender: (node, info) => {
return (
<>
{!info.image.url && <Skeleton.Image active />}
<img
className='hidden'
src={base64Obj[imgs[current].fullPath] ?? imgs[current].fullPath}
ref={imgRef}
onLoad={() => handleImageLoaded(imgs[current].fullPath)}
crossOrigin="anonymous"
/>
{info.image.url && node}
</>
);
},
visible,
onVisibleChange: setVisible,
current,
onChange: (index) => {
setCurrent(index);
},
}}
>
<div className={classNames({ "flex gap-1": true, [className]: true })}>
{showImgs.map((r, rIndx) => {
return (
<Image
key={rIndx}
width={width}
height={height}
src={r.fullThumbnailPath}
onClick={() => {
setCurrent(rIndx);
}}
/>
);
})}
<div
className={classNames({
"bg-gray-400/30 cursor-pointer flex items-center justify-center rounded":
true,
hidden: !num,
[moreClassName]: true,
})}
onClick={() => {
setCurrent(showCount - 1);
setVisible(true);
}}
style={{
height: `${height}px`,
width: `${width}px`,
}}
>
+{num}
</div>
</div>
</Image.PreviewGroup>
);
};