vue3 upload文件上传及断电续传、含进度条,含node服务代码块。

最近一直想写个大型文件续传的逻辑代码块,今天就把它贡献出来吧。代码不做讲解,有兴趣的自己体会。

path: src\pages\tempContent\displayCounter\index.vue

<!--
 * Copyright ©
 * #  
 * @author: zw
 * @date: 2021-12-07 
 -->

<template>
  <input type="file" ref="uploadRef" @change="total = 0" />
  <el-button type="primary" @click="uploadFile" :disabled="total > 0 ? true : false" :loading="computedFileSize" >
    上传
  </el-button>
  <el-button @click="pauseBykeep">{{ loading ? "继续" : "暂停" }}</el-button>
  <div class="">
    <div class="divSpan">
      {{ computedFileSize ? "计算文件大小" : `上传进度:${total}%` }}
    </div>
    <el-progress :percentage="total" :status="total < 100 ? 'exception' : 'success'" />
  </div>
</template>

<script lang="js">
import { defineComponent, reactive, toRefs } from 'vue'
import axios from "axios";
import SparkMD5 from "spark-md5";
import { useInstance } from "@/common";
import { SIZE, fileParse, chunkList, formData } from "./utils";
export default defineComponent({
  name: 'displayCounter',
  setup(props, {emit, slots}) {
    const { $message: message } = useInstance();
    const state = reactive({
      uploadRef: null,
      total: 0,
      computedFileSize: false,
      loading: false,
      partList: [],
      hash: '',
      isErr: false,
      abort: false
    });
    const uploadFile = () => {
      state.computedFileSize = true;
      createChunkFile(state.uploadRef.files[0]);
    }
    //创建切片
    const createChunkFile = async (file) => {
      if (!file) return;

      // 解析为BUFFER数据
      const buffer = await fileParse(file);
      const spark = new SparkMD5.ArrayBuffer();
      spark.append(buffer);
      const hash = spark.end();
      
      // 把一个文件分割成为好几个部分(固定数量/固定大小),每一个切片有自己的部分数据和自己的名字
      state.partList = chunkList(hash, file);
      state.hash = hash;
      getLoadingFiles(hash);
    };
    //检查当前文件是否曾经上传
    const getLoadingFiles = async (hash) => {
      const { data: { data, code }} = await axios({url: "http://localhost:8888/loadingUpload", method: 'GET', params: { hash }});
      if (code !== 0) return;

      if (data.length === 0) {
        uploadFn();
      } else {
        const progress = parseInt(Math.ceil(100 / state.partList.length));
        state.total = data.length * progress;
        uploadFn(data);
      }
    };
    //根据切片数创造切片数个请求
    const uploadFn = async (list) => {
      function request(formData) {
        return axios.post("http://localhost:8888/upload", formData, { headers: { "Content-Type": "multipart/form-data" } });
      }
      const uploadList = state.partList.map(item => formData(item, request));
      state.computedFileSize = false;
      const index = list ? list.length : 0;
      uploadSend(uploadList, index);
    };
    //点击暂停或者继续的函数
    const pauseBykeep = () => {
      if (!state.uploadRef.files[0]) return;
      if (state.loading) { //继续上传
        state.loading = false;
        state.abort = false;
        uploadFn();
      } else {//暂停上传
        state.loading = true;
        state.abort = true;
      }
    };
    //上传单个切片
    const uploadSend = async (uploadList, index) => {
      if (state.abort) return;// 已经中断则不再上传
      if (index >= uploadList.length) return uploadComplete();// 都传完了
      try {
        const progress = parseInt(Math.ceil(100 / state.partList.length));
        const { data: { data, code } } = await uploadList[index]();
        if (code === 2) state.isErr = true;
        if (code === 0) {
          const status = state.total + progress;
          state.partList.shift(1);
          state.total = status >= 100 ? 100 : status;
        }
      } catch (error) {
        state.loading = true;
        state.abort = true;
      }
      if (state.isErr) {
        return message.error("当前文件已上传");
      }
      index++;
      uploadSend(uploadList, index);
    };
    //最后一个切片上传完成,合并切片
    const uploadComplete = async () => {
      const {data: { data, code }} = await axios({url: "http://localhost:8888/merge", method: 'GET', params: { hash: state.hash } });
      if (code !== 0) return;
      message.success("上传成功");
    };
   return {
     uploadFile,
     pauseBykeep,
     ...toRefs(state)
    }
   }
});
</script>

<style lang="css" scoped>
.el-progress {
  width: 400px;
}
</style>

utils的代码提取块。

path: src\pages\tempContent\displayCounter\utils.js

 // 切片大小
export const SIZE = 10 * 1024 * 1024;
//转换文件类型(解析为BUFFER数据)
export function fileParse(file){
  return new Promise(resolve => {
    const fileRead = new FileReader();
    fileRead.readAsArrayBuffer(file);
    fileRead.onload = (evt) => resolve(evt.target.result);
  });
}
// 获取后缀名
export function suffixName(name) {
 return /\.([0-9a-zA-Z]+)$/i.exec(name)[1];
}
// 文件分割
export function chunkList(hash, file) {
  const partList = [];
  const count = Math.ceil(file.size / SIZE);
  const partSize = file.size / count;
  const suffix = suffixName(file.name);
  let i = 0, cur = 0;
  while (i < count) {
    const item = { chunk: file.slice(cur, cur + partSize), filename: `${hash}_${i}.${suffix}` };
    cur += partSize;
    partList.push(item);
    i++;
  }
  return partList;
}
/**
 * 
 * @param {*} item 逐个chunk
 * @param {*} callbeck 接收一个函数
 * @returns 外部传入的函数方法
 */
export function formData(item, callbeck) {
  const formData = new FormData();
  formData.append("chunk", item.chunk);
  formData.append("filename", item.filename);
  return callbeck.bind(null, formData);
}

common代码提取块。

path: src\common\index.js

import { getCurrentInstance } from 'vue'
function useInstance (target) {
  const Instance = getCurrentInstance();
  const Properties = Instance.appContext.config.globalProperties;
  return target ? Properties[target] : Properties
}

function create_uuid () {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
    return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16)
  })
}

function loop(m, c) {
  return m.find(e => e.id === c ? e : loop(e.children, c));
}

export {
  useInstance,
  create_uuid,
  loop,
}

node 服务模块的代码。文件用的模块自己按照习惯去安装配置就行。

path: server\server.js

const express = require("express");
const fs = require("fs");
const { join, resolve } = require("path");
const multiparty = require("multiparty");
const cors = require("cors");

const PORT = 8888;
const uploadDir = `${__dirname}/upload`;
const app = express();
app.use(express.static("./"));
app.use(cors());

app.listen(PORT, () => console.log(`服务端已启动,端口号:${PORT}`));
app.use(express.urlencoded({ extended: false, limit: "2048mb" }));

// 判断upload文件夹的存在
function isDirSync(currentPath) {
  try {
    return fs.statSync(currentPath).isDirectory();
  } catch (e) {
    if (e.code === 'ENOENT') return false;
    throw e;
  }
}
function handleMultiparty(req, res, temp) {
  return new Promise((resolve, reject) => {
    const options = { maxFieldsSize: 200 * 1024 * 1024 };// multiparty的配置
    !temp ? (options.uploadDir = uploadDir) : null;
    const form = new multiparty.Form(options);
    form.parse(req, (err, fields, files) => {// multiparty解析
      if (err) return reject(err);
      resolve({ fields, files });
    });
  });
}

//查找已经上传的文件
function findSync(startPath) {
  const result = [];

  function finder(_path) {
    const files = fs.readdirSync(_path);
    files.forEach(item => {
      const _findPath = join(_path, item);
      const stats = fs.statSync(_findPath);
      if (stats.isFile()) result.push(item.split(".")[0]);
    });
  }

  finder(startPath);
  return result;
}

//查找上传过程中的文件
function findLoadingSync(startPath, hash) {
  const result = [];

  function finder(_path, isChild) {
    if (!isDirSync(join(__dirname, 'upload'))) {
      fs.mkdirSync(join(__dirname, 'upload'));
    }
    const _files = fs.readdirSync(_path);
    _files.forEach(item => {
      const _findPath = join(_path, item);
      const stats = fs.statSync(_findPath);
      if (stats.isDirectory() && hash == item) finder(_findPath, item);
      if (stats.isFile() && isChild == hash) result.push(item);
    });
  }

  finder(startPath);
  return result;
}

//判断当前文件是否曾经上传,如果上传且没有上传完成返回已经上传的文件切片名称
app.get("/loadingUpload", (req, res) => {
  const { hash } = req.query;
  const _hasUpList = findLoadingSync(uploadDir, hash);
  res.send({ code: 0, data: _hasUpList });
});

app.post("/upload", async (req, res) => {
  const { fields, files } = await handleMultiparty(req, res, true);

  const [chunk] = files.chunk;
  const [filename] = fields.filename;
  const _hash = /([0-9a-zA-Z]+)_\d+/.exec(filename)[1];
  let _path = `${uploadDir}/${_hash}`;
  const _hasUpList = findSync(uploadDir);
  if (_hasUpList.indexOf(_hash) > -1) {
    return res.send({ code: 2, msg: "当前文件已经上传!" });
  }
  !fs.existsSync(_path) ? fs.mkdirSync(_path) : null;
  _path = `${_path}/${filename}`;
  fs.access(_path, async (err) => { // 存在的则不再进行任何的处理
  if (!err) {
    return res.send({ code: 0, path: _path.replace(__dirname, `http://127.0.0.1:${PORT}`) });
  };

    // 不存在的再创建
    const readStream = fs.createReadStream(chunk.path);
    const writeStream = fs.createWriteStream(_path);
    readStream.pipe(writeStream);
    readStream.on("end", () => {
      fs.unlinkSync(chunk.path);
      res.send({ code: 0, path: _path.replace(__dirname, `http://127.0.0.1:${PORT}`) });
    });
  });
});

app.get("/merge", (req, res) => {
  const { hash } = req.query;
  const _path = `${uploadDir}/${hash}`;
  const fileList = fs.readdirSync(_path);
  let suffix;

  fileList.sort((a, b) => /_(\d+)/.exec(a)[1] - /_(\d+)/.exec(b)[1]);
  fileList.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);
  res.send({ code: 0, path: `http://127.0.0.1:${PORT}/upload/${hash}.${suffix}` });
});

app.use((req, res) => {
  res.status(404);
  res.send("NOT FOUND!");
});

vue2版本 el-upload

<!--
 * Copyright ©
 * #  
 * @author: zw
 * @date: 2022-09-13 
 -->


<template>
  <el-row>
    <el-col>
      <el-form class="ml-10" :model="query" inline="inline" label-position="left" size="small">

        <el-col :span="1.5" class="fr">
          <el-form-item class="mb-10 mt-10">
            <el-button type="success" icon="el-icon-upload" @click="upload.visible = true">文件上传</el-button>
          </el-form-item>
        </el-col>

      </el-form>
    </el-col>

    <el-dialog title="上传升级包" :visible.sync="upload.visible" append-to-body width="35%">

      <el-upload align="center" ref="upload" :limit="1" :multiple="false" :accept="upload.accept" action="" name="file" :auto-upload="false" :before-remove="file => $confirm(`确定移除 ${file.name}?`)" :on-success="onSuccess" :on-error="onError" :http-request="customUpload" drag>

        <i class="el-icon-upload" />
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        <div class="el-upload__tip" slot="tip">仅支持上传升级包文件</div>
        <br>
        <br>
        <div class="el-upload__tip" style="color:blue" slot="tip">提示:仅允许导入<span style="color:red">{{upload.accept}}</span>格式文件!</div>
        <div class="el-upload__tip" slot="tip">
          <el-progress v-if="upload.total > 0" class="w--30%" :percentage="upload.total" :status="upload.total < 100 ? 'exception' : 'success'" />
        </div>
      </el-upload>

      <div slot="footer">
        <el-button type="primary" @click="$refs.upload.submit()" size="mini">确 定</el-button>
        <el-button @click="upload.visible = false" size="mini">取 消</el-button>
      </div>

    </el-dialog>
  </el-row>
</template>

<script>
import axios from "axios";
import SparkMD5 from "spark-md5";
import { fileParse, chunkList, formData } from "@/utils";
const BASE_URL = "http://localhost:8888"
export default {
  name: 'upload',
  data() {
    return {
      query: {},
      upload: {
        visible: false,
        uploadCtx: null,
        total: 0,
        computedFileSize: false,
        loading: false,
        partList: [],
        hash: '',
        isErr: false,
        abort: false
      }
    };
  },

  mounted() { },

  methods: {
    onSuccess(response = {}) {
      this.$message.success(response.msg);
    },
    onError(response = {}) {
      this.$message.error(response.msg);
    },
    pauseBykeep() {
      if (!this.upload.uploadCtx.file) return;
      if (this.upload.loading) { //继续上传
        this.upload.loading = false;
        this.upload.abort = false;
        this.uploadFn();
      } else {//暂停上传
        this.upload.loading = true;
        this.upload.abort = true;
      }
    },
    customUpload(ctx) {
      console.log(ctx);
      this.upload.uploadCtx = ctx;
      this.upload.computedFileSize = true;
      this.createChunkFile(ctx.file);
    },
    async createChunkFile(file) {
      if (!file) return;

      // 解析为BUFFER数据
      const buffer = await fileParse(file);
      const spark = new SparkMD5.ArrayBuffer();
      spark.append(buffer);
      const hash = spark.end();

      // 把一个文件分割成为好几个部分(固定数量/固定大小),每一个切片有自己的部分数据和自己的名字
      this.upload.partList = chunkList(hash, file);
      this.upload.hash = hash;
      this.getLoadingFiles(hash);
    },
    async getLoadingFiles(hash) {
      const { data: { data, code } } = await axios({ url: `${BASE_URL}/loadingUpload`, method: 'GET', params: { hash } });
      if (code !== 0) return;
      if (data.length === 0) {
        this.uploadFn();
      } else {
        const progress = parseInt(Math.ceil(100 / this.upload.partList.length));
        this.upload.total = data.length * progress;
        this.uploadFn(data);
      }
    },
    uploadFn(list) {
      const request = (formData) => axios.post(`${BASE_URL}/upload`, formData, { headers: { "Content-Type": "multipart/form-data" } });
      const uploadList = this.upload.partList.map(item => formData(item, request));
      this.upload.computedFileSize = false;
      const index = list ? list.length : 0;
      this.uploadSend(uploadList, index);
    },
    async uploadSend(uploadList, index) {
      if (this.upload.abort) return;// 已经中断则不再上传
      if (index >= uploadList.length) return this.uploadComplete();// 都传完了
      try {
        const progress = parseInt(Math.ceil(100 / this.upload.partList.length));
        const { data: { code } } = await uploadList[index]();
        if (code === 2) this.upload.isErr = true;
        if (code === 0) {
          const status = this.upload.total + progress;
          this.upload.partList.shift(1);
          this.upload.total = status >= 100 ? 100 : status;
        }
      } catch (error) {
        console.error(error);
        this.upload.loading = true;
        this.upload.abort = true;
        this.$refs.upload.onError({ msg: error.message });
      }
      if (this.upload.isErr) {
        return this.$refs.upload.onError({ msg: "当前文件已上传" });
      }
      index++;
      this.uploadSend(uploadList, index);
    },
    async uploadComplete() {
      const { data: { code } } = await axios({ url: `${BASE_URL}/merge`, method: 'GET', params: { hash: this.upload.hash } });
      if (code !== 0) return;
      this.$refs.upload.onSuccess({ msg: "上传成功" });
    }
  }
  //  End
}

</script>

<style lang='css' scoped>
</style>

你可能感兴趣的:(vue,Element,javaScript,vue.js,前端)