分享一個看起來很酷的圖片上傳元件

林恒發表於2024-07-25

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

可能有人覺得,這個元件很簡單,沒什麼技術含量,其實確實也啥技術含量。但是,我是想借這個元件,來表達一種封裝的思想在裡面,希望可以幫助到一些朋友。

簡單的描述下這個元件的功能:

  • 使用者可以點選下面顏色比較絢麗的上傳按鈕,選擇本地圖片進行上傳,也可以直接點選圖片區域進行上傳。
  • 上傳過程中,會有一個上傳中的進度條,上傳完成後,會有一個上傳成功的提示,如果失敗了,會有一個上傳失敗的提示,而且支援重試。
  • 可以點選圖片右上角的刪除按鈕,刪除圖片。
  • 併發控制,最多隻能同時上傳 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,讓元件更加簡潔。
  • 以及自頂向下的架構設計,自底向上的功能實現。

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。

分享一個看起來很酷的圖片上傳元件

相關文章