大文件上传

单个文件直接上传

1、前端根据input组件的upload功能唤起文件管理器,然后选择文件,日常文件上传类型一般有两种
:以base64字符串上传(使用fileReader对象获取文件的base64字符串)
:以二进制流文件上传(使用formDate模拟form表单上传)
2、后端根据文件的类型来判别或者进行对应的逻辑处理
3、接口返回成功标识,如果有需要会显示网络链接

base64格式

是一种基于64个可打印字符来表示二进制数据的表达方法,常用语处理文本数据的场合,表示、传输、存储一些二进制数据
图片的base64编码就是将一张图片编码成一串字符串,使用该字符串可以代替图片地址,直接在浏览器打卡可以访问地址

formData格式

FormData参考文档: FormData - Web API 接口参考 | MDN

formData就是将form表单元素的name和value进行组合,实现表单数据的序列化,从而减少表单元素的拼接,提高工作效率。
Web API 提供了FormData方法,提供了一种表示表单数据的键值对的构造方式,通过FormData.append(key, value)向FormData中添加新的属性值。

前端

        <!-- 上传 -->
        <el-upload
          class="upload-demo"
          drag
          action="http://localhost:300/pc/upload"
          accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
          :before-upload="parseFile"
          :http-request="upload"
          :data="additionalData"
        >
parseFile(file) {
      this.additionalData = { type: "test" };
      return new Promise((resovle) => {
        this.$nextTick(() => {
          resovle(true);
        });
      });
    },

    upload(e) {
      console.log(e);
      let formData = new FormData();
      formData.append("file", e.file);
      formData.append("type", "test");
      this.$myrequest({
        url: "http://localhost:300/pc/upload",
        method: "post",
        headers: {
          "Content-Type": "multipart/form-data",
        },
        data: formData,
      }).then((res) => {
        console.log(res);
      });
    },

后端


//文件上传
const path=require('path')
app.use(koaBody({
	multipart:true,
	enableTypes:['json','form','text'],
	keepExtensions: true,
	formidable: {
        maxFileSize: 500*1024*1024	// 设置上传文件大小最大限制,默认2M
    },
	//是否支持multipart-formdate的表单
}));

router.post('/upload',async ctx=>{
const file=ctx.request.files.file//是一个文件数组,file是定义接受的参数名,也是前端传递的参数名,注意对应
//ctx.request.files 文件的类型、大小来限制上传文件的类型和大小,进行图片、视频、zip文件等的上传划分
const filename=file.originalFilename//上传文件的文件名字
const uploadPath=path.join(__dirname, '../target')
//文件的全路径
const filePath=`${uploadPath}/${filename}}`
return new Promise((resolve,reject)=>{
	const reader=fs.createReadStream(file.filepath)
	const upStream=fs.createWriteStream(filePath)//没有文件路径之前需要先创建一个文件路径,创建可写流
	//对可写流进行监听
	upStream.on('open',function(){console.log('open');})
	//流写入成功后调用的事件,在这里处理返回的结果
	upStream.on('finish',function(){
		console.log('finish');
		//对图片计算md5值,或者处理自己的逻辑
	})
	upStream.on('close', function () {
		console.log("close");
	  });
	  upStream.on('error', function (err) {
		  // 有错误的话,在这个里面处理
		  console.log("error", err);
		  reject(err)
	  });
	  // 可读流通过管道写入可写流
	  reader.pipe(upStream);
	  ctx.body = { "code": 200, "description": "上传成功",url:filePath };

})
})

在这里插入图片描述
在这里插入图片描述
注意:
koa-body的版本不同的话,获取的数据会不同,注意属性名字,一个是PresistentFile一个是file类型,两者可以进行上传,只需要取值字段对应即可

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

后端返回文件网络地址-前端下载文件

流程:前端请求下载接口,后端直接返回一个二进制文件流或者文件网络地址,前端创建a标签进行href属性直接赋值,添加download属性然后代码触发点击,最后消除标签即可

后端

需要用到koa-static:静态资源请求中间件,静态资源例如(html、js、css、jpg、png等)

const static=require('koa-static')

app.use(static(__dirname,+'/target',{
	index:false,
	hidden:false,
	defer:false
}))

//下载文件
router.get('/url',async (ctx,next)=>{
	let id=ctx.request.query.id
	 //这里应该是根据标识来对数据库进行查询然后返回对应的url
	let url = "http://localhost:3000/target/1寸.jpg";
    //响应首部 Access-Control-Expose-Headers 就是控制“暴露”的开关,它列出了哪些首部可以作为响应的一部分暴露给外部。
  ctx.set("Access-Control-Expose-Headers","content-disposition");
  ctx.set('content-disposition', 'attachment;filename=' + encodeURIComponent('1寸.jpg'))//前端通过这个响应头拿到文件的名称
  ctx.body = { code: 200, msg: "操作成功", url: url };
})

前端

    <el-button @click="downloadUrl">下载文件text</el-button>
     downloadUrl() {
      this.$myrequest({
        url: "/url",
        method: "get",
      }).then((res) => {
        let url = res.data.url;
        console.log(res);
        const fileName = res.headers["content-disposition"]
          .split(";")[1]
          .split("filename=")[1];
        console.log(url);
        this.changeBlob(url).then((res) => {
          const blob = new Blob([res]);
          console.log("blob", blob);
          if ("download" in document.createElement("a")) {
            // 非IE下载
            const elink = document.createElement("a");
            elink.download = fileName;
            elink.style.display = "none";
            elink.href = window.URL.createObjectURL(blob);
            document.body.appendChild(elink);
            elink.click();
            URL.revokeObjectURL(elink.href); // 释放URL对象
            document.body.removeChild(elink);
          } else {
            // IE10+ 下载
            navigator.MsSaveBlob(blob, name);
          }
        });
      });
    },
    // 地址转文件
    changeBlob(url) {
      console.log(33);
      //模拟发送http请求,将文件链接转换成文件流
      return new Promise((resolve) => {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", url, true);
        xhr.responseType = "blob";
        xhr.onload = () => {
          if (xhr.status === 200) {
            resolve(xhr.response);
          }
        };
        xhr.send();
      });
    },

在这里插入图片描述
但这个地址点进去,依然是不可得到的

后端返回二进制流-前端下载文件

后端

//下载文件
router.get("/url", async (ctx, next) => {
  let id = ctx.request.query.id;
  //这里应该是根据标识来对数据库进行查询然后返回对应的url
  let filePath =path.join(__dirname, "../target")+ "/1寸.jpg";
  //响应首部 Access-Control-Expose-Headers 就是控制“暴露”的开关,它列出了哪些首部可以作为响应的一部分暴露给外部。
  let fileName = "1寸.jpg";
  const file = fs.readFileSync(filePath);

  ctx.set("Access-Control-Expose-Headers", "content-disposition");
  ctx.set(
    "content-disposition",
    "attachment;filename=" + encodeURIComponent(fileName)
  ); //前端通过这个响应头拿到文件的名称
  ctx.body = file;
});

前端

    downloadUrl() {
      this.$myrequest({
        url: "/url",
        method: "get",
      }).then((res) => {
        let url = res.data.url;
        const fileName = res.headers["content-disposition"]
          .split(";")[1]
          .split("filename=")[1];
        const blob = new Blob(url);
        if ("download" in document.createElement("a")) {
          // 非IE下载
          const elink = document.createElement("a");
          elink.download = fileName;
          elink.style.display = "none";
          elink.href = window.URL.createObjectURL(blob);
          document.body.appendChild(elink);
          elink.click();
          URL.revokeObjectURL(elink.href); // 释放URL对象
          document.body.removeChild(elink);
        } else {
          // IE10+ 下载
          navigator.MsSaveBlob(blob, fileName);
        }
      });
    },

在这里插入图片描述
但为什么打开图片文件是损坏的图片

//下载文件
router.post("/url", async (ctx, next) => {
  const name = ctx.request.body.name;
  console.log(name);
  const uploadPath = path.join(__dirname, "../target");

  const path1 = `${uploadPath}/${name}`;
  console.log(path1);
  ctx.attachment(path1);
  ctx.body = path1;
});

    downloadUrl() {
      this.$myrequest({
        url: "http://localhost:300/pc/url",
        method: "post",
        data: { name: "62f839e88cb4134148b655d00" },
      }).then((res) => {
        console.log(res);
		window.open(res)
      });
    },

在这里插入图片描述
前端请求参数为文件名,后端返回该文件名在服务器中的存储路径

大文件分片上传

参考:
面试官:你如何实现大文件上传
字节跳动面试官:请你实现一个大文件上传和断点续传

大文件上传的思路:
利用Blob.prototype.slice方法,文件的slice可以返回原文件的某一个切片,和数组的slice类似
前端:

1、预先定好单个切片大小,将文件切分为一个个切片,然后借助Promise.all去并发网络请求,同时上传多个切片,这样从原本传一个大文件,变成了并发传多个小的文件切片,减少上传的顺序
由于是并发,需要利用FotmData(),append的方法,给每个切片添加属性,标志是哪一个切片

服务端:
负责接受前端传输的切片,并在接受到所有切片后合并所有的切片

使用Node的http模块搭建的服务器,前端使用axios来发送请求
开始总是报错
在这里插入图片描述
但是合并这个接口使用postman中发送json格式的数据,有可以成功(单独发送)前面并没有访问上传分片的接口

这里需要插入,axios在方post请求的时候,已经会默认把数据转换为JSON格式的数据

后面排查好久,发现是后端代码的错误,后端总共写了两个接口一个是upload还有merge的接口。但是我在监听访问的时候,将if判断语句写在了一个server.on里面了
那么就会出错,因为merge接口里面需要用到req.on去监听参数的传递
解决办法就是再重新开一个server.on,或者在req.on监听参数的传递之前判断一下req.url的参数

后端

const http = require("http");
const multiparty = require("multiparty");
const path = require("path");
const fse = require("fs-extra");
const bodyParser = require("body-parser");
const server = http.createServer();
const UPLOAD_DIR = path.resolve(__dirname, ".", "qiepian");//在当前文件的根目录下去创建一个qiepian的文件夹

function resolvePost(req) {
  return new Promise((resolve) => {
    let chunk = "";
    req.on("data", (data) => {
      //req接收到了前端的数据
      console.log(4);
      chunk += data;
    });
    req.on("end", () => {
      console.log(chunk);
      resolve(JSON.parse(chunk));
    });
  });
}

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

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;
  }
  if (req.url === "/upload") {
    const multipart = new multiparty.Form();
    multipart.parse(req, async (err, fields, files) => {
      if (err) {
        return res.end("解析错误");
      }
      console.log("fields", fields);
      console.log("files=", files);
      const [file] = files.file;
      const [fileName] = fields.fileName;
      const [chunkName] = fields.chunkName;
      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.on('request',async (req,res)=>{
	if (req.method === "OPTIONS") {
	  (res.status = 200), res.end();
	  return;
	}
	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: "文件合成功",
		  })
		);
	  }
})



async function mergeFileChunk(filePath, fileName, size) {
  const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`);
  let 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);
}

server.listen(400, () => {
  console.log("项目启动test");
});

前端

      <div v-if="step == 1">
        <!-- 上传 -->
        <el-upload
          class="upload-demo"
          drag
          action="#"
          :before-upload="parseFile"
        >
          <i class="el-icon-upload"></i>

          <div class="el-upload-text">
            <p v-if="files">已选择:{{ files.name }}</p>
            点击或拖拽访固定资产导入文件到此处,导入住在、商户数量受许可限制
          </div>
        </el-upload>
        <el-button type="primary" @click="upFileParse">下一步</el-button>
      </div>

import axios from "axios";
export default {
  components: { breadCrumb, tableStep2, dataCreateSuc, DataCreateSuc },
  mixins: [mixins],
  data() {
    return {
      files: null,
      additionalData: null,
      errorData: [],
      rightData: [],
      step: 1,
      chunkList: [],

请求部分

 methods: {
    createChunk(file, size = 2 * 20000) {
      const chunkList = [];
      let cur = 0;
      while (cur < file.size) {
        chunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return chunkList;
    },

    parseFile(file) {
      this.files = file;
      //并完成文件的切片
      this.chunkList = this.createChunk(this.files);
      console.log(this.chunkList);
      return false;
    },
    //给每个切片都封装一个promise,用于并发请求
    async uploadFile(list) {
      const requestList = list
        .map(({ file, fileName, index, chunkName }) => {
          const formData = new FormData();
          formData.append("file", file);
          formData.append("fileName", fileName);
          formData.append("index", index);
          formData.append("chunkName", chunkName);
          return { formData, index };
        })
        .map(({ formData, index }) =>
          axios
            .request({
              method: "post",
              url: "http://localhost:400/upload",
              header: { "Content-Type": "application/x-www-form-urlencoded" },
              data: formData,
            })
            .then((res) => {
              console.log(res, index);
            })
        );
      await Promise.all(requestList); //当全部分片上传完毕之后,通知后端去合并分片
      this.mergeChunk();
    },
    async mergeChunk() {
      var data = JSON.stringify({
        size: this.files.size,
        fileName:this.files.name,
      });
      axios
        .request({
          method: "post",
          url: "http://localhost:400/merge",
          headers: {
            "Content-Type": "application/json",
            "Content-Length": data.length,
          },
          data: data,
        })
        .then((res) => {
          console.log(res);
        });
    },
    upFileParse() {
      //确认上传
      //上传文件
      const uploadList = this.chunkList.map(({ file }, index) => ({
        file,
        size: file.size,
        percent: 0,
        chunkName: `${this.files.name}-${index}`,
        fileName: this.files.name,
        index,
      }));
      this.uploadFile(uploadList);
    },

在这里插入图片描述

完整步骤

1、分片上传

1、input监听change事件,保存文件
2、当点击上传的时候,先对文件进行分片,分片之后,该每一片加上一些属性信息
3、给分片后的数组,包装为一个promise请求数组
4、使用Promise.all发送并发请求
5、并发请求完毕之后,向后端发送请求,请求合并分片

前端

<template>
  <div>
    新建装修
    <input type="file" id="input" @change="parseFile" />
    <el-button @click="upFileParse">上传</el-button>
    <bread-crumb></bread-crumb>
  </div>
</template>

<script>
import breadCrumb from "components/breadCrumb";
import axios from "axios";
const SIZE = 10 * 1024 * 1024;
const Status = {
  wait: "wait",
  pause: "pause",
  uploading: "uploading",
};
export default {
  components: { breadCrumb },
  name: "createVue",
  data() {
    return {
      container: {
        file: null,
      },
      data: [], //保存切片文件数组
    };
  },
  methods: {
    parseFile(e) {
      this.container.file = e.target.files[0];
    },
    createFileChunk(file, size =SIZE) {//使用while循环和slice方法将切片放入数组中并返回
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    },
    async uploadChunks() {
      const requestList = this.data
        .map(({ chunk, hash }) => {
          const formData = new FormData();//将切片文件、切片hash、文件名放入到formData中
          formData.append("chunk", chunk);
          formData.append("hash", hash);
          formData.append("filename", this.container.file.name);
          return { formData }; //一定要加括号
        })
        .map(({ formData }) =>
          axios
            .request({
              method: "post",
              url: "http://localhost:401",
              header: { "Content-Type": "application/x-www-form-urlencoded" },
              data: formData,
            })
            .then((res) => {
              console.log(res);
            })
        );
      await Promise.all(requestList);
            this.mergeRequest()
    },
    async mergeRequest() {
      axios.request({
        url: "http://localhost:401/merge",
        method: "post",
        headers: {
          "Content-Type": "application/json",
        },
        data: JSON.stringify({ filename: this.container.file.name,size:SIZE }),
      });
    },
    async upFileParse() {
      console.log(3);
      if (!this.container.file) return;
      const fileChunkList = this.createFileChunk(this.container.file);
      this.data = fileChunkList.map(({ file }, index) => ({//2、生成切片后给切片一个标识为hash,暂时用文件名+下标标识,这样后端就可以知道当前切片是第几个切片,用于之后的合并切片
        chunk: file,
        hash: this.container.file.name + "-" + index,
      }));
      await this.uploadChunks();//去上传所有的切片
    },
  },
};
</script>

<style></style>

后端

const http = require("http");
const multiparty = require("multiparty");
const path = require("path");
const fse = require("fs-extra");
const bodyParser = require("body-parser");
const server = http.createServer();
const UPLOAD_DIR = path.resolve(__dirname, ".", "qiepian2");
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;
  }
  if (req.url === "/") {
    const multipart = new multiparty.Form();//使用multiparty处理前端来的formData
    multipart.parse(req, async (err, fields, files) => {//files参数保存了formData中文件,fields参数保存了formData中的非文件参数字段
      if (err) {
        console.log(err);
        return;
      }
      const [chunk] = files.chunk;
      const [hash] = fields.hash;
      const [filename] = fields.filename;
      //创建临时文件夹,用于临时存储chunk
      //添加chunkDir前缀与文件名区分
      const chunkDir = path.resolve(UPLOAD_DIR, "chunkDir" + filename);
      if (!fse.existsSync(chunkDir)) {
        await fse.mkdirs(chunkDir);
      }

      await fse.move(chunk.path, `${chunkDir}/${hash}`);
      res.end(
        JSON.stringify({
          code: 0,
          message: "切片上传成功",
        })
      );
    });
  }
});

const resolvePost = (req) => {
  return new Promise((resolve) => {
    let chunk = "";
    req.on("data", (data) => {
      chunk += data;
    });
    req.on("end", () => {
      resolve(JSON.parse(chunk));
    });
  });
};
//写入文件流
function pipeStream(path, writeStream) {
  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) => {
  const chunkDir = path.resolve(UPLOAD_DIR, "chunkDir" + filename);
  const chunkPaths = await fse.readdir(chunkDir);
  chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
  console.log(size);
  await Promise.all(
    chunkPaths.map((chunkPath, index) =>
      pipeStream(
        path.resolve(chunkDir, chunkPath),
        fse.createWriteStream(filePath, { start: index * size })
      )
    )
  );
  //合并后删除保存切片的目录
  fse.rmdirSync(chunkDir)
};

server.on("request", async (req, res) => {
  if (req.method === "OPTIONS") {
    (res.status = 200), res.end();
    return;
  }
  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,
		mess:"success"
	}))
  }
});

server.listen(401, () => {
  console.log("项目启动test");
});

在这里插入图片描述
在这里插入图片描述

2、显示上传进度条

上传进度分为两种:1、每个切片的上传进度2、整个文件的上传进度
整个文件的上传进度是基于每个切片上传进度计算而来,

单个切片进度条

后端不需要做任何处理,前端使用axios提供的API-onUploadProgress去完成

在这里插入图片描述

progressEvent.loaded表示当前上传的数据大小,progressEvent.total表示整个要上传的数据带大小
由于每个切片都要触发独立的监听事件,以及显示上传进度,使用table来渲染,然后再使用一个工厂函数,根据传入的切片返回不同的监听函数

在这里插入图片描述
在这里插入图片描述

<template>
  <div>
    新建装修
    <input type="file" id="input" @change="parseFile" />
    <el-button @click="upFileParse">上传</el-button>

  ---  <el-table :data="data">
      <el-table-column
        prop="hash"
        label="chunk hash"
        align="center"
      ></el-table-column>
      <el-table-column label="size(KB)" align="center" width="120">
        <template v-slot="{ row }">
          {{ row.size|transformByte}}
        </template>
      </el-table-column>
      <el-table-column label="percentage" align="center">
        <template v-slot="{ row }">
          <el-progress
            :percentage="row.percentage"
            color="#909399"
          ></el-progress>
        </template>
      </el-table-column>
----    </el-table>

    <bread-crumb></bread-crumb>
  </div>
</template>

<script>
import breadCrumb from "components/breadCrumb";
import axios from "axios";
const SIZE = 10 * 1024 * 2;
export default {
  components: { breadCrumb },
  name: "createVue",
---  filters: {
    transformByte(val) {
      return Number((val / 1024).toFixed(0));
    },
  },
  data() {
    return {
      container: {
        file: null,
      },
      data: [], //保存切片文件数组
    };
  },
  methods: {
    parseFile(e) {
      this.container.file = e.target.files[0];
    },
    createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    },
    async uploadChunks() {
      const requestList = this.data
--        .map(({ chunk, hash, index }) => {
          const formData = new FormData();
          formData.append("chunk", chunk);
          formData.append("hash", hash);
          formData.append("filename", this.container.file.name);
  --        return { formData, index }; //一定要加括号
        })
   --     .map(({ formData, index }) =>
          axios
            .request({
              method: "post",
              url: "http://localhost:401",
              header: { "Content-Type": "application/x-www-form-urlencoded" },
    ---          onUploadProgress: this.uploadProgressEvent(this.data[index]),
              data: formData,
            })
            .then((res) => {
              console.log(res);
            })
        );
      await Promise.all(requestList);
      this.mergeRequest();
    },
    async mergeRequest() {
      axios.request({
        url: "http://localhost:401/merge",
        method: "post",
        headers: {
          "Content-Type": "application/json",
        },
        data: JSON.stringify({
          filename: this.container.file.name,
          size: SIZE,
        }),
      });
    },
    async upFileParse() {
      console.log(3);
      if (!this.container.file) return;
      const fileChunkList = this.createFileChunk(this.container.file);
      this.data = fileChunkList.map(({ file }, index) => ({
        chunk: file,
  --      index,
        hash: this.container.file.name + "-" + index,
--		size:file.size,
   --     percentage: 0,
      }));
      await this.uploadChunks();
    },
 --   uploadProgressEvent(item) {
      return (e) => {
        item.percentage = parseInt(String((e.loaded / e.total) * 100));
      };
    },
  },
};
</script>

<style></style>

每个切片在上传的时候都会监听函数更新data数组对应元素的percentage属性,之后把将data数组放到视图中展示

总进度条

将每个切片已经上传的部分累加,除以整个文件的带下,就能得出当前文件的上传进度—计算属性

    <el-progress
      type="circle"
      :percentage="uploadPercentage"
      width="50"
    ></el-progress>
  computed: {
    uploadPercentage() {
      if (!this.container.file || !this.data.length) return 0;
      const loaded = this.data
        .map((item) => item.size * item.percentage)
        .reduce((acc, cur) => acc + cur);
      return parseInt((loaded / this.container.file.size).toFixed(2));
    },
  },

在这里插入图片描述

断点续传

原理:前端/服务端需要记住已经上传的切片,这样下次上传就可以跳过之前上传的部分,怎么记住???

1、前端使用localStorage记录已经上传的切片的hash
2、服务端保存已经上传的切片的hash,前端每次上传前向服务端获取已经上传的切片
使用2

秒传

首先对于整个文件的判断,如果文件名hash与后端保存的文件hash一样。那就不需要再上传了,直接返回上传成功–秒传的感觉

1、使用spark-md5根据文件内容创建文件hash,当大文件的时候,会阻塞UI渲染,因此使用web-worker新建一个线程,来完成文件名创建hash
2、在前端点击上传的时候,先请求verify接口,看该文件是否已经上传
3、没有上传,那么依次执行分片和并请求,上传了就直接返回成功

文件名用hash表示

无论是前端还是服务端,都必须要生成文件和切片的hash,而不是通过初始的文件名来进行判断或存储,目的:只要文件内容不变,hash就不回改变,因此是根据文件内容生成hash

spark-md5 根据文件内容计算出文件的hash值
但如果是一个特别大的文件,读取文件的内容来生成hash值,会很费时间并且会引起UI的阻塞,导致页面假死,因此使用web-worker在worker线程计算hash,这样用户仍然可以在主界面交互

web-worker
JS采用的是单线程模型(对于多核CPU,无法发挥计算机的能力)
web-worker就是创建一个新的线程,在主线程运行的同时,Worker线程在后台运行,等计算完毕之后,再把结果返回给主线程。
web-worker一旦创建,就会始终运行,且不会被主线程上的活动打断,因此一旦使用完毕,就应该关闭

1、同源限制
2、DOM限制
3、通信联系
4、脚本限制
5、文件限制

Web Worker 使用教程
作者: 阮一峰

/public/hash.js
// 导入脚本
// import script for encrypted computing
self.importScripts("/spark-md5.min.js");

// 生成文件 hash
// create file hash
self.onmessage = e => {
	console.log(e.data);
  const { fileChunkList } = e.data;
  const spark = new self.SparkMD5.ArrayBuffer();
  let percentage = 0;
  let count = 0;
  const loadNext = index => {
    const reader = new FileReader();
    reader.readAsArrayBuffer(fileChunkList[index].file);
    reader.onload = e => {
      count++;
      spark.append(e.target.result);
      if (count === fileChunkList.length) {
        self.postMessage({
          percentage: 100,
          hash: spark.end()
        });
        self.close();
      } else {
        percentage += 100 / fileChunkList.length;
        self.postMessage({
          percentage
        });
        loadNext(count);
      }
    };
  };
  loadNext(0);
};

前端主线程与worker的通信
    //生成文件hash
    calculateHash(fileChunkList) {
      return new Promise((resolve) => {
        this.container.worker = new Worker("/hash.js");
        this.container.worker.postMessage({ fileChunkList });
        this.container.worker.onmessage = (e) => {
          const { percentage, hash } = e.data;
          this.hashPercentage = percentage;
          if (hash) {
            resolve(hash);
          }
        };
      });
    },
    async upFileParse() {
      if (!this.container.file) return;
      const fileChunkList = this.createFileChunk(this.container.file);
      this.container.hash = await this.calculateHash(fileChunkList);
      const { shouldUpload } = await this.verifyUpload(
        this.container.file.name,
        this.container.hash
      );
      if (!shouldUpload) {
        alert("文件已经上成功");
        return;
      }
      this.data = fileChunkList.map(({ file }, index) => ({
        chunk: file,
        fileHash: this.container.hash,
        index,
        hash: this.container.file.name + "-" + index,
        size: file.size,
        percentage: 0,
      }));
      await this.uploadChunks();
    },
    //上传文件前先判断服务端是否已经存在上传资源,如果已经有了则直接返回上传成功的信息
    async verifyUpload(filename, fileHash) {
      const { data } = await axios.request({
        url: "http://localhost:401/verify",
		method:"post",
        headers: {
          "Content-Type": "application/json",
        },
        data: JSON.stringify({ filename, fileHash }),
      });
      console.log(data);
      return data;//不需要使用JSON.parse(data),axios内部已经做了转换
    },
后端
const http = require("http");
const multiparty = require("multiparty");
const path = require("path");
const fse = require("fs-extra");
const bodyParser = require("body-parser");
const server = http.createServer();
const UPLOAD_DIR = path.resolve(__dirname, ".", "qiepian2");

const extractExt = (filename) =>
  filename.slice(filename.lastIndexOf("."), filename.length); //提取文件名

// 创建临时文件夹用于临时存储 chunk
// 添加 chunkDir 前缀与文件名做区分
// create a directory for temporary storage of chunks
// add the 'chunkDir' prefix to distinguish it from the chunk name
const getChunkDir = (fileHash) =>
  path.resolve(UPLOAD_DIR, `chunkDir_${fileHash}`);

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;
  }
  if (req.url === "/") {
    const multipart = new multiparty.Form();
    multipart.parse(req, async (err, fields, files) => {
      if (err) {
        console.log(err);
        return;
      }
      const [chunk] = files.chunk;
      const [hash] = fields.hash;
      const [fileHash] = fields.fileHash;
      const [filename] = fields.filename;
      const filePath = path.resolve(
        UPLOAD_DIR,
        `${fileHash}${extractExt(filename)}`
      );
      //   const chunkDir = path.resolve(UPLOAD_DIR, "chunkDir" + filename);
      const chunkDir = getChunkDir(fileHash);
      const chunkPath = path.resolve(chunkDir, hash);
      // 文件存在直接返回
      // return if file is exists
      if (fse.existsSync(filePath)) {
        res.end("file exist");
        return;
      }
      // 切片存在直接返回
      // return if chunk is exists
      if (fse.existsSync(chunkPath)) {
        res.end("chunk exist");
        return;
      }

      // 切片目录不存在,创建切片目录
      if (!fse.existsSync(chunkDir)) {
        await fse.mkdirs(chunkDir);
      }

      await fse.move(chunk.path, `${chunkDir}/${hash}`);
      res.end(
        JSON.stringify({
          code: 0,
          message: "切片上传成功",
        })
      );
    });
  }
});

const resolvePost = (req) => {
  return new Promise((resolve) => {
    let chunk = "";
    req.on("data", (data) => {
      chunk += data;
    });
    req.on("end", () => {
      resolve(JSON.parse(chunk));
    });
  });
};
function pipeStream(path, writeStream) {
  return new Promise((resolve) => {
    const readStream = fse.createReadStream(path);
    readStream.on("end", () => {
      fse.unlinkSync(path);
      resolve();
    });
    readStream.pipe(writeStream);
  });
}

const mergeFileChunk = async (filePath, fileHash, size) => {
  //此时改成了根据fileHash而不是filename
  //   const chunkDir = path.resolve(UPLOAD_DIR, "chunkDir" + filename);
  const chunkDir = getChunkDir(fileHash);
  // 根据切片下标进行排序
  // 否则直接读取目录的获得的顺序会错乱
  const chunkPaths = await fse.readdir(chunkDir);
  chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
  await Promise.all(
    chunkPaths.map((chunkPath, index) =>
      pipeStream(
        path.resolve(chunkDir, chunkPath),
        fse.createWriteStream(filePath, { start: index * size })
      )
    )
  );
  fse.rmdirSync(chunkDir);
};

server.on("request", async (req, res) => {
  if (req.method === "OPTIONS") {
    (res.status = 200), res.end();
    return;
  }
  if (req.url === "/merge") {
    const data = await resolvePost(req);
    const { fileHash, filename, size } = data;
    const ext = extractExt(filename);
    // const filePath = path.resolve(UPLOAD_DIR, `${filename}`);
    const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
    await mergeFileChunk(filePath, fileHash, size);
    res.end(
      JSON.stringify({
        code: 0,
        mess: "success",
      })
    );
  }
});

server.on("request", async (req, res) => {
  if (req.method === "OPTIONS") {
    (res.status = 200), res.end();
    return;
  }
  if (req.url === "/verify") {
    const data = await resolvePost(req);
    const { fileHash, filename } = data;
    const ext = extractExt(filename);
    const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
    if (fse.existsSync(filePath)) {
      res.end(JSON.stringify({ shouldUpload: false }));
    } else {
      res.end(JSON.stringify({ shouldUpload: true }));
    }
  }
});

server.listen(401, () => {
  console.log("项目启动test");
});

断点续传=暂停上传+恢复上传

暂停上传需要使用axios提供的CancelToken取消请求
???取消请求之后怎么去保证合并请求这个请求不会发送给后端,因此之前在verify这个接口,后端需要返回目前切片文件夹有那几个切片,然后再切片请求中记录已经发出了多少个切片请求时成功的—注意这里和参考不同,参考是利用原生的XMLHttpRequest来发送请求的
当文件切片上传后,服务端会建立一个文件夹存储所有上传的切片,所以每次前端上传窜前就可以调用一个接口服务,服务端将已经上传的切片的切片名返回,前端再跳过这些已经上传切片,实现续传的效果

这个接口与之前秒传的接口合并:

1、服务端已经存在该文件,不需要再次上传
2、服务端不存在该文件或者已经上传部分文件切片,通知前端进行上传,并把已经上传的文件切片返回给前端

前端

<template>
  <div>

    <el-button @click="handleResume" v-if="status === Status.pause"
      >恢复上传</el-button
    >
    <el-button v-else @click="handlePause">暂停上传</el-button>


const Status = {
  wait: "wait",
  pause: "pause",
  uploading: "uploading",
};
const CancelToken = axios.CancelToken;
let source = CancelToken.source();

  data() {
    return {
      Status,
      status: Status.wait,
      container: {
        file: null,
        hash: "",
        worker: null,
      },
      requestList: [],//用来保存请求成功的切片请求数组
      hashPercentage: 0,
      data: [], //保存切片文件数组
    };
  },
  methods: {
    //生成文件hash
    calculateHash(fileChunkList) {},
    parseFile(e) {
      // source.cancel("终断上传");
      //source = CancelToken.source();
      this.container.file = e.target.files[0];
    },
    //上传文件前先判断服务端是否已经存在上传资源,如果已经有了则直接返回上传成功的信息
    async verifyUpload(filename, fileHash) {
      const { data } = await axios.request({
        url: "http://localhost:401/verify",
        method: "post",
        headers: {
          "Content-Type": "application/json",
        },
        data: JSON.stringify({ filename, fileHash }),
      });
      console.log(data);
      return data; //不需要使用JSON.parse(data),axios内部已经做了转换
    },
    createFileChunk(file, size = SIZE) {
    },
    async uploadChunks(uploadedList = []) {
      const requestList = this.data
        .filter(({ hash }) => !uploadedList.includes(hash))//过滤掉后端已经保存过了的切片,就不需要再发送请求了
        .map(({ chunk, hash, index }) => {
          const formData = new FormData();
          formData.append("chunk", chunk);
          formData.append("hash", hash);
          formData.append("fileHash", this.container.hash);
          formData.append("filename", this.container.file.name);
          return { formData, index }; //一定要加括号
        })
        .map(({ formData, index }) =>
          axios
            .request({
              method: "post",
              url: "http://localhost:401",
              header: { "Content-Type": "application/x-www-form-urlencoded" },
              onUploadProgress: this.uploadProgressEvent(this.data[index]),
              cancelToken: source.token,//取消请求事件监听
              data: formData,
            })
            .then((res) => {
              console.log(res);
              this.requestList.push(requestList);//请求成功的话,就往里面push一个元素
            })
            .catch((err) => {
              console.log(err);
            })
        );
      await Promise.all(requestList);//注意promise。all的用法,及时之前的请求失败了,那也算完成
      console.log(uploadedList, this.requestList, this.data);
      if (uploadedList.length + this.requestList.length === this.data.length) {//判断后端保存的文件切片数量和请求成功的切片数量加起来是不是等于总的切片数量,来确定是否发送并发请求
        console.log(333);
        await this.mergeRequest();
      }
    },
    async mergeRequest() { 
    },
    async upFileParse() {
      if (!this.container.file) return;
      this.status = Status.uploading;
      const fileChunkList = this.createFileChunk(this.container.file);
      this.container.hash = await this.calculateHash(fileChunkList);
      const { shouldUpload, uploadedList } = await this.verifyUpload(
        this.container.file.name,
        this.container.hash
      );
      if (!shouldUpload) {//障眼法---秒传效果
        alert("文件已经上成功");
        this.status = Status.wait;
        return;
      }
      this.data = fileChunkList.map(({ file }, index) => ({
        chunk: file,
        fileHash: this.container.hash,
        index,
        hash: this.container.file.name + "-" + index,
        size: file.size,
        percentage: 0,
      }));
      await this.uploadChunks(uploadedList);
    },
    uploadProgressEvent(item) { 
    },
    //暂停上传
    handlePause() {
      this.status = Status.pause;
      this.requestList = [];
      console.log("暂停上传了");
      source.cancel("暂停上传了");
      source = CancelToken.source(); //重置source,确保能够续传
    },
    async handleResume() {
      this.status = Status.uploading;
      const { uploadedList } = await this.verifyUpload(
        this.container.file.name,
        this.container.hash
      );
      await this.uploadChunks(uploadedList);
    },
  },
};
</script>

<style></style>

后端

const http = require("http");
const multiparty = require("multiparty");
const path = require("path");
const fse = require("fs-extra");
const bodyParser = require("body-parser");
const server = http.createServer();
const UPLOAD_DIR = path.resolve(__dirname, ".", "qiepian2");

const extractExt = (filename) =>
  filename.slice(filename.lastIndexOf("."), filename.length); //提取文件名

// 创建临时文件夹用于临时存储 chunk
// 添加 chunkDir 前缀与文件名做区分
// create a directory for temporary storage of chunks
// add the 'chunkDir' prefix to distinguish it from the chunk name
const getChunkDir = (fileHash) =>
  path.resolve(UPLOAD_DIR, `chunkDir_${fileHash}`);
//返回已经上传的所有切片名
const createUploadedList = async (fileHash) =>
  fse.existsSync(getChunkDir(fileHash))
    ? await fse.readdir(getChunkDir(fileHash))
    : [];

function pipeStream(path, writeStream) { 
}


const resolvePost = (req) => { 
  };
server.on("request", async (req, res) => { 
  if (req.url === "/") {
    const multipart = new multiparty.Form();
    multipart.parse(req, async (err, fields, files) => {
      if (err) {
        console.log(err);
        return;
      }
      const [chunk] = files.chunk;
      const [hash] = fields.hash;
      const [fileHash] = fields.fileHash;
      const [filename] = fields.filename;
      const filePath = path.resolve(
        UPLOAD_DIR,
        `${fileHash}${extractExt(filename)}`
      );
      //   const chunkDir = path.resolve(UPLOAD_DIR, "chunkDir" + filename);
      const chunkDir = getChunkDir(fileHash);
      const chunkPath = path.resolve(chunkDir, hash);
      // 文件存在直接返回
      // return if file is exists
      if (fse.existsSync(filePath)) {
        res.end("file exist");
        return;
      }
      // 切片存在直接返回
      // return if chunk is exists
      if (fse.existsSync(chunkPath)) {
        res.end("chunk exist");
        return;
      }

      // 切片目录不存在,创建切片目录
      if (!fse.existsSync(chunkDir)) {
        await fse.mkdirs(chunkDir);
      }

      await fse.move(chunk.path, path.resolve(chunkDir,hash));
      res.end(
        JSON.stringify({
          code: 0,
          message: "切片上传成功",
        })
      );
    });
  }
});


const mergeFileChunk = async (filePath, fileHash, size) => {
  //此时改成了根据fileHash而不是filename
  //   const chunkDir = path.resolve(UPLOAD_DIR, "chunkDir" + filename);
  const chunkDir = getChunkDir(fileHash);
  // 根据切片下标进行排序
  // 否则直接读取目录的获得的顺序会错乱
  const chunkPaths = await fse.readdir(chunkDir);
  chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
  await Promise.all(
    chunkPaths.map((chunkPath, index) =>
      pipeStream(
        path.resolve(chunkDir, chunkPath),
        fse.createWriteStream(filePath, { start: index * size })
      )
    )
  );
  fse.rmdirSync(chunkDir);
};

server.on("request", async (req, res) => {
  if (req.method === "OPTIONS") {
    (res.status = 200), res.end();
    return;
  }
  if (req.url === "/merge") {
    const data = await resolvePost(req);
    const { fileHash, filename, size } = data;
    const ext = extractExt(filename);
    // const filePath = path.resolve(UPLOAD_DIR, `${filename}`);
    const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
    await mergeFileChunk(filePath, fileHash, size);
    res.end(
      JSON.stringify({
        code: 0,
        mess: "success",
      })
    );
  }
});

server.on("request", async (req, res) => {
  if (req.method === "OPTIONS") {
    (res.status = 200), res.end();
    return;
  }
  if (req.url === "/verify") {
    const data = await resolvePost(req);
    const { fileHash, filename } = data;
    const ext = extractExt(filename);
    const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
    if (fse.existsSync(filePath)) {
      res.end(JSON.stringify({ shouldUpload: false }));
    } else {
      res.end(
        JSON.stringify({
          shouldUpload: true,
         ----- uploadedList: await createUploadedList(fileHash),
        })
      );
    }
  }
});

server.listen(401, () => {
  console.log("项目启动test");
});

全部代码----进度条的改进以及一些初始操作+删除所有上传的文件

前端

<template>
  <div>
    新建装修
    <input
      type="file"
      id="input"
      @change="parseFile"
      :disabled="status !== Status.wait"
    />
    <el-button @click="upFileParse" :disabled="uploadDisabled">上传</el-button>
    <el-button @click="handleResume" v-if="status === Status.pause"
      >恢复上传</el-button
    >
    <el-button
      v-else
      @click="handlePause"
      :disabled="
        status !== Status.uploading || !container.hash || !container.file
      "
      >暂停上传</el-button
    >
    <el-button @click="handleDelete">删除该所有上传的文件</el-button>

    <el-progress type="circle" :percentage="uploadPercentage"></el-progress>
    <el-progress type="circle" :percentage="fakeUploadPercentage">
    </el-progress>
    <div>calculate chunk hash</div>
    <el-progress :percentage="hashPercentage"></el-progress>

    <el-table :data="data">
      <el-table-column
        prop="hash"
        label="chunk hash"
        align="center"
      ></el-table-column>
      <el-table-column label="size(KB)" align="center" width="120">
        <template v-slot="{ row }">
          {{ row.size | transformByte }}
        </template>
      </el-table-column>
      <el-table-column label="percentage" align="center">
        <template v-slot="{ row }">
          <el-progress
            :percentage="row.percentage"
            color="#909399"
          ></el-progress>
        </template>
      </el-table-column>
    </el-table>

    <bread-crumb></bread-crumb>
  </div>
</template>

<script>
import breadCrumb from "components/breadCrumb";
import axios from "axios";
const SIZE = 10 * 1024 * 2;
const Status = {
  wait: "wait",
  pause: "pause",
  uploading: "uploading",
};
const CancelToken = axios.CancelToken;
let source = CancelToken.source();
export default {
  components: { breadCrumb },
  name: "createVue",
  filters: {
    transformByte(val) {
      return Number((val / 1024).toFixed(0));
    },
  },
  computed: {
    uploadPercentage() {
      if (!this.container.file || !this.data.length) return 0;
      const loaded = this.data
        .map((item) => item.size * item.percentage)
        .reduce((acc, cur) => acc + cur);
      console.log(parseInt((loaded / this.container.file.size).toFixed(2)));
      return parseInt((loaded / this.container.file.size).toFixed(2));
    },
    uploadDisabled() {
      return (
        !this.container.file ||
        [Status.pause, Status.uploading].includes(this.status)
      );
    },
  },
  data() {
    return {
      Status,
      status: Status.wait,
      container: {
        file: null,
        hash: "",
        worker: null,
      },
      requestList: [],
      hashPercentage: 0,
      data: [], //保存切片文件数组
      fakeUploadPercentage: 0,
    };
  },
  watch: {
    uploadPercentage(now) {
      if (now > this.fakeUploadPercentage) {
        this.fakeUploadPercentage = now;
      }
    },
  },
  methods: {
    async handleDelete() {
      const { data } = await axios.request({
        url: "http://localhost:401/delete",
        method: "post",
      });
      if (data.code === 0) {
        this.$message.success("delete success");
      }
    },
    //生成文件hash
    calculateHash(fileChunkList) {
      return new Promise((resolve) => {
        this.container.worker = new Worker("/hash.js");
        this.container.worker.postMessage({ fileChunkList });
        this.container.worker.onmessage = (e) => {
          const { percentage, hash } = e.data;
          this.hashPercentage = percentage;
          if (hash) {
            resolve(hash);
          }
        };
      });
    },
    parseFile(e) {
      source.cancel("终止上传");
      source = CancelToken.source();
      this.fakeUploadPercentage = 0; //重新选择文件的时候需要做一些必要的初始化
      this.data = [];
      this.requestList = [];
      this.container.file = e.target.files[0];
    },
    //上传文件前先判断服务端是否已经存在上传资源,如果已经有了则直接返回上传成功的信息
    async verifyUpload(filename, fileHash) {
      const { data } = await axios.request({
        url: "http://localhost:401/verify",
        method: "post",
        headers: {
          "Content-Type": "application/json",
        },
        data: JSON.stringify({ filename, fileHash }),
      });
      return data; //不需要使用JSON.parse(data),axios内部已经做了转换
    },
    createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    },
    async uploadChunks(uploadedList = []) {
      const requestList = this.data
        .filter(({ hash }) => !uploadedList.includes(hash))
        .map(({ chunk, hash, index }) => {
          const formData = new FormData();
          formData.append("chunk", chunk);
          formData.append("hash", hash);
          formData.append("fileHash", this.container.hash);
          formData.append("filename", this.container.file.name);
          return { formData, index }; //一定要加括号
        })
        .map(({ formData, index }) =>
          axios
            .request({
              method: "post",
              url: "http://localhost:401",
              header: { "Content-Type": "application/x-www-form-urlencoded" },
              onUploadProgress: this.uploadProgressEvent(this.data[index]),
              cancelToken: source.token,
              data: formData,
            })
            .then(() => {
              this.requestList.push(requestList[index]);
            })
            .catch(() => {})
        );
      await Promise.all(requestList);
      console.log(uploadedList, this.requestList, this.data);
      if (uploadedList.length + this.requestList.length >= this.data.length) {
        await this.mergeRequest();
        this.status = Status.wait;
      }
    },
    async mergeRequest() {
      axios
        .request({
          url: "http://localhost:401/merge",
          method: "post",
          headers: {
            "Content-Type": "application/json",
          },
          data: JSON.stringify({
            filename: this.container.file.name,
            size: SIZE,
            fileHash: this.container.hash,
          }),
        })
        .then(() => {
          this.container = {
            file: null,
            hash: "",
            worker: null,
          };
          this.status = Status.wait;
        });
    },
    async upFileParse() {
      if (!this.container.file) return;
      this.status = Status.uploading;
      const fileChunkList = this.createFileChunk(this.container.file);
      this.container.hash = await this.calculateHash(fileChunkList);
      const { shouldUpload, uploadedList } = await this.verifyUpload(
        this.container.file.name,
        this.container.hash
      );
      if (!shouldUpload) {
        this.fakeUploadPercentage = 100;
        this.status = Status.wait;
        this.container.file = null;
        return;
      }
      this.data = fileChunkList.map(({ file }, index) => ({
        chunk: file,
        fileHash: this.container.hash,
        index,
        hash: this.container.file.name + "-" + index,
        size: file.size,
        percentage: uploadedList.includes(index) ? 100 : 0,
      }));
      await this.uploadChunks(uploadedList);
    },
    uploadProgressEvent(item) {
      return (e) => {
        item.percentage = parseInt(String((e.loaded / e.total) * 100));
      };
    },
    //暂停上传
    handlePause() {
      this.status = Status.pause;
      this.requestList = [];
      console.log("暂停上传了");
      source.cancel("暂停上传了");
      source = CancelToken.source(); //重置source,确保能够续传
    },
    async handleResume() {
      this.status = Status.uploading;
      const { uploadedList } = await this.verifyUpload(
        this.container.file.name,
        this.container.hash
      );
      await this.uploadChunks(uploadedList);
    },
  },
};
</script>

<style></style>

后端

const http = require("http");
const multiparty = require("multiparty");
const path = require("path");
const fse = require("fs-extra");
const bodyParser = require("body-parser");
const server = http.createServer();
const UPLOAD_DIR = path.resolve(__dirname, ".", "qiepian2");

const extractExt = (filename) =>
  filename.slice(filename.lastIndexOf("."), filename.length); //提取文件名

// 创建临时文件夹用于临时存储 chunk
// 添加 chunkDir 前缀与文件名做区分
// create a directory for temporary storage of chunks
// add the 'chunkDir' prefix to distinguish it from the chunk name
const getChunkDir = (fileHash) =>
  path.resolve(UPLOAD_DIR, `chunkDir_${fileHash}`);
//返回已经上传的所有切片名
const createUploadedList = async (fileHash) =>
  fse.existsSync(getChunkDir(fileHash))
    ? await fse.readdir(getChunkDir(fileHash))
    : [];

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


const resolvePost = (req) => {
	return new Promise((resolve) => {
	  let chunk = "";
	  req.on("data", (data) => {
		chunk += data;
	  });
	  req.on("end", () => {
		resolve(JSON.parse(chunk));
	  });
	});
  };
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;
  }
  if (req.url === "/") {
    const multipart = new multiparty.Form();
    multipart.parse(req, async (err, fields, files) => {
      if (err) {
        console.log(err);
        return;
      }
      const [chunk] = files.chunk;
      const [hash] = fields.hash;
      const [fileHash] = fields.fileHash;
      const [filename] = fields.filename;
      const filePath = path.resolve(
        UPLOAD_DIR,
        `${fileHash}${extractExt(filename)}`
      );
      //   const chunkDir = path.resolve(UPLOAD_DIR, "chunkDir" + filename);
      const chunkDir = getChunkDir(fileHash);
      const chunkPath = path.resolve(chunkDir, hash);
      // 文件存在直接返回
      // return if file is exists
      if (fse.existsSync(filePath)) {
        res.end("file exist");
        return;
      }
      // 切片存在直接返回
      // return if chunk is exists
      if (fse.existsSync(chunkPath)) {
        res.end("chunk exist");
        return;
      }

      // 切片目录不存在,创建切片目录
      if (!fse.existsSync(chunkDir)) {
        await fse.mkdirs(chunkDir);
      }

      await fse.move(chunk.path, path.resolve(chunkDir,hash));
      res.end(
        JSON.stringify({
          code: 0,
          message: "切片上传成功",
        })
      );
    });
  }
});


const mergeFileChunk = async (filePath, fileHash, size) => {
  //此时改成了根据fileHash而不是filename
  //   const chunkDir = path.resolve(UPLOAD_DIR, "chunkDir" + filename);
  const chunkDir = getChunkDir(fileHash);
  // 根据切片下标进行排序
  // 否则直接读取目录的获得的顺序会错乱
  const chunkPaths = await fse.readdir(chunkDir);
  chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
  await Promise.all(
    chunkPaths.map((chunkPath, index) =>
      pipeStream(
        path.resolve(chunkDir, chunkPath),
        fse.createWriteStream(filePath, { start: index * size })
      )
    )
  );
  fse.rmdirSync(chunkDir);
};

server.on("request", async (req, res) => {
  if (req.method === "OPTIONS") {
    (res.status = 200), res.end();
    return;
  }
  if (req.url === "/merge") {
    const data = await resolvePost(req);
    const { fileHash, filename, size } = data;
    const ext = extractExt(filename);
    // const filePath = path.resolve(UPLOAD_DIR, `${filename}`);
    const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
    await mergeFileChunk(filePath, fileHash, size);
    res.end(
      JSON.stringify({
        code: 0,
        mess: "success",
      })
    );
  }
});

server.on("request", async (req, res) => {
  if (req.method === "OPTIONS") {
    (res.status = 200), res.end();
    return;
  }
  if (req.url === "/verify") {
    const data = await resolvePost(req);
    const { fileHash, filename } = data;
    const ext = extractExt(filename);
    const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
    if (fse.existsSync(filePath)) {
      res.end(JSON.stringify({ shouldUpload: false }));
    } else {
      res.end(
        JSON.stringify({
          shouldUpload: true,
          uploadedList: await createUploadedList(fileHash),
        })
      );
    }
  }
  if(req.url==='/delete'){//整个切片文件夹全部删除
	await fse.remove(path.resolve(UPLOAD_DIR))
	res.end(
		JSON.stringify({
			code:0,
			message:'删除所有文件成功'
		})
	)
  }
});








server.listen(401, () => {
  console.log("项目启动test");
});

在这里插入图片描述
后面可以结合该前端文件封装文件上传组件
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一夕ξ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值