原创不易,注释都在代码中,点赞收藏不迷路~
1.安装依赖
npm i -S spark-md5
2.对接 api(需后端接口支持)
/* * @Description: 大文件上传接口 * @Author: zhangy * @Date: 2022-05-16 12:47:41 * @LastEditors: zhangy * @LastEditTime: 2022-05-17 16:28:01 */import request from '@/utils/request'// 校验export function getUploadStatus(data) { return request({ url: 'api/file/part/check', method: 'get', params: data })}// 上传export function sliceUpload(data) { return request({ url: 'api/file/part/upload', method: 'post', data })}// 合并export function mergeUpload(data) { return request({ url: 'api/file/part/merge', method: 'get', params: data })}export default { getUploadStatus, sliceUpload, mergeUpload }
3.封装切片上传的 mixin
/* * @Description: 大文件上传、分片上传、断点续传、文件秒传 * @Author: zhangy * @Date: 2022-05-16 13:10:13 * @LastEditors: zhangy * @LastEditTime: 2022-05-19 10:14:33 */const SparkMD5 = require('spark-md5')import { getUploadStatus, sliceUpload, mergeUpload } from '@/api/chunksUploadAPI'// 切片大小(单位:B)const CHUNK_SIZE = 5 * 1024 * 1024/** * @description: 分块计算文件的md5值 * @param {*} file 文件 * @param {*} chunkSize 分片大小 * @returns {*} */function calculateFileMd5(file, chunkSize) { return new Promise((resolve, reject) => { const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice const chunks = Math.ceil(file.size / chunkSize) let currentChunk = 0 const spark = new SparkMD5.ArrayBuffer() const fileReader = new FileReader() fileReader.onload = function(e) { spark.append(e.target.result) currentChunk++ if (currentChunk < chunks) { loadNext() } else { const md5 = spark.end() resolve(md5) } } fileReader.onerror = function(e) { reject(e) } function loadNext() { const start = currentChunk * chunkSize let end = start + chunkSize if (end > file.size) { end = file.size } fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)) } loadNext() })}/** * @description: 分块计算文件的md5值 * @param {*} file 文件 * @returns {Promise} */function calculateFileMd5ByDefaultChunkSize(file) { return calculateFileMd5(file, CHUNK_SIZE)}/** * @description: 文件切片 * @param {*} file * @param {*} size 切片大小 * @returns [{file}] */function createFileChunk(file, size = CHUNK_SIZE) { const chunks = [] let cur = 0 while (cur < file.size) { chunks.push({ file: file.slice(cur, cur + size) }) cur += size } return chunks}/** * @description: 获取文件的后缀名 */function getFileType(fileName) { return fileName.substr(fileName.lastIndexOf('.') + 1).toLowerCase()}/** * @description: 根据文件的md5值判断文件是否已经上传过了 * @param {*} md5 文件的md5 * @param {*} 准备上传的文件 * @returns {Promise} */function checkMd5(md5, file) { return new Promise(resolve => { getUploadStatus({ md5 }) .then(res => { if (res.data.code === 20000) { // 文件已经存在了,秒传(后端直接返回已上传的文件) resolve({ uploaded: true, url: res.data.msg, code: res.data.code }) } else if (res.data.code === 40004) { // 文件不存在需要上传 resolve({ uploaded: false, url: '', code: res.data.code }) } else { resolve({ uploaded: false, url: '', code: 500 }) } }) .catch(() => { resolve({ uploaded: false, url: '', code: 500 }) }) })}/** * @description: 执行分片上传 * @param {*} file 上传的文件 * @param {*} i 第几分片,从0开始 * @param {*} md5 文件的md5值 * @param {*} vm 虚拟 dom 指向组件 this * @returns {Promise} */function PostFile(file, i, md5, vm) { const name = file.name // 文件名 const size = file.size // 总大小 const shardCount = Math.ceil(size / CHUNK_SIZE) // 总片数 if (i >= shardCount) { return } const start = i * CHUNK_SIZE const end = start + CHUNK_SIZE const packet = file.slice(start, end) // 将文件进行切片 /* 构建form表单进行提交 */ const form = new FormData() form.append('md5', md5) // 前端生成uuid作为标识符传个后台每个文件都是一个uuid防止文件串了 form.append('file', packet) // slice方法用于切出文件的一部分 form.append('name', name) form.append('totalSize', size) form.append('total', shardCount) // 总片数 form.append('index', i + 1) // 当前是第几片 return new Promise((resolve, reject) => { sliceUpload(form) .then(res => { if (res.data.code === 20001) { // 拿到已上传过的切片 resolve({ uploadedList: res.data.chunkList ? res.data.chunkList.map(item => `${md5}-${item}`) : [] }) } else if (res.data.code === 20002) { resolve({ uploadedList: [] }) } else { resolve({ uploadedList: [], code: 500 }) // reject() } }) .catch(() => { // reject() resolve({ uploadedList: [], code: 500 }) }) })}/** * @description: 合并文件 * @param {*} shardCount 分片数 * @param {*} fileName 文件名 * @param {*} md5 文件md值 * @param {*} fileType 文件类型 * @param {*} fileSize 文件大小 * @returns {Promise} */function merge(shardCount, fileName, md5, fileType, fileSize) { return mergeUpload({ shardCount, fileName, md5, fileType, fileSize })}export default { data() { return { chunks: [], percent: 0, percentCount: 0 } }, methods: { /** * @description: 上传文件 * @param {*} file 文件 * @returns {Object} 包含成功的文件地址、名称等 */ async chunksUpload(file) { this.chunks = [] // step1 获取文件切片 const initChunks = createFileChunk(file) // step2 获取文件 md5 值 const md5 = await calculateFileMd5ByDefaultChunkSize(file) // step3 获取文件的上传状态 const { uploaded, url, code } = await checkMd5(md5, file) if (uploaded) { // step4 如果上传成功 this.percent = 100 // step5 拿到结果 return url } if (!uploaded && code === 500) { return this.errorInfo() } // step4 如果文件未传成功,执行切片上传 const { uploadedList } = await PostFile(file, 0, md5, this) // todo 方法1:逐次发送请求 const requestList = [] // 请求集合 initChunks.forEach(async(chunk, index) => { // 过滤掉已上传的切片 if (uploadedList.indexOf(md5 + '-' + (index + 1)) < 0) { const fn = () => { return PostFile(file, index, md5, this) } requestList.push(fn) } }) let reqNum = 0 // 记录发送的请求个数 const send = async() => { if (reqNum >= requestList.length) { // step5 如果所有切片已上传,执行合并 const res = await merge(initChunks.length, file.name, md5, getFileType(file.name), file.size) if (res.data.code === 20000) { return res.data.msg } else { this.errorInfo() return {} } } const sliceRes = await requestList[reqNum]() if (sliceRes.code && sliceRes.code === 500) { return this.errorInfo() } // 计算当下所上传切片数 const count = initChunks.length - uploadedList.length if (this.percentCount === 0) { this.percentCount = 100 / count } this.percent += this.percentCount reqNum++ return send() } const mergeResult = await send() return mergeResult // todo 方法2:使用Promise.all 统一发送请求 // const requestList = initChunks.map(async(chunk, index) => { // // 过滤掉已上传的切片 // if (uploadedList.indexOf(md5 + '-' + (index + 1)) < 0) { // return PostFile(file, index, md5, this) // } // }) // return Promise.all(requestList) // .then(async() => { // const res = await merge(initChunks.length, file.name, md5, getFileType(file.name), file.size) // if (res.data.code === 20000) { // return res.data.msg // } // }) // .catch(() => { // return this.$message.error('出错了,请稍后重试!') // }) }, /** * @description: 错误提示 */ errorInfo() { this.$message.error('出错了,请稍后重试!') } }}
4.使用
<template><div id="app"> <el-upload ref="vedioUpload" action drag :limit="1" accept=".mp4" :auto-upload="false" :show-file-list="false" :on-change="onFileChange" > <i class="el-icon-upload" /> <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> </el-upload> <div>上传进度: {{ percent.toFixed()+ '%' }}</div> <div v-if="videoUrl" class="video-box"> <video :src="videoUrl" controls /> </div> </div></template><script> import chunksUpload from '@/mixins/chunks-upload.js' export default { mixins: [chunksUpload], data() { return { videoUrl: '' } } methods: { async onFileChange(file) { this.clearVideoUpload() const fileType = file.raw.type const fileSize = file.raw.size / 1024 / 1024 if (fileType !== 'video/mp4') return this.$message.error('只能上传 MP4 格式的视频!') if (fileSize > 1000) return this.$message.error('视频大小不能超过 1G!') const res = await this.chunksUpload(file.raw) console.log('上传结果', res) }, clearVideoUpload() { this.videoUrl = '' this.percent = 0 } } }</script><style lang="scss" scoped>.video-box { width: 400px; height: 250px; video { width: 100%; height: 100%; }}</style>原文:https://juejin.cn/post/7099362510532247589