🧑💻 寫在開頭
點贊 + 收藏 === 學會🤣🤣🤣
可能有人覺得,這個元件很簡單,沒什麼技術含量,其實確實也啥技術含量。但是,我是想借這個元件,來表達一種封裝的思想在裡面,希望可以幫助到一些朋友。
簡單的描述下這個元件的功能:
- 使用者可以點選下面顏色比較絢麗的上傳按鈕,選擇本地圖片進行上傳,也可以直接點選圖片區域進行上傳。
- 上傳過程中,會有一個上傳中的進度條,上傳完成後,會有一個上傳成功的提示,如果失敗了,會有一個上傳失敗的提示,而且支援重試。
- 可以點選圖片右上角的刪除按鈕,刪除圖片。
- 併發控制,最多隻能同時上傳 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,讓元件更加簡潔。
- 以及自頂向下的架構設計,自底向上的功能實現。
如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。