我是如何在项目中实现大文件分片上传,暂停续传的

发表于 2年以前  | 总阅读数:480 次

前言

最近我们公司的项目中多了一个需求,因为我们的管理系统需要管理背景音乐的存储,那就肯定涉及到前端的上传音乐功能了,可能是由于我们公司的编辑们所制作的BGM质量比较高,所以每一个BGM文件都会比较大,每一个都在20M以上,所以我使用了大文件的分片上传,并做了暂停上传续传功能,接下来就通过一个小demo,给大家演示一下吧!!!

BGM切片上传

1.大致流程

分为以下几步:

  • 1.前端接收BGM并进行切片
  • 2.将每份切片都进行上传
  • 3.后端接收到所有切片,创建一个文件夹存储这些切片
  • 4.后端将此文件夹里的所有切片合并为完整的BGM文件
  • 5.删除文件夹,因为切片不是我们最终想要的,可删除
  • 6.当服务器已存在某一个文件时,再上传需要实现“秒传”

2.前端实现切片

简单来说就是,咱们上传文件时,选中文件后,浏览器会把这个文件转成一个Blob对象,而这个对象的原型上上有一个slice方法,这个方法是大文件能够切片的原理,可以利用这个方法来给大文件切片

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

data() {
    return {
        fileObj: {
            file: null
        }
    };
  },
  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长什么样子
      },
      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
      }

例子我就用我最近很喜欢听得一首歌嘉宾-张远,他的大小是32M

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

点击上传,看看chunkList长什么样子吧:

image.png

证明我们切片成功了!!!分成了7个切片

3.上传切片并展示进度条

我们先封装一个请求方法,使用的是axios

import axios from "axios";

axiosRequest({
      url,
      method = "post",
      data,
      headers = {},
      onUploadProgress = (e) => e, // 进度回调
    }) {
      return new Promise((resolve, reject) => {
        axios[method](url, data, {
          headers,
          onUploadProgress, // 传入监听进度回调
        })
          .then((res) => {
            resolve(res);
          })
          .catch((err) => {
            reject(err);
          });
      });
    }

接着上一步,我们获得了所有切片,接下来要把这些切片保存起来,并逐一去上传

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(); // 执行上传切片的操作
    },

uploadChunks就是执行上传所有切片的函数

+ 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]
+            ), // 传入监听上传进度回调
+          })
+        );
+      await Promise.all(requestList); // 使用Promise.all进行请求
+    },
+ createProgressHandler(item) {
+      return (e) => {
+         // 设置每一个切片的进度百分比
+        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`); // 切片存储目录

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;
            // 保存切片的文件夹的路径,比如  张远-嘉宾.flac-chunks
            const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`);
            // // 切片目录不存在,创建切片目录
            if (!fse.existsSync(chunkDir)) {
                await fse.mkdirs(chunkDir);
            }
            // 把切片移动到切片文件夹
            await fse.move(file.path, `${chunkDir}/${chunkName}`);
            res.end(
                JSON.stringify({
                    code: 0,
                    message: "切片上传成功"
                }));
        });
    }
})

server.listen(3000, () => console.log("正在监听 3000 端口"));

接下来就是页面上进度条的显示了,其实很简单,我们想要展示总进度条,和各个切片的进度条,各个切片的进度条我们都有了,我们只需要算出总进度就行,怎么算呢?这么算:各个切片百分比 * 各个切片的大小 / 文件总大小

+ <div style="width: 300px">
+      总进度:
+      <el-progress :percentage="totalPercent"></el-progress>
+      切片进度:
+      <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 === 0) return 0;
+      const loaded = fileObj.chunkList
+        .map(({ size, percent }) => size * percent)
+        .reduce((pre, next) => pre + next);
+      return parseInt((loaded / fileObj.file.size).toFixed(2));
+    },
+  },

我们再次上传音乐,查看效果:

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

后端也成功保存了

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

4.合并切片为BGM

好了,咱们已经保存好所有切片,接下来就要开始合并切片了,我们会发一个/merge请求,叫后端合并这些切片,前端代码添加合并的方法:

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]
            ),
          })
        );
      await Promise.all(requestList); // 使用Promise.all进行请求

+      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接口:

// 接收请求的参数
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
    // 获取切片文件夹里所有切片,返回一个数组
    chunkPaths = await fse.readdir(chunkDir);
    // 根据切片下标进行排序
    // 否则直接读取目录的获得的顺序可能会错乱
    chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
    const arr = chunkPaths.map((chunkPath, index) => {
        return pipeStream(
            path.resolve(chunkDir, chunkPath),
            // 指定位置创建可写流
            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({
                code: 0,
                message: "文件合并成功"
            })
        );
    }

现在我们重新上传音乐,发现切片上传成功了,也合并成功了:

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

5.删除切片

上一步我们已经完成了切片合并这个功能了,那之前那些存在后端的切片就没用了,不然会浪费服务器的内存,所以我们在确保合并成功后,可以将他们删除

// 合并切片
const mergeFileChunk = async (filePath, fileName, size) => {
    // filePath:你将切片合并到哪里,的路径
    const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`);
    let chunkPaths = null
    // 获取切片文件夹里所有切片,返回一个数组
    chunkPaths = await fse.readdir(chunkDir);
    // 根据切片下标进行排序
    // 否则直接读取目录的获得的顺序可能会错乱
    chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
    const arr = chunkPaths.map((chunkPath, index) => {
        return pipeStream(
            path.resolve(chunkDir, chunkPath),
            // 指定位置创建可写流
            fse.createWriteStream(filePath, {
                start: index * size,
                end: (index + 1) * size
            })
        )
    })
    await Promise.all(arr)
    fse.rmdirSync(chunkDir); // 合并后删除保存切片的目录
};

我们再次上传,再看看,那个储存此音乐的切片文件夹被我们删了

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

6.秒传功能

所谓的秒传功能,其实没那么高大上,通俗点说就是,当你上传一个文件时,后端会判断服务器上有无这个文件,有的话就不执行上传,并返回给你“上传成功”,想要执行此功能,后端需要重新写一个接口/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({
                    shouldUpload: false
                })
            );
        } else {
            res.end(
                JSON.stringify({
                    shouldUpload: true
                })
            );
        }

前端在上传文件步骤也要做拦截:

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,
        percent: 0,
        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
+     }

现在我们重新上传音乐,因为服务器上已经存在了张远-嘉宾这首歌了,所以,直接alert出秒传:上传成功

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

暂停续传

1.大致流程

暂停续传其实很简单,比如一个文件被切成10片,当你上传成功5片后,突然暂停,那么下次点击续传时,只需要过滤掉之前已经上传成功的那5片就行,怎么实现呢?其实很简单,只需要点击续传时,请求/verity接口,返回切片文件夹里现在已成功上传的切片列表,然后前端过滤后再把还未上传的切片的继续上传就行了,后端的/verify接口需要做一些修改

if (req.url === "/verify") {
        // 返回已经上传切片名列表
        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({
                    shouldUpload: false
                })
            );
        } else {
            res.end(
                JSON.stringify({
                    shouldUpload: true,
+                     uploadedList: await createUploadedList(`${fileName}-chunks`)
                })
            );
        }
    }

2.暂停上传

前端增加一个暂停按钮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,确保能续传
+     }

3.续传

增加一个续传按钮,并增加一个keepUpload事件

+ <el-button @click="keepUpload"> 续传 </el-button>

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

4.优化进度条

续传中,由于那些没有上传的切片会从零开始传,所以会导致总进度条出现倒退现象,所以我们要对总进度条做一下优化,确保他不会倒退,做法就是维护一个变量,这个变量只有在总进度大于他时他才会更新成总进度

总进度:
+ <el-progress :percentage="tempPercent"></el-progress>

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

这个demo比较粗糙,有些地方没有考虑到的,请同学们指出。谢谢了!!!

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/mj5gFP3nIYY_BUKtbnToZQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237228次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录