之前写过一篇文章Vue+element-ui实现大文件分片上传,使用element-upload的
http-request这个参数去覆盖默认上传行为,达到大文件分片上传的效果,之前可能写的有点乱,并且那种方法有一些缺陷
今天重新改造了一下,出了这篇组件改造,下面开始正文。
PS:我写这篇文章是2020年10月23日,element-ui的版本是2.13.2。
源码在node_modules/element-ui/packages/upload中,把整个文件夹复制下来,放到你项目的components目录中,看下目录结构,我们主要改动到的是1,2,5三个文件
其中 6注册组件 的方法是我们引用element-ui时,给Vue.use(ElementUi)时使用的,我们改造组件的时候用不到,直接删了,顺便把src文件夹下的5个文件都移动到upload文件夹,然后删除src文件夹。
操作之后,你得到的应该如下图的5个文件。
我们直接把组件拷过来了,组件中有一些对element-ui的依赖,先删除掉。
1.index.vue文件,删除红框中的代码
2.upload-list.vue文件,删除红框中的代码,把{{ t(‘el.upload.deleteTip’) }}改成‘按Delete删除’,locale和t()是多国语言,我们直接写成中文就好了,因为我们已经全局引入了element-ui了,所以ElProgress进度条这个组件在全局就已经存在了,所以也可以删掉。
既然是改造成分片上传,组件就要新增下面2个参数
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
chunk-size | 每块切片的大小,单位 B,如 5MB 需要写成 1024*1024*5 | Number | 1024*1024*10 |
thread | 线程,允许同一时间上传的分片数量 | Number | 3 |
在index.vue中的props中添加
chunkSize: {
type: Number,
default: 10 * 1024 * 1024,
},
thread: {
type: Number,
default: 3,
},
并且将props中data的Object改成Function,
说明:改造前data是一个对象,直接传入需要的参数,改造之后,data是一个方法,每个分片上传都会调用这个方法,这个方法的返回值会作为当前分片上传时需要的参数,这么做的原因是:每个分片要携带的信息是不一样的,比如每个分片的hash,每个分片的下标等等。
参数 | 说明 | 类型 |
---|---|---|
data | 此方法要返回一个对象,这个对象内容就是每个分片携带的额外参数,参数option是一个对象,里面有chunkSize(当前分片的大小),chunkTotal(分片总数),chunkIndex(分片下标),chunkHash(分片hash),fileName(文件名),fileHash(文件hash),fileSize(文件大小)七个属性 | function(options) |
接着在render()方法中的uploadData的props中添加2个传给upload.vue的参数
接着修改upload.vue文件,在props中接收index.vue传进来的2个参数chunkSize和thread,同时把data的值Object改成Function
安装一下spark-md5,npm i spark-md5
,用来计算文件hash用的
然后在upload.vue第4行左右写一个计算文件hash的方法
import SparkMD5 from 'spark-md5'
function getFileMD5(file) {
return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer()
const fileReader = new FileReader();
fileReader.onload = (e) => {
spark.append(e.target.result)
resolve(spark.end())
}
fileReader.onerror = () => {
reject("")
}
fileReader.readAsArrayBuffer(file)
})
}
接着找到upload.vue文件的post()方法,在开始上传之前计算好文件hash等信息,传给ajax.js使用,红框部分就是新增的代码,用了await,记得在post前加上async。
接着修改ajax.js,把ajax.js中upload()方法替换成下面的代码,
这个方法修改前是单文件上传,逻辑是上传成功就调用option.onSuccess()回调,上传失败就调用onError()回调,上传过程中调用option.onProgress()更新上传进度条
现在改成分片上传,逻辑改成了,一个sendRequest()队列,并且通过thread来控制并发数,在全部分片上传完成之后才调用option.onSuccess()回调,只要有一个分片上传失败就调用option.onError(),并且修改了进度条option.onProgress()的计算方式
PS:这里有优化空间,队列某个分片上传失败,可以新增重试机制,重试2-3次失败之后再调用onError
export default function upload(option) {
if (typeof XMLHttpRequest === 'undefined') {
return;
}
const action = option.action // 文件上传上传路径
const percentage = [] // 文件上传进度的数组,单项就是一个分片的进度
//将Blob转为上传时需要的FormData格式
const formDataList = option.chunkList.map((item, index) => {
const formData = new FormData()
// 额外加入组件外外面传入的data数据
const md5 = option.chunkHashList[index]
const chunkOption = {
chunkSize: item.size,//当前分片大小
chunkTotal: Math.ceil(option.file.size / option.chunkSize),// 所有切片数量
chunkIndex: index,// 当前切片下标
chunkHash: md5,// 当前切片hash
fileName: option.file.name,// 文件名
fileHash: option.fileHash,// 整个文件hash
fileSize: option.file.size, // 总文件大小
}
if (option.data) {
const data = option.data(chunkOption)
//data是个方法,遍历data的返回值,让formData携带data中的参数
if (data) {
Object.keys(data).forEach(key => {
formData.append(key, data[key])
})
}
}
formData.append(option.filename, item) // 文件的Blob
return formData
})
// 更新上传进度条的方法
const updataPercentage = (e) => {
let loaded = 0// 当前已经上传文件的总大小
percentage.forEach(item => {
loaded += item
})
e.percent = loaded / option.file.size * 100
option.onProgress(e)
}
const xhrList = [] // 所有的xhr请求
function sendRequest(formDataList, limit) {
let counter = 0 //上传成功的数量
let index = 0 //当前上传文件的下标
let isStop = false
const len = formDataList.length
const start = async () => {
if (isStop) {
return
}
const item = formDataList.shift()
if (item) {
const chunkIndex = index++
const xhr = new XMLHttpRequest()
// 分片上传失败回调
xhr.onerror = function error() {
isStop = true
option.onerror(getError(action, option, xhr))
}
// 分片上传成功回调
xhr.onload = function onload() {
if (xhr.status < 200 || xhr.status >= 300) {
isStop = true
option.onerror(getError(action, option, xhr))
}
// 最后一个上传完成
if (counter === len - 1) {
const result = xhrList.map(item => getBody(item))
option.onSuccess(result)
} else {
counter++
start()
}
}
// 上传中的时候更新进度条
if (xhr.upload) {
xhr.upload.onprogress = function progress(e) {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100
}
percentage[chunkIndex] = e.loaded
updataPercentage(e)
}
}
xhr.open('post', action, true)
if (option.withCredentials && 'withCredentials' in xhr) {
xhr.withCredentials = true
}
const headers = option.headers || {}
// 添加请求头
for (const item in headers) {
if (Object.prototype.hasOwnProperty.call(headers, item) && headers[item] !== null) {
xhr.setRequestHeader(item, headers[item])
}
}
// 文件开始上传,并把xhr对象存入xhrList中
xhr.send(item)
xhrList.push(xhr)
}
}
while (limit > 0) {
setTimeout(() => {
start()
}, Math.random() * 1000)
limit -= 1
}
}
try {
sendRequest(formDataList, option.thread)
return xhrList
} catch (error) {
option.onError(error)
}
}
改到这里就已经八九不离十了,还有最后一步,原先的ajax.js返回值是一个xhr,但是我们改造完之后返回值是一个xhr的数组集合,所以在取消上传的时候,要做一些修改,找到upload.vue文件的abort()方法,把里面2处reqs[uid].abort();改成红框部分的代码即可,到这里就大功告成了。
https://gitee.com/GaoWeiQiang1996/element-chunk-upload
on-success回调函数中的response参数也会有一些变化,以前是一个请求,现在有多少个分片就变成了有多少个请求的数组
其实可以添加一个开关来控制是否开启分片上传,后来想想,如果不用分片上传,把chunkSize设置成1024*1024*1024*1024,就和关闭分片上传没什么区别了,所以就没加。
其实还可以加秒传,断点续传等,在post()方法中我们就已经拿到文件hash和分片hash了,这时候去请求后端,拿回已经上传和文件hash,过滤掉,就能实现秒传断点续传了。