1. 程式人生 > 程式設計 >如何基於js管理大檔案上傳及斷點續傳詳析

如何基於js管理大檔案上傳及斷點續傳詳析

目錄
  • 前言
  • 前端結構
  • 後端結構(node + express)
  • 基於FormData實現檔案上傳
  • 基於BASE64實現檔案上傳
    • BASE64具體方法
    • 前端生成檔名傳給後端
  • 上傳進度管控
    • 大檔案上傳
      • 服務端程式碼(大檔案上傳+斷點續傳)
        • 總結

          前言

          前端小夥伴們平常在開發過程中檔案上傳是經常遇到的一個問題,也許你能夠實現相關的功能,但是做完後回想程式碼實現上是不是有點"力不從心"呢?你真的瞭解檔案上傳嗎?如何做到大檔案上傳以及斷電續傳呢,前後端通訊常用的格式,檔案上傳進度管控,服務端是如何實現的?接下來讓我們開啟手摸手系列的學習吧!!!如有不足之處,望不吝指教,接下來按照下圖進行學習探討

          如何基於js管理大檔案上傳及斷點續傳詳析

          一切就緒,開始吧!!!

          前端結構

          頁面展示

          如何基於js管理大檔案上傳及斷點續傳詳析

          專案依賴

          如何基於js管理大檔案上傳及斷點續傳詳析

          後端結構(node + express)

          目錄結構

          如何基於js管理大檔案上傳及斷點續傳詳析

          Axios的簡單封裝

          let instance = axios.create();
          instance.defaults.baseURL = 'http://127.0.0.1:8888';
          instance.defaults.headers['Content-Type'] = 'multipart/form-data';
          instance.defaults.transformRequest = (data,headers) => {
              const contentType = headers['Content-Type'];
              if (contentType === "application/x-www-form-urlencoded") return Qs.stringify(data);
              return data;
          };
          instance.interceptors.response.use(response => {
              return response.data;
          });
          

          檔案上傳一般是基於兩種方式,FormData以及Base64

          基於FormData實現檔案上傳

           //前端程式碼
              // 主要展示基於ForData實現上傳的核心程式碼
              upload_button_upload.addEventListener('click',function () {
                      if (upload_button_upload.classList.contains('disable') || upload_button_upload.classList.contains('loading')) return;
                      if (!_file) {
                          alert('請您先選擇要上傳的檔案~~');
                          return;
                      }
                      changeDisable(true);
                      // 把檔案傳遞給伺服器:FormData
                      let formData = new FormData();
                      // 根據後臺需要提供的欄位進行新增
                      formData.append('file',_file);
                      formData.append('filename',
          _file.name); instance.post('/upload_single',formData).then(data => { if (+data.code === 0) { alert(`檔案已經上傳成功~~,您可以基於 ${data.servicePath} 訪問這個資源~~`); return; } return Promise.reject(data.codeText); }).catch(reason => { alert('檔案上傳失敗,請您稍後再試~~'); }).finally(() => { clearHandle(); changeDisable(false); }); });

          基於BASE64實現檔案上傳

          BASE64具體方法

          export changeBASE64(file) => {
             return new Promise(resolve => {
              let fileReader = new FileReader();
              fileReader.readAsDataURL(file);
              fileReader.onload = ev => {
                  resolve(ev.target.result);
              };
            });
          };
          

          具體實現

          upload_inp.addEventListener('change',async function () {
                  let file = upload_inp.files[0],BASE64,data;
                  if (!file) return;
                  if (file.size > 2 * 1024 * 1024) {
                      alert('上傳的檔案不能超過2MB~~');
                      return;
                  }
                  upload_button_select.classList.add('loading');
                  // 獲取Base64
                  BASE64 = await changeBASE64(file);
                  try {
                      data = await instance.post('/upload_single_base64',{
                      // encodeURIComponent(BASE64) 防止傳輸過程中特殊字元亂碼,同時後端需要用decodeURIComponent進行解碼
                          file: encodeURIComponent(BASE64),filename: file.name
                      },{
                          headers: {
                              'Content-Type': 'application/x-www-form-urlencoded'
                          }
                      });
                      if (+data.code === 0) {
                          alert(`恭喜您,檔案上傳成功,您可以基於 ${data.servicePath} 地址去訪問~~`);
                          return;
                      }
                      throw data.codeText;
                  } catch (err) {
                      alert('很遺憾,檔案上傳失敗,請您稍後再試~~');
                  } finally {
                      upload_button_select.classList.remove('loading');
                  }
              **});**
          

          上面這個例子中後端收到前端傳過來的檔案會對它進行生成一個隨機的名字,存下來,但是有些公司會將這一步放在前端進行,生成名字後一起發給後端,接下來我們來實現這個功能

          前端生成檔名傳給後端

          這裡就需要用到上面提到的外掛SparkMD5,具體怎麼用就不做贅述了,請參考文件

          封裝讀取檔案流的方法

          const changeBuffer = file => {
              return new Promise(resolve => {
                  let fileReader = new FileReader();
                  fileReader.readAsArrayBuffer(file);
                  fileReader.onload = ev => {
                      let buffer = ev.target.result,spark = new SparkMD5.ArrayBuffer(),HASH,suffix;
                      spark.append(buffer);
                      // 得到檔名
                      HASH = spark.end();
                      // 獲取字尾名
                      suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)[1];
                      resolve({
                          buffer,suffix,filename: `${HASH}.${suffix}`
                      });
                  };
              });
            };
          

          上傳伺服器相關程式碼

          upload_button_upload.addEventListener('click',async function () {
                  if (checkIsDisable(this)) return;
                  if (!_file) {
                      alert('請您先選擇要上傳的檔案~~');
                      return;
                  }
                  changeDisable(true);
                  // 生成檔案的HASH名字
                  let {
                      filename
                  } = await changeBuffer(_file);
                  let formData = new DepldEqTfFormData();
                  formData.append('file',_file);
                  formData.append('filename',filename);
                  instance.post('/upload_single_name',formData).then(data => {
                      if (+data.code === 0) {
                          alert(`檔案已經上傳成功~~,您可以基於 ${data.servicePath} 訪問這個資源~~`);
                          return;
                      }
                      return Promise.reject(data.codeText);
                  }).catch(reason => {
                      alert('檔案上傳失敗,請您稍後再試~~');
                  }).finally(() => {
                      changeDisable(false);
                      upload_abbre.style.display = 'none';
                      upload_abbre_img.src = '';
                      _file = null;
                  });
              });
          

          上傳進度管控

          這個功能相對來說比較簡單,文中用到的請求庫是axios,進度管控主要基於axios提供的onUploadProgress函式進行實現,這裡一起看下這個函式的實現原理

          監聽xhr.upload.onprogress

          如何基於js管理大檔案上傳及斷點續傳詳析

          檔案上傳後得到的物件

          如何基於js管理大檔案上傳及斷點續傳詳析

          具體實現

          (function () {
              let upload = document.querySelector('#upload4'),upload_inp = upload.querySelector('.upload_inp'),upload_button_select = upload.querySelector('.upload_button.select'),upload_progress = upload.querySelector('.upload_progress'),upload_progress_value = upload_progress.querySelector('.value');
          
              // 驗證是否處於可操作性狀態
              const checkIsDisable = element => {
                  let classList = element.classList;
                  return classList.contains('disable') || classList.contains('loading');
              };
          
              upload_inp.addEventListener('change',data;
                  if (!file) return;
                  upload_button_select.classList.add('loading');
                  try {
                      let formData = new FormData();
                      formData.append('file',file);
                      formData.append('filename',file.name);
                      data = await instance.post('/upload_single',formData,{
                          // 檔案上傳中的回撥函式 xhr.upload.onprogress
                          onUploadProgress(ev) {
                              let {
                                  loaded,total
                              } = ev;
                              upload_progress.style.display = 'block';
                              upload_progress_value.style.width = `${loaded/total*100}%`;
                          }
                      });
                      if (+data.code === 0) {
                          upload_progress_value.style.width = `100%`;
                          alert(`恭喜您,檔案上傳成功,您可以基於 ${data.servicePath} 訪問該檔案~~`);
                          return;
                      }
                      throw data.codeText;
                  } catch (err) {
                      alert('很遺憾,檔案上傳失敗,請您稍後再試~~');
                  } finally {
                      upload_button_select.classList.remove('loading');
                      upload_progress.style.display = 'none';
                      upload_progress_value.style.width = `0%`;
                  }
              });
          
              upload_button_select.addEventListener('click',function () {
                  if (checkIsDisable(this)) return;
                  upload_inp.click();
              });
          })();
          

          大檔案上傳

          大檔案上傳一般採用切片上傳的方式,這樣可以提高檔案上傳的速度,前端拿到檔案流後進行切片,然後與後端進行通訊傳輸,一般還會結合斷點繼傳,這時後端一般提供三個介面,第一個介面獲取已經上傳的切片資訊,第二個介面將前端切片檔案進行傳輸,第三個介面是將所有切片上傳完成後告訴後端進行檔案合併

          如何基於js管理大檔案上傳及斷點續傳詳析

          進行切片,切片的方式分為固定數量以及固定大小,我們這裡兩者結合一下

          // 實現檔案切片處理 「固定數量 & 固定大小」
          let max = 1024 * 100,count = Math.ceil(file.size / max),index = 0,chunks = [];
          if (count > 100) {
              max = file.size / 100;
              count = 100;
          }
          while (index < count) {
              chunks.push({
              // file檔案本身就具有slice方法,見下圖
                  file: file.slice(index * max,(index + 1) * max),filename: `${HASH}_${index+1}.${suffix}`
              });
              index++;
          }
          

          傳送至服務端

          chunks.forEach(chunk => {
              let fm = new FormData;
              fm.append('file',chunk.file);
              fm.append('filename',chunk.filename);
              instance.post('/upload_chunk',fm).then(data => {
                  if (+data.code === 0) {
                      complate();
                      return;
                  }
                  return Promise.reject(data.codeText);
              }).catch(() => {
                  alert('當前切片上傳失敗,請您稍後再試~~');
                  clear();
              });
             });
          

          檔案上傳 + 斷電續傳 + 進度管控

              upload_inp.addEventListener('change',async function () {
                  let file = upload_inp.files[0];
                  if (!file) return;
                  upload_button_select.classList.add('loading');
                  upload_progress.style.display = 'block';
          
                  // 獲取檔案的HASH
                  let already = [],data = null,{
                          HASH,suffix
                      } = await changeBuffer(file);
          
                  // 獲取已經上傳的切片資訊
                  try {
                      data = await instance.get('/upload_already',{
                          params: {
                              HASH
                          }
                      });
                      if (+data.code === 0) {
                          already = data.fileList;
                      }
                  } catch (err) {}
          
                  // 實現檔案切片處理 「固定數量 & 固定大小」
                  let max = 1024 * 100,chunks = [];
                  if (count > 100) {
                      max = file.size / 100;
                      count = 100;
                  }
                  while (index < count) {
                      chunks.push({
                          file: file.slice(index * max,filename: `${HASH}_${index+1}.${suffix}`
                      });
                      index++;
                  }
          
                  // 上傳成功的處理
                  index = 0;
                  const clear = () => {
                      upload_button_select.classList.remove('loading');
                      upload_progress.style.display = 'none';
                      upload_progress_value.style.width = '0%';
                  };
                  const complate = async () => {
                      // 管控進度條
                      index++;
                      upload_progress_value.style.width = `${index/count*100}%`;
          
                      // 當所有切片都上傳成功,我們合併切片
                      if (index < count) return;
                      upload_progress_value.style.width = `100%`;
                      try {
                          data = await instance.post('/upload_merge',{
                              HASH,count
                          },{
                              headers: {
                                  'Content-Type': 'application/x-www-form-urlencoded'
                              }
                          });
                          if (+data.code === 0) {
                              alert(`恭喜您,檔案上傳成功,您可以基於 ${data.servicePath} 訪問該檔案~~`);
                              clear();
                              return;
                          }
                          throw data.codeText;
                      } catch (err) {
                          alert('切片合併失敗,請您稍後再試~~');
                          clear();
                      }
                  };
          
                  // 把每一個切片都上傳到伺服器上
                  chunks.forEach(chunk => {
                      // 已經上傳的無需在上傳
                      if (already.length > 0 && already.includes(chhttp://www.cppcns.comunk.filename)) {
                          complate();
                          return;
                      }
                      let fm = new FormData;
                      fm.append('file',chunk.file);
                      fm.append('filename',chunk.filename);
                      instance.post('/upload_chunk',fm).then(data => {
                          if (+data.code === 0) {
                              complate();
                              return;
                          }
                          return Promise.reject(data.codeText);
                      }).catch(() => {
                          alert('當前切片上傳失敗,請您稍後再試~~');
                          clear();
                      });
                  });
              });
          

          服務端程式碼(大檔案上傳+斷點續傳)

           // 大檔案切片上傳 & 合併切片
              const merge = function merge(HASH,count) {
                  return new Promise(async (resolve,reject) => {
                      let path = `${uploadDir}/${HASH}`,fileList = [],isExists;
                      isExists = await exists(path);
                      if (!isExists) {
                          reject('HASH path is not found!');
                          return;
                      }
                      fileList = fs.readdirSync(path);
                      if (fileList.length < count) {
                          reject('the slice has not been uploaded!');
                          return;
                      }
                      fileList.sort((a,b) => {
                          let reg = /_(\d+)/;
                          return reg.exec(a)[1] - reg.exec(b)[1];
                      }).forEach(item => {
                          !suffix ? suffix = /\.([0-9a-zA-Z]+)$/.exec(item)[1] : null;
                          fs.appendFileSync(`${uploadDir}/${HASH}.${suffix}`,fs.readFileSync(`${path}/${item}`));
                          fs.unlinkSync(`${path}/${item}`);
                      });
                      fs.rmdirSync(path);
                      resolve({
                          path: `${uploadDir}/${HASH}.${suffix}`,filename: `${HASH}.${suffix}`
                      });
                  });
              };
              app.post('/upload_chunk',async (req,res) => {
                  try {
                      let {
                          fields,files
                      } = await multiparty_upload(req);
                      let file = (files.file && files.file[0]) || {},filename = (fields.filename && fields.filename[0]) || "",path = '',isExists = false;
                      // 建立存放切片的臨時目錄
                      let [,HASH] = /^([^_]+)_(\d+)/.exec(filename);
                      path = `${uploadDir}/${HASH}`;
                      !fs.existsSync(path) ? fs.mkdirSync(path) : null;
                      // 把切片儲存到臨時目錄中
                      path = `${uploadDir}/${HASH}/${filename}`;
                      isExists = await exists(path);
                      if (isExists) {
                          res.send({
                              code: 0,codeText: 'file is exists',originalFilename: filename,servicePath: path.replace(__dirname,HOSTNAME)
                          });
                          return;
                      }
                      writeFile(res,path,file,filename,true);
                  } catch (err) {
                      res.send({
                          code: 1,codeText: err
                      });
                  }
              });
              app.post('/upload_merge',res) => {
                  let {
                      HASH,count
                  } = req.body;
                  try {
                      let {
                          filename,path
                      } = await merge(HASH,count);
                      res.send({
                          code: 0,codeText: 'merge success',HOSTNAME)
                      });
                  } catch (err) {
                      res.send({
                          code: 1,codeText: err
                      });
                  }
              });
              app.get('/upload_already',res) => {
                  let {
                      HASH
                  } = req.query;
                  let path = `${uploadDir}/${HASH}`,fileList = [];
                  try {
                      fileList = fs.readdirSync(path);
             客棧         fileList = fileList.sort((a,b) => {
                          let reg = /_(\d+)/;
                          return reg.exec(a)[1] - reg.exec(b)[1];
                      });
                      res.send({
                          code: 0,codeText: '',fileList: fileList
                      });
                  } catch (err) {
                      res.send({
                          code: 0,fileList: fileList
                      });
                  }
              });
          

          總結

          到此這篇關於如何基於管理大檔案上傳及斷點續傳的文章就介紹到這了,更多相關js大檔案上傳及斷點續傳內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!