<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          你好!來(lái)實(shí)現(xiàn)大文件分片上傳,暫停續(xù)傳的

          共 31910字,需瀏覽 64分鐘

           ·

          2022-01-14 11:02

          大廠技術(shù)  高級(jí)前端  Node進(jìn)階

          點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)

          回復(fù)1,加入高級(jí)Node交流群

          前言

          最近我們公司的項(xiàng)目中多了一個(gè)需求,因?yàn)槲覀兊墓芾硐到y(tǒng)需要管理背景音樂(lè)的存儲(chǔ),那就肯定涉及到前端的上傳音樂(lè)功能了,可能是由于我們公司的編輯們所制作的BGM質(zhì)量比較高,所以每一個(gè)BGM文件都會(huì)比較大,每一個(gè)都在20M以上,所以我使用了大文件的分片上傳,并做了暫停上傳續(xù)傳功能,接下來(lái)就通過(guò)一個(gè)小demo,給大家演示一下吧!!!

          BGM切片上傳

          1.大致流程

          分為以下幾步:

          • 1.前端接收BGM并進(jìn)行切片
          • 2.將每份切片都進(jìn)行上傳
          • 3.后端接收到所有切片,創(chuàng)建一個(gè)文件夾存儲(chǔ)這些切片
          • 4.后端將此文件夾里的所有切片合并為完整的BGM文件
          • 5.刪除文件夾,因?yàn)?code style="box-sizing: border-box;font-size: 14px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);word-break: break-all;">切片不是我們最終想要的,可刪除
          • 6.當(dāng)服務(wù)器已存在某一個(gè)文件時(shí),再上傳需要實(shí)現(xiàn)“秒傳”

          2.前端實(shí)現(xiàn)切片

          簡(jiǎn)單來(lái)說(shuō)就是,咱們上傳文件時(shí),選中文件后,瀏覽器會(huì)把這個(gè)文件轉(zhuǎn)成一個(gè)Blob對(duì)象,而這個(gè)對(duì)象的原型上上有一個(gè)slice方法,這個(gè)方法是大文件能夠切片的原理,可以利用這個(gè)方法來(lái)給打文件切片

          <input type="file" @change="handleFileChange" />
          <el-button @click="handleUpload"> 上傳 </el-button>

          data() {
              return {
                  fileObj: {
                      filenull
                  }
              };
            },
            methods: {
                handleFileChange(e) {
                    const [file] = e.target.files
                    if (!file) return
                    this.fileObj.file = file
                },
                handleUpload () {
                    const fileObj = this.fileObj
                    if (!fileObj.file) return
                    const chunkList = this.createChunk(fileObj.file)
                    console.log(chunkList) // 看看chunkList長(zhǎng)什么樣子
                },
                createChunk(file, size = 5 * 1024 * 1024) {
                    const chunkList = []
                    let cur = 0
                    while(cur < file.size) {
                        // 使用slice方法切片
                        chunkList.push({ file: file.slice(cur, cur + size) })
                        cur += size
                    }
                    return chunkList
                }

          例子我就用我最近很喜歡聽(tīng)得一首歌嘉賓-張遠(yuǎn),他的大小是32M

          截屏2021-07-08 下午8.06.22.png

          點(diǎn)擊上傳,看看chunkList長(zhǎng)什么樣子吧:

          image.png

          證明我們切片成功了!!!分成了7個(gè)切片

          3.上傳切片并展示進(jìn)度條

          我們先封裝一個(gè)請(qǐng)求方法,使用的是axios

          import axios from "axios";

          axiosRequest({
                url,
                method = "post",
                data,
                headers = {},
                onUploadProgress = (e) => e, // 進(jìn)度回調(diào)
              }) {
                return new Promise((resolve, reject) => {
                  axios[method](url, data, {
                    headers,
                    onUploadProgress, // 傳入監(jiān)聽(tīng)進(jìn)度回調(diào)
                  })
                    .then((res) => {
                      resolve(res);
                    })
                    .catch((err) => {
                      reject(err);
                    });
                });
              }

          接著上一步,我們獲得了所有切片,接下來(lái)要把這些切片保存起來(lái),并逐一去上傳

          handleUpload() {
                const fileObj = this.fileObj;
                if (!fileObj.file) return;
                const chunkList = this.createChunk(fileObj.file);
          +      this.fileObj.chunkList = chunkList.map(({ file }, index) => ({
          +        file,
          +        size: file.size,
          +        percent: 0,
          +        chunkName: `${fileObj.file.name}-${index}`,
          +        fileName: fileObj.file.name,
          +        index,
          +      }));
          +      this.uploadChunks(); // 執(zhí)行上傳切片的操作
              },

          uploadChunks就是執(zhí)行上傳所有切片的函數(shù)

          async uploadChunks() {
          +      const requestList = this.fileObj.chunkList
          +        .map(({ file, fileName, index, chunkName }) => {
          +          const formData = new FormData();
          +          formData.append("file", file);
          +          formData.append("fileName", fileName);
          +          formData.append("chunkName", chunkName);
          +          return { formData, index };
          +        })
          +        .map(({ formData, index }) =>
          +          this.axiosRequest({
          +            url: "http://localhost:3000/upload",
          +            data: formData,
          +            onUploadProgress: this.createProgressHandler(
          +              this.fileObj.chunkList[index]
          +            ), // 傳入監(jiān)聽(tīng)上傳進(jìn)度回調(diào)
          +          })
          +        );
          +      await Promise.all(requestList); // 使用Promise.all進(jìn)行請(qǐng)求
          +    },
          + createProgressHandler(item) {
          +      return (e) => {
          +         // 設(shè)置每一個(gè)切片的進(jìn)度百分比
          +        item.percent = parseInt(String((e.loaded / e.total) * 100));
          +      };
          +    },

          我不知道他們后端Java是怎么做的,我這里使用Nodejs模擬一下

          const http = require("http");
          const path = require("path");
          const fse = require("fs-extra");
          const multiparty = require("multiparty");

          const server = http.createServer();
          const UPLOAD_DIR = path.resolve(__dirname, "."`qiepian`); // 切片存儲(chǔ)目錄

          server.on("request"async (req, res) => {
              res.setHeader("Access-Control-Allow-Origin""*");
              res.setHeader("Access-Control-Allow-Headers""*");
              if (req.method === "OPTIONS") {
                  res.status = 200;
                  res.end();
                  return;
              }
              console.log(req.url)

              if (req.url === '/upload') {
                  const multipart = new multiparty.Form();

                  multipart.parse(req, async (err, fields, files) => {
                      if (err) {
                          console.log('errrrr', err)
                          return;
                      }
                      const [file] = files.file;
                      const [fileName] = fields.fileName;
                      const [chunkName] = fields.chunkName;
                      // 保存切片的文件夾的路徑,比如  張遠(yuǎn)-嘉賓.flac-chunks
                      const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`);
                      // // 切片目錄不存在,創(chuàng)建切片目錄
                      if (!fse.existsSync(chunkDir)) {
                          await fse.mkdirs(chunkDir);
                      }
                      // 把切片移動(dòng)到切片文件夾
                      await fse.move(file.path, `${chunkDir}/${chunkName}`);
                      res.end(
                          JSON.stringify({
                              code0,
                              message"切片上傳成功"
                          }));
                  });
              }
          })

          server.listen(3000, () => console.log("正在監(jiān)聽(tīng) 3000 端口"));

          接下來(lái)就是頁(yè)面上進(jìn)度條的顯示了,其實(shí)很簡(jiǎn)單,我們想要展示總進(jìn)度條,和各個(gè)切片的進(jìn)度條,各個(gè)切片的進(jìn)度條我們都有了,我們只需要算出總進(jìn)度就行,怎么算呢?這么算:各個(gè)切片百分比 * 各個(gè)切片的大小 / 文件總大小

          <div style="width: 300px">
          +      總進(jìn)度:
          +      <el-progress :percentage="totalPercent"></el-progress>
          +      切片進(jìn)度:
          +      <div v-for="item in fileObj.chunkList" :key="item">
          +        <span>{{ item.chunkName }}:</span>
          +        <el-progress :percentage="item.percent"></el-progress>
          +      </div>
          +</div>


          + computed: {
          +    totalPercent() {
          +      const fileObj = this.fileObj;
          +      if (fileObj.chunkList.length === 0return 0;
          +      const loaded = fileObj.chunkList
          +        .map(({ size, percent }) => size * percent)
          +        .reduce((pre, next) => pre + next);
          +      return parseInt((loaded / fileObj.file.size).toFixed(2));
          +    },
          +  },

          我們?cè)俅紊蟼饕魳?lè),查看效果:

          截屏2021-07-08 下午10.33.51.png

          后端也成功保存了

          截屏2021-07-08 下午10.34.28.png

          4.合并切片為BGM

          好了,咱們已經(jīng)保存好所有切片,接下來(lái)就要開(kāi)始合并切片了,我們會(huì)發(fā)一個(gè)/merge請(qǐng)求,叫后端合并這些切片,前端代碼添加合并的方法:

          async uploadChunks() {
                const requestList = this.fileObj.chunkList
                  .map(({ file, fileName, index, chunkName }) => {
                    const formData = new FormData();
                    formData.append("file", file);
                    formData.append("fileName", fileName);
                    formData.append("chunkName", chunkName);
                    return { formData, index };
                  })
                  .map(({ formData, index }) =>
                    this.axiosRequest({
                      url"http://localhost:3000/upload",
                      data: formData,
                      onUploadProgressthis.createProgressHandler(
                        this.fileObj.chunkList[index]
                      ),
                    })
                  );
                await Promise.all(requestList); // 使用Promise.all進(jìn)行請(qǐng)求

          +      this.mergeChunks()
              },
          + mergeChunks(size = 5 * 1024 * 1024) {
          +       this.axiosRequest({
          +         url: "http://localhost:3000/merge",
          +         headers: {
          +           "content-type""application/json",
          +         },
          +         data: JSON.stringify({ 
          +          size,
          +           fileName: this.fileObj.file.name
          +         }),
          +       });
          +     }

          后端增加/merge接口:

          // 接收請(qǐng)求的參數(shù)
          const resolvePost = req =>
              new Promise(res => {
                  let chunk = ''
                  req.on('data', data => {
                      chunk += data
                  })
                  req.on('end', () => {
                      res(JSON.parse(chunk))
                  })

              })
          const pipeStream = (path, writeStream) => {
              console.log('path', path)
              return new Promise(resolve => {
                  const readStream = fse.createReadStream(path);
                  readStream.on("end", () => {
                      fse.unlinkSync(path);
                      resolve();
                  });
                  readStream.pipe(writeStream);
              });
          }

          // 合并切片
          const mergeFileChunk = async (filePath, fileName, size) => {
              // filePath:你將切片合并到哪里,的路徑
              const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`);
              let chunkPaths = null
              // 獲取切片文件夾里所有切片,返回一個(gè)數(shù)組
              chunkPaths = await fse.readdir(chunkDir);
              // 根據(jù)切片下標(biāo)進(jìn)行排序
              // 否則直接讀取目錄的獲得的順序可能會(huì)錯(cuò)亂
              chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
              const arr = chunkPaths.map((chunkPath, index) => {
                  return pipeStream(
                      path.resolve(chunkDir, chunkPath),
                      // 指定位置創(chuàng)建可寫流
                      fse.createWriteStream(filePath, {
                          start: index * size,
                          end: (index + 1) * size
                      })
                  )
              })
              await Promise.all(arr)
          };
          if (req.url === '/merge') {
                  const data = await resolvePost(req);
                  const { fileName, size } = data;
                  const filePath = path.resolve(UPLOAD_DIR, fileName);
                  await mergeFileChunk(filePath, fileName, size);
                  res.end(
                      JSON.stringify({
                          code0,
                          message"文件合并成功"
                      })
                  );
              }

          現(xiàn)在我們重新上傳音樂(lè),發(fā)現(xiàn)切片上傳成功了,也合并成功了:

          截屏2021-07-09 下午1.44.29.png

          5.刪除切片

          上一步我們已經(jīng)完成了切片合并這個(gè)功能了,那之前那些存在后端的切片就沒(méi)用了,不然會(huì)浪費(fèi)服務(wù)器的內(nèi)存,所以我們?cè)诖_保合并成功后,可以將他們刪除

          // 合并切片
          const mergeFileChunk = async (filePath, fileName, size) => {
              // filePath:你將切片合并到哪里,的路徑
              const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`);
              let chunkPaths = null
              // 獲取切片文件夾里所有切片,返回一個(gè)數(shù)組
              chunkPaths = await fse.readdir(chunkDir);
              // 根據(jù)切片下標(biāo)進(jìn)行排序
              // 否則直接讀取目錄的獲得的順序可能會(huì)錯(cuò)亂
              chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
              const arr = chunkPaths.map((chunkPath, index) => {
                  return pipeStream(
                      path.resolve(chunkDir, chunkPath),
                      // 指定位置創(chuàng)建可寫流
                      fse.createWriteStream(filePath, {
                          start: index * size,
                          end: (index + 1) * size
                      })
                  )
              })
              await Promise.all(arr)
              fse.rmdirSync(chunkDir); // 合并后刪除保存切片的目錄
          };

          我們?cè)俅紊蟼鳎倏纯矗莻€(gè)儲(chǔ)存此音樂(lè)的切片文件夾被我們刪了

          截屏2021-07-09 下午1.46.59.png

          6.秒傳功能

          所謂的秒傳功能,其實(shí)沒(méi)那么高大上,通俗點(diǎn)說(shuō)就是,當(dāng)你上傳一個(gè)文件時(shí),后端會(huì)判斷服務(wù)器上有無(wú)這個(gè)文件,有的話就不執(zhí)行上傳,并返回給你“上傳成功”,想要執(zhí)行此功能,后端需要重新寫一個(gè)接口/verify

          if (req.url === "/verify") {
                  const data = await resolvePost(req);
                  const { fileName } = data;
                  const filePath = path.resolve(UPLOAD_DIR, fileName);
                  console.log(filePath)
                  if (fse.existsSync(filePath)) {
                      res.end(
                          JSON.stringify({
                              shouldUploadfalse
                          })
                      );
                  } else {
                      res.end(
                          JSON.stringify({
                              shouldUploadtrue
                          })
                      );
                  }

          前端在上傳文件步驟也要做攔截:

          async handleUpload() {
                const fileObj = this.fileObj;
                if (!fileObj.file) return;
          +      const { shouldUpload } = await this.verifyUpload(
          +         fileObj.file.name,
          +       );
          +       if (!shouldUpload) {
          +         alert("秒傳:上傳成功");
          +         return;
          +       }
                const chunkList = this.createChunk(fileObj.file);
                this.fileObj.chunkList = chunkList.map(({ file }, index) => ({
                  file,
                  size: file.size,
                  percent0,
                  chunkName`${fileObj.file.name}-${index}`,
                  fileName: fileObj.file.name,
                  index,
                }));
                this.uploadChunks();
              },
          async verifyUpload (fileName) {
          +       const { data } = await this.axiosRequest({
          +         url: "http://localhost:3000/verify",
          +         headers: {
          +           "content-type""application/json",
          +         },
          +         data: JSON.stringify({
          +           fileName,
          +         }),
          +       });
          +       return data
          +     }

          現(xiàn)在我們重新上傳音樂(lè),因?yàn)榉?wù)器上已經(jīng)存在了張遠(yuǎn)-嘉賓這首歌了,所以,直接alert出秒傳:上傳成功

          截屏2021-07-09 下午2.17.02.png

          暫停續(xù)傳

          1.大致流程

          暫停續(xù)傳其實(shí)很簡(jiǎn)單,比如一個(gè)文件被切成10片,當(dāng)你上傳成功5片后,突然暫停,那么下次點(diǎn)擊續(xù)傳時(shí),只需要過(guò)濾掉之前已經(jīng)上傳成功的那5片就行,怎么實(shí)現(xiàn)呢?其實(shí)很簡(jiǎn)單,只需要點(diǎn)擊續(xù)傳時(shí),請(qǐng)求/verity接口,返回切片文件夾里現(xiàn)在已成功上傳的切片列表,然后前端過(guò)濾后再把還未上傳的切片的繼續(xù)上傳就行了,后端的/verify接口需要做一些修改

          if (req.url === "/verify") {
                  // 返回已經(jīng)上傳切片名列表
                  const createUploadedList = async fileName =>
          +             fse.existsSync(path.resolve(UPLOAD_DIR, fileName))
          +                 ? await fse.readdir(path.resolve(UPLOAD_DIR, fileName))
          +                 : [];
                  const data = await resolvePost(req);
                  const { fileName } = data;
                  const filePath = path.resolve(UPLOAD_DIR, fileName);
                  console.log(filePath)
                  if (fse.existsSync(filePath)) {
                      res.end(
                          JSON.stringify({
                              shouldUploadfalse
                          })
                      );
                  } else {
                      res.end(
                          JSON.stringify({
                              shouldUploadtrue,
          +                     uploadedList: await createUploadedList(`${fileName}-chunks`)
                          })
                      );
                  }
              }

          2.暫停上傳

          前端增加一個(gè)暫停按鈕pauseUpload事件

          <el-button @click="pauseUpload"> 暫停 </el-button>


          const CancelToken = axios.CancelToken;
          const source = CancelToken.source();

          axiosRequest({
                url,
                method = "post",
                data,
                headers = {},
                onUploadProgress = (e) => e,
              }) {
                return new Promise((resolve, reject) => {
                  axios[method](url, data, {
                    headers,
                    onUploadProgress,
          +           cancelToken: source.token
                  })
                    .then((res) => {
                      resolve(res);
                    })
                    .catch((err) => {
                      reject(err);
                    });
                });
              },
          + pauseUpload() {
          +       source.cancel("中斷上傳!");
          +       source = CancelToken.source(); // 重置source,確保能續(xù)傳
          +     }

          3.續(xù)傳

          增加一個(gè)續(xù)傳按鈕,并增加一個(gè)keepUpload事件

          <el-button @click="keepUpload"> 續(xù)傳 </el-button>

          async keepUpload() {
          +       const { uploadedList } = await this.verifyUpload(
          +         this.fileObj.file.name
          +       );
          +       this.uploadChunks(uploadedList);
          +     }

          4.優(yōu)化進(jìn)度條

          續(xù)傳中,由于那些沒(méi)有上傳的切片會(huì)從零開(kāi)始傳,所以會(huì)導(dǎo)致總進(jìn)度條出現(xiàn)倒退現(xiàn)象,所以我們要對(duì)總進(jìn)度條做一下優(yōu)化,確保他不會(huì)倒退,做法就是維護(hù)一個(gè)變量,這個(gè)變量只有在總進(jìn)度大于他時(shí)他才會(huì)更新成總進(jìn)度

          總進(jìn)度:
          <el-progress :percentage="tempPercent"></el-progress>

          + watch: {
          +       totalPercent (newVal) {
          +           if (newVal > this.tempPercent) this.tempPercent = newVal
          +       }
          +   },

          結(jié)語(yǔ)

          Node 社群


          我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。


             “分享、點(diǎn)贊在看” 支持一波??

          瀏覽 54
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美一区二区丁香五月天激情 | 国产亚洲精品久久777777 | 久久久成人高清无码 | 父女乱情沈娜娜 | 乱伦小视频 |