🧑 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
可能有人觉得,这个组件很简单,没什么技术含量,其实确实也啥技术含量。但是,我是想借这个组件,来表达一种封装的思想在里面,希望可以帮助到一些朋友。
简单的描述下这个组件的功能:
- 用户可以点击下面颜色比较绚丽的上传按钮,选择本地图片进行上传,也可以直接点击图片区域进行上传。
- 上传过程中,会有一个上传中的进度条,上传完成后,会有一个上传成功的提示,如果失败了,会有一个上传失败的提示,而且支持重试。
- 可以点击图片右上角的删除按钮,删除图片。
- 并发控制,最多只能同时上传 N 张图片,也就是所谓的限频,这里是 2 张。
是不是看了这么多功能之后,就开始有点头皮发麻了?哈哈,不要怕,这就带你了解下,如何拆解这种功能,而且,学会了这种拆解的办法,后面你遇到更加复杂的,也可以得心应手。
拆解功能,逐步实现
首先,我们思考,我们该使用自底向上的思路,还是自顶向下的思路来拆解这个功能呢?我的建议自顶向下的思路去思考架构,然后自底向上的去实现功能。
因为我们这个图片上传组件是支持多长图片同时上传的,而且,我们还需要支持上传失败重试的功能,所以,为了让功能更加聚焦,我们把关注点放在 PhotoItem 上,没一个 PhotoItem 就是一个图片上传的单元。他可以独立的上传,独立的删除,独立的重试。
那么,为了让 PhotoItem 这个组件更加简洁,我们把上传逻辑放在hooks useUpload
中,这样,PhotoItem 只需要关注自己的展示逻辑即可。
这样做的目的是做到关注点分离,通常来讲,也是符合单一职责原则的。写出来的组件维护性必定大大提升。
代码实现
我们先来看下 useUpload 的代码,因为PhotoItem 依赖他,我们先实现它。
"use client"; export const useUploader = (uploadAction) => { const [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(null); const upload = useCallback(async (file) => { setIsUploading(true); setError(null); try { return await uploadAction(file); } catch (err) { setError(err.message || 'Upload failed'); } finally { setIsUploading(false); } }, [uploadAction]); const reset = useCallback(() => { setIsUploading(false); setError(null); }, []); return { upload, isUploading, error, reset }; };
可以看到,我们的 hooks 非常之简单,就是暴露了一个实现图片上传的狗子 upload,然而,他替我们的组件管理了上传中
,上传失败
,的状态,因此,接下来看,我们的PhotoItem 组件将会有多清晰。
export const PhotoItem = ({ file, onRemove, onUploadComplete, onUploadError, }) => { const { upload, isUploading, error, reset } = useUploader(); const startUpload = useCallback(async () => { try { const url = await upload(file); onUploadComplete(url); } catch (err) { onUploadError(); } }, [file, upload, onUploadComplete, onUploadError]); useEffect(() => { startUpload(); }, [queueUpload, startUpload]); const handleRetry = () => { reset(); startUpload(); }; return ( <div className="relative w-full h-20"> <img src={URL.createObjectURL(file)} /> {!isUploading && !error( Uploaded )} {isUploading && ( <Progress /> )} {error && ( <span>Failed</span> )} </div> ); };
OK,到目前为止,还是极其简单的,但是我们貌似忘记了一个很核心的功能,限制并发数。为什么要限制并发数,因为我们自己的服务器或者三方的服务器,可能会有并发数的限制,如果我们不限制并发数,可能会导致一次传多张图片是卡住。
思考,如何限制并发数
我们想一样,是谁触发了上传的呢?是不是 PhotoItem 组件呢?是的,我们可以在 PhotoItem 组件中,去控制并发数,但是,这样做,会导致 PhotoItem 组件的逻辑变得复杂,因为他不仅要关注自己的展示逻辑,还要关注并发数的控制逻辑。这就显的不太合适了。所以,我们应该把他丢出去对吧,截止到目前为止,我们的PhotoUploader 这个组件似乎并没有干任何事情,我们思考下,并发控制的逻辑是否应该是他来呢?
答案是显然的,我们应该把并发控制的逻辑放在 PhotoUploader 组件中,因为他是整个上传组件的入口,他应该关注并发控制,而不是 PhotoItem 组件,而且最本质的原因是,PhotoItem 也不关心是否有其他的 PhotoItem 。
那么,问题来了,并发控制怎么写呢?使用什么数据结构较为合适呢?不卖关子了,我们知道,队列是最合适的数据结构,因为他是先进先出的,我们可以把上传任务放在队列中,然后,每次上传完成,就从队列中取出一个任务,继续上传。
好,我们改造一下,我们的 PhotoItem 组件,让他不要直接执行上传逻辑,而是把他做成一个任务,然后,把任务放在队列中,然后,我们在 PhotoUploader 组件中,去控制并发数。
export const PhotoItem = ({ file, onRemove, ... queueUpload // 加一个队列操作器 }) => { const { upload, isUploading, error, reset } = useUploader(); ... useEffect(() => { queueUpload(startUpload); // 修改这里 }, [queueUpload, startUpload]); const handleRetry = () => { reset(); queueUpload(startUpload);//修改这里 }; // .... 其他几乎不变
在来看看我们的 PhotoUploader 组件,他是如何控制并发数的。很简单,我们只需要维护一个队列,然后,每次上传完成,就从队列中取出一个任务,继续上传。
const processQueue = useCallback(() => { while (activeUploadsRef.current < MAX_CONCURRENT_UPLOADS && uploadQueueRef.current.length > 0) { const nextUpload = uploadQueueRef.current.shift(); activeUploadsRef.current++; nextUpload(); } }, []); const queueUpload = useCallback((startUpload) => { if (activeUploadsRef.current < MAX_CONCURRENT_UPLOADS) { activeUploadsRef.current++; startUpload(); } else { uploadQueueRef.current.push(startUpload); } }, []);
这里,只给出最最核心的逻辑,实际上就是维护的了一个任务队列,然后,每次上传完成,就判断下队列中是否还有任务,并且是否超过了并发数,如果没有超过,并且队列中还有任务,就继续上传。仅此而已。
总结一下
这个图片上传组件,看似简单,但是,他涉及到了很多的知识点,比如并发控制,上传失败重试,组件拆解,自顶向下的架构设计,自底向上的功能实现。我们在实现这个组件的过程中。有过很多的思考,比如:
- 如何拆解功能,让组件更加聚焦,做到关注点分离。
- 控制并发数,使用队列是最合适的数据结构。
- 如何设计一个 hooks,让组件更加简洁。
- 以及自顶向下的架构设计,自底向上的功能实现。
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。
1.本站内容仅供参考,不作为任何法律依据。用户在使用本站内容时,应自行判断其真实性、准确性和完整性,并承担相应风险。
2.本站部分内容来源于互联网,仅用于交流学习研究知识,若侵犯了您的合法权益,请及时邮件或站内私信与本站联系,我们将尽快予以处理。
3.本文采用知识共享 署名4.0国际许可协议 [BY-NC-SA] 进行授权
4.根据《计算机软件保护条例》第十七条规定“为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬。”您需知晓本站所有内容资源均来源于网络,仅供用户交流学习与研究使用,版权归属原版权方所有,版权争议与本站无关,用户本人下载后不能用作商业或非法用途,需在24个小时之内从您的电脑中彻底删除上述内容,否则后果均由用户承担责任;如果您访问和下载此文件,表示您同意只将此文件用于参考、学习而非其他用途,否则一切后果请您自行承担,如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。
5.本站是非经营性个人站点,所有软件信息均来自网络,所有资源仅供学习参考研究目的,并不贩卖软件,不存在任何商业目的及用途
暂无评论内容