<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

        管理后臺基于 Ant Design Pro 搭建,組件庫是 Ant Design 5.20,本文會對糟糕的性能和用戶體驗進行多輪優化。

      一、存在的問題

        核心就是上傳的圖像數量龐大,公司的網絡速度慢,被全國94%的網絡用戶超越。

        1

      1)預覽圖顯示慢

        2

        3

      2)圖像請求失敗

        上傳組件預覽圖請求失敗圖裂。

        4

        點擊上傳組件中的預覽按鈕,在大圖預覽時的可拖動區域的小圖,也會請求失敗圖裂

        5

      3)上傳時間長

        上傳時間長,頁面意外關閉需重新上傳。

        6

        接下來會記錄優化的整個過程,其中不乏一些失敗的優化手段,我都做了一一記錄。

        性能優化的核心是降低圖像尺寸,分批次請求等,體驗優化的核心是增加反饋交互,續傳文件等。

      二、預覽小圖優化

        首先讓運營做了一個人工的前置優化。

        原始圖先讓攝影師自行壓縮一遍,從 4.5M 壓縮到 1.5M,總容量從 2.19G 縮小到 466M。

      1)質量變換

        為預覽小圖增加質量變換(公司購買了七牛云的服務),經過調試,選擇 3p 的圖片質量。

      https://static.xxx.com/activity/review/1746525272362kzrjtd.jpg

        7

      https://static.xxx.com/activity/review/1746525272362kzrjtd.jpg?imageMogr2/thumbnail/!5p

        8

      https://static.xxx.com/activity/review/1746525272362kzrjtd.jpg?imageMogr2/thumbnail/!3p

        9

        三次請求的圖像尺寸縮小了 538 倍,請求效率提升了不少。

        10

      2)批量預請求

        另一個優化舉措是在上傳之前先用腳本批量(10個一組)去請求壓縮裁剪后的圖。

        在全部請求完成后才呈現上傳區域。

      useEffect(() => {
        // 預加載圖像
        const imageLoader = new BatchImageLoader({ batchSize: 10 });
        const imageUrls = current.reviews ? current.reviews.map(url => scaleImg(url)) : [];
        // 批量加載
        imageUrls.length > 0 && imageLoader.loadImagesBatch(imageUrls)
            .then((results: ImageLoadResult[]) => {
              setImgLoading(false); // 關閉加載中的狀態
            })
            .catch((error: Error) => {
              console.error('批量加載失敗:', error);
        });
        return () => {
          imageLoader.clearPendingRequests(); //銷毀請求
          setImgLoading(true); // 開啟加載中的狀態
        }
      }, [current])

        11

      3)骨架屏

        還給 Modal 組件賦值 loading 屬性(5.18.0新增),出現骨架屏效果。

        12

        不過,在骨架屏消失后,本來以為預請求了一次,不會再訪問出錯,結果還是會有一定概率出錯。

        公司網絡比較慢,很容易復現此類問題。

        4

      4)3次重復請求

        并且從上傳區域的預加載,到加載組件內的預覽圖,再到拖動區域的小圖,總共進行了 3 次請求,太冗余。

        14

        上述優化,除了質量參數達到了優化的效果,其余沒有達到預期,遂放棄。

      三、自定義上傳列表項

      1)懶加載

        Upload 組件的默認行為,是會加載組件內的所有預覽圖,所以需要自定義上傳列表項(itemRender),做懶加載優化。

      const itemRender = (originNode: React.ReactElement, file: UploadFile, fileList:UploadFile[], actions: {
          download: () => void;
          preview: () => void;
          remove: () => void;
        }) => {
          return (
            <div className="ant-upload-list-item-container">
              <div className="ant-upload-list-item ant-upload-list-item-done">
              {file.status === 'uploading' ? (
                <div style={{width: '100%', textAlign:'center'}}>
                  文件上傳中<Progress percent={file.percent} showInfo={false} size="small" strokeWidth={2}/>
                </div>
              ) : 
                <span className="ant-upload-list-item-thumbnail">
                  <img 
                    src={file.thumbUrl} 
                    alt={file.name} 
                    className="ant-upload-list-item-image" 
                    loading='lazy' 
                    onError={e => onImgError(e, file.thumbUrl)}
                  />
                </span>
              }
                <div className="ant-upload-list-item-actions">
                  {/* 預覽按鈕 */}
                  <a onClick={actions.preview}>
                    <EyeOutlined />
                  </a>
                  {/* 刪除按鈕 */}
                  <a onClick={actions.remove}>
                    <DeleteOutlined />
                  </a>
                </div>
              </div>
            </div>
          );
        }

        自定義的列表項與默認的列表項在行為和樣式上保持了高度的一致。

        但是為 img 增加了 loading 懶加載屬性。

      • loading="lazy" 告訴瀏覽器,在用戶滾動至圖片附近時才開始加載圖片。
      • loading="eager"(這是默認值)則指示瀏覽器在頁面加載時立即加載所有圖片。

        經過調試發現,300個圖像請求,默認會先請求100個左右。

        15

        盡管如此,還是會有小概率的情況出現圖裂。

      2)重加載一次

        為此,在圖像請求錯誤時,增加一次重新加載的機制,而可拖動區域的小圖也要加上此機制。

      // 預覽圖像發生錯誤時的事件 data-retried
      const onImgError = (e:SyntheticEvent, src:string|undefined) => {
          const img = e.target as HTMLImageElement;
          // 只重新加載一次
          if (!img.dataset.retried) {
            img.dataset.retried = 'true';
            src && (img.src = src);
            console.error('重新加載', src);
          }
      }

      四、可拖動區域

        5

      1)網絡錯誤

        拖動區域的小圖裂掉主要是因為上傳區域的圖裂了導致,兩者的地址是相同的。

        得到了圖裂時候的錯誤信息。

      https://static.xxx.com/activity/review/1757929803424ukyomp.jpg?imageMogr2/thumbnail/!3p net::ERR_HTTP2_PROTOCOL_ERROR 200 (OK)

        在客戶端(你的瀏覽器)和服務器之間的 HTTP/2 通信過程中,發生了某種違反協議規則的事情,導致連接被中止。

        查看 Chrome Net Logs (網絡日志),它可以記錄所有網絡活動。

      1. 在 Chrome 地址欄輸入 chrome://net-export/。
      2. 點擊 "Start Logging to Disk" 并保存一個文件。
      3. 在瀏覽器中復現這個錯誤。
      4. 返回 chrome://net-export/ 點擊 "Stop Logging"。
      5. 將日志文件上傳到 https://netlog-viewer.appspot.com 進行分析。搜索 ERR_HTTP2_PROTOCOL_ERROR,可以看到更詳細的錯誤上下文。

        上述操作完成后,并沒有得到具體的錯誤信息,可能是日志文件還沒有全部導出。

      2)懶加載

        但是在拖動區域,也增加了 loading 屬性,有時候即使組件內的預覽圖沒有顯示,此處也能正確顯示。

      <img src={item.thumbUrl} loading='lazy' />

        17

      3)選中效果

        還增加了一處體驗優化,即為當前預覽小圖增加選中效果,就是增加個 2px 的邊框。

        寬高也設置了一下,還用到了CSS中的 object-fit 屬性。

      • cover:用于被替換的內容在保持其寬高比的同時,填充元素的整個內容框。
      • contain:用于被替換的內容將被縮放,以在填充元素的內容框時保持其寬高比。

        選取關鍵字 cover,將圖像能夠等比縮放的填充滿整個內容區域。

      {
        width:'100%';
        height: 45;
        object-fit: 'cover';
        border: 2px solid #a0d911;
      }

        18

      五、預覽大圖

      1)禁止打開預覽彈框

        預覽彈框有一處體驗優化,就是當上傳組件還有文件在上傳中時,禁止打開預覽彈框。

        避免在彈框的拖動區域,修改圖像順序時發生異常

        19

        在預覽彈框中,調節大圖尺寸,并且為其配置 objectFit 屬性,將圖像能夠等比縮放的填充到內容區域。

      <img 
       style={{ width: '100%', height: '60vh', marginTop: 5, objectFit: 'contain' }} 
       src='https://static.allreaday.com/xxx.jpg' 
       alt="預覽圖"
      />

        20

      2)loading切換

        由于在切換預覽時的加載時間較長(公司網絡較差),于是增加了 loading 加載的過渡動畫。

        21

        后續,也為大圖增加了質量變換,值為 50p,確保肉眼看的時候能保持清晰度和色彩度。

      https://static.xxx.com/xxx.jpg?imageMogr2/thumbnail/!50p

      六、緩存上傳文件

      1)localStorage

        在上傳按鈕旁增加恢復最近數據的按鈕,每次上傳時會將文件列表緩存到 localStorage 中

        22

        在 Upload 組件的 onChange 事件中,增加緩存的配置,并且為上傳組件增加 isCache 屬性控制是否開啟緩存。

      const onChange = (data: UploadChangeParam) => {
        const { fileList } = data;
        // ...
        isCache && setUploadImageCache(fileList);
        // ...
      }
      /**
       * 緩存上傳文件
       */
      export function setUploadImageCache(data:UploadFile[]) {
        localStorage.setItem('upload_image', JSON.stringify(data));
      }

      2)續傳失敗

        本來將 uploading 也存儲到了緩存中,并且在更新狀態時,組件也顯示了上傳中的樣式,但是無法續傳。

        可能是沒有 file 對象,即沒有文件信息導致無法上傳。

        目前就暫時無法給上傳中的文件實現斷點續傳,所以只能先把文件名給列出來。

        23

        在警告框中提供了復制按鈕(用OR分隔的名稱),點擊復制后,找到對應的文件夾(macOS),選中搜索,

        24

        將得到的查詢條件復制到搜索框中,就能得到還未上傳的圖像。

        25

      3)存儲溢出

        當上傳300多張圖像時,頁面報錯了。意思就是存儲超過最大閾值,localStorage 最大是 5M~10M。

      QuotaExceededError: Failed to execute 'setItem' on 'Storage': Setting the value of 'upload_image' exceeded the quota.

        嘗試采用數據壓縮,但仍然會報錯,只能另辟蹊徑。

      /**
       * 緩存上傳文件
       */
      export function setUploadImageCache(data:UploadFile[]) {
        // 壓縮數據
        const compress = btoa(encodeURIComponent(JSON.stringify(data)));
        localStorage.setItem('upload_image', compress);
      }
      /**
       * 讀取上傳文件的緩存
       */
      export function getUploadImageCache() {
        const files = localStorage.getItem('upload_image');
        if(!files) return [];
        // 解壓數據
        const uncompress = decodeURIComponent(atob(files));
        return JSON.parse(uncompress);
      }

      4)IndexedDB

        將數據緩存到 IndexedDB 中,理論上它可以占用 50% 的磁盤空間,所以不存在溢出的問題。

          /**
           * 存儲數據(支持大型數據)
           * @param key 鍵名
           * @param value 要存儲的值(可為任意類型)
           */
          async setItem(key: string, value: any): Promise<void> {
            if (!this.db) await this.init();
        
            return new Promise<void>((resolve, reject) => {
              const transaction = this.db!.transaction(['largeData'], 'readwrite');
              const store = transaction.objectStore('largeData');
              const item: StoredItem = { key, value, timestamp: Date.now() };
        
              const request: IDBRequest = store.put(item);
        
              request.onsuccess = () => resolve();
              request.onerror = () => {
                console.error('存儲數據失敗:', request.error);
                reject(request.error);
              };
            });
          }
        
          /**
           * 獲取存儲的數據
           * @param key 鍵名
           * @returns 存儲的值,若不存在則返回 null
           */
          async getItem(key: string): Promise<any> {
            if (!this.db) await this.init();
        
            return new Promise<any>((resolve, reject) => {
              const transaction = this.db!.transaction(['largeData'], 'readonly');
              const store = transaction.objectStore('largeData');
              const request: IDBRequest = store.get(key);
        
              request.onsuccess = () => {
                const result = request.result as StoredItem | undefined;
                resolve(result ? result.value : null);
              };
        
              request.onerror = () => {
                console.error('獲取數據失敗:', request.error);
                reject(request.error);
              };
            });
          }

        為了能自動清理 IndexedDB 的數據,在組件中上傳文件時,觸發清理的操作,下面查看占用空間的代碼。

      const quota = await navigator.storage.estimate();
      console.log(`已用空間: ${quota.usage} 字節`);
      console.log(`總配額: ${quota.quota} 字節`);
      const percentageUsed = (quota.usage / quota.quota) * 100;
      console.log(`已使用可用存儲的 ${percentageUsed}%`);

        現在即使緩存了 300 多張圖像,也能實現恢復。

      七、上傳反饋

      1)不友好的等待

        當需要上傳幾百張圖時,在選好圖像后,組件就會有比較長的時間沒有狀態變化。

        26

        不給用戶反饋,體驗很不友好。需要在合適的時機更新上傳區域的 loading 狀態。

        27

      2)自定義上傳按鈕

        沒有找到合適的事件,就想自定義上傳按鈕,然后控制按鈕的點擊事件,以此來更新狀態。

        Upload 組件只要在子元素位置增加按鈕元素就能替換默認的上傳按鈕。

      <Upload {...props}>
        <Button icon={<UploadOutlined />}>Click to Upload</Button>
      </Upload>

        但是在 Ant Design Pro 中的 ProFormUploadButton 組件內,并不能覆蓋上傳按鈕。

        除非自定義 ProFormUploadButton 組件的邏輯,但開發成本較高。

      3)點擊事件

        首先想到的是在 ProFormUploadButton 外增加個容器元素,注冊點擊事件,通過冒泡的方式觸發。

      <div onClick={}>
        <ProFormUploadButton />
      </div>

        但這樣的話,點擊范圍會比較大,并不局限于上傳按鈕,而是整個區域都會冒泡觸發。

        然后想到的是直接給生成的 input[type=file] 控件注冊點擊事件。

      document.getElementById(name)?.addEventListener('click', () => {
        setUploadAreaLoading(true); 
      });

        組件的屬性 name 會渲染成 input 按鈕的 id 屬性。

        在 onChange 事件中調用 setUploadAreaLoading(false) 取消 loading 狀態。

      4)取消文件選擇

        但會有一個問題,就是在彈出的選擇文件框中,不選擇文件,點擊取消,那么就不能觸發 onChange 事件。

        針對取消按鈕,瀏覽器也沒有提供專門的事件。

        28

        雖然為 window 注冊 focus 事件,能夠實現在點擊取消時觸發事件。

      window.addEventListener('focus', function() {
        const fileInput = document.getElementById(name) as HTMLInputElement;
        console.log(fileInput && fileInput.files)
      });

        但是無法準確讀取 fileList 列表,當文件少的時候,fileList 會馬上更新。

        但是當文件幾百個時,在觸發 focus 事件時,fileList 仍然是空的。

        而我優化的目的就是要在加載這些文件的過程中,出現 loading 過渡動畫,如此就無法判斷了。

        為 file 控件注冊 change 事件的確能馬上拿到 files 列表。

      document.getElementById(name)?.addEventListener('change', (e) => {
        const fileInput = e.target as HTMLInputElement;
        if(!fileInput) return;
        const files = fileInput.files;
        console.log('change', files);
        if(files && files.length > 0) {
          setUploadAreaLoading(true);
        }
        // fileInput.value = '';   //清空已選文件
      });

        但是在 Upload 組件中,再次上傳時,卻無法觸發 file 控件 change 事件。

        即使在 Upload 組件內清空文件的值,也無法再次觸發事件,很奇怪。

        后面發現原來上傳控件(input[type=file])在上傳完成后會被刪除,之前注冊的事件就沒有了。

      <span class="ant-upload">
        <input id="reviews" type="file" accept="image/*" multiple="" style="display: none;">
      </span>

        需要自定義 Upload 組件的上傳區域,暫時不修改。

        先與業務方同步,告知他們開啟緩存的上傳組件會有取消彈框無法自動移除 loading 的問題。

      5)事件委托

        既然不能指定控件綁定事件,那么可以借用委托,給容器元素注冊事件。

        如果用 DOM 的方式查找容器,像下面這樣,那么當頁面中有多個上傳組件時,就會給所有的容器都注冊事件。

      document.getElementsByClassName('ant-upload-select')

        而 Upload 提供了 ref,可以通過該屬性注冊特定組件內的容器。

      const fileContainer = uploadRef.current?.nativeElement;
      const onClick = (e:Event) => {
        const fileInput = e.target as HTMLInputElement;
        // 攔截非上傳控件的點擊事件
        if(!fileInput || fileInput.id !== name) {
          return;
        }
        /**
         * 在上傳區域開啟加載狀態
         * 因為當上傳幾百張圖時,整個界面就會卡住,不給用戶反饋
         */
        setUploadAreaLoading(true);
      };
      fileContainer?.addEventListener('click', onClick, false);

        同樣用委托的方式為 file 控件增加 change 事件,當有文件時,更新 loading 狀態。

      const onChange = (e:Event) => {
        const fileInput = e.target as HTMLInputElement;
        // 攔截非上傳控件的點擊事件
        if(!fileInput || fileInput.id !== name) {
          return;
        }
        const files = fileInput.files;
        if(files && files.length > 0) {
          setUploadAreaLoading(true);
        }
      };
      fileContainer?.addEventListener('change', onChange, false);

        這樣就能實現在選中文件后,組件顯示 loading 等待狀態,取消選擇框,也能關閉等待狀態。

       

       posted on 2025-10-27 10:34  咖啡機(K.F.J)  閱讀(1719)  評論(3)    收藏  舉報
      主站蜘蛛池模板: 日夜啪啪一区二区三区| 亚洲AV无码一二区三区在线播放| 久久亚洲人成网站| 人人人爽人人爽人人av| 精品无码成人片一区二区| 精品综合一区二区三区四区| 成人免费A级毛片无码片2022| 达尔| 老熟妇欲乱一区二区三区| 久久精品国产99久久久古代| 波多野结衣一区二区三区高清av | 国产精品第一页中文字幕| 99久久国产综合精品女图图等你| 望奎县| 成人国产精品免费网站| 亚洲旡码欧美大片| 亚洲最大成人在线播放| 欧美老少配性行为| 国产精品美女网站| 兴城市| 九九热久久只有精品2| 少妇特黄a一区二区三区| 成人免费无码大片a毛片| 亚洲一区二区三区av激情| 亚洲精品成人一二三专区| 久久综合色之久久综合色| 日韩精品一区二区三免费| 一本久久a久久精品综合| 成全高清在线播放电视剧| 亚洲精品一区二区天堂| 免费人成视频在线播放| 女子spa高潮呻吟抽搐| 免费人成网站免费看视频| 99久久精品国产一区二区蜜芽| 自拍视频一区二区三区四区| 精品国产乱子伦一区二区三区| 久久人人97超碰国产精品| 激情国产一区二区三区四| 国产人妻人伦精品1国产丝袜| 中文字幕av中文字无码亚 | 台湾佬自拍偷区亚洲综合|