前言
后后需要支持excel上传内容,格式如下:
由于我司多媒体文件在七牛保存,若后端上传数据,则要先保存到后端服务器,存储消耗不容忽视;而且上传数据可能会失败,然后涉及记录失败条目、失败重传、当前进度等
开发周期较长,因此小组讨论后决定采用比较简单的策略:
- 按 excel 行顺序上传,提示当前正在上传的行号
- 单选上传多媒体资源完成后,将内容保存到数据库
- 记录失败的行,上传结束后给出提示,用户自行重传失败的条目
然而理想总是丰满的,现实总是骨感的,实现时才发现不是看上去那么容易。
主要问题:
- 我们要等单个 row 里的资源全部上传到七牛后,再将数据 post 到后台,而 js 异步执行,无法保证按excel行顺序上传
- 用户选择的资源可能并不是真正的多媒体文件,使用 js 的 file.type 方法获取的文件类型可能不准确,比如将 .xlsx 改为 .jpg, file.type 得到的类型是image/jpeg
- 用户在表里填写了文件名,实际上传时可能漏传一些文件,这时候即便成功,也是错误的数据
所谓兵来将挡水来土掩,对以上问题,摸索出解决方案如下:
- 使用 Promise 模型,多层嵌套
- 根据文件后缀和二进制头,双重判断文件类型
- 用户选择文件后即进行校验,缺少文件则无法上传
Promise 模型
js 的Promise支持链式调用,因此单个 row 的资源文件,调用 Promise.all() 全部上传到七牛后,再将该行内容发送给后端,然后进行下一步。
发送数据给后端
例如发送数据给后端可以这样实现
// 声明一个返回 promise 的函数
function sendToDB(data){
return new Promise((resolve, reject) => {
createArticleApi(data)
.then(resp=>{
resolve()
})
.catch(err => {
reject()
})
})
}
// 执行
arr.reduce(
(promise, data) => {
return promise.then(() => {
sendToDB(data)
})
}, Promise.resolve()
)
.then(data => {})
.catch(err => {})
然后上传的时候调用链
promise.resolve(row1).then(row2).then(row3).then(row4)...
但实际上传时,由于Promise链上的任一reject会触发catch异常,而发送给后端可能返回失败,导致excel未全部上传完毕就提前退出,因此每个Promise要单独执行,出错后能控制继续执行 or 终止
这里要用到js 语法 (function f(){})() 声明并立即调用函数,函数执行完后返回的promise决定是否继续
let idx = 1
// 直接调用第一个promise, 启动
let p = new Promise(resolve => {
resolve(sendToDB(arr[0]))
})
// 若写入数据库出错,返回resolve继续执行
while(idx <= arr.length){
(function(idx){
p = p.then(() => {
sendToDb(arr[idx])
.then(()=>{})
.catch(()=>{})
if(idx === arr.length){
return
}
return arr[idx]
}).catch(() => {
if(idx === arr.length){
return
}
// 忽略错误,继续执行
return Promise.resolve()
})
})(idx)
idx += 1
}
上传的时候执行类似
promise.resolve(row1.then(resolve))
.then(row2.catch(resolve))
.then(row3.catch(resolve))
.then(row4.then(resolve))...
上传文件到七牛
而前面封装好的七牛接口,返回的也是promise对象,而我要在调用时才拿返回结果,所以需要一个返回七牛上传结果的函数
js 的闭包可实现这个功能,调用函数后返回一个返回promise的函数
闭包
// 七牛上传接口为异步
// promise 组确保每一行的资源上传完成后,才开始下一步执行
function promiseFunc(line){
return function(){
return new Promise((resolve, reject) => {
// 当前上传的行号
_this.uploadingLineNo = line.lineno
let imgsPromises = line.imgs.map(name => {
return _this.uploadSingleFile(name, 'image')
})
let mediasPromises = line.medias.map(name => {
return _this.uploadSingleFile(name, 'video')
})
Promise.all([Promise.all(imgsPromises), Promise.all(mediasPromises)])
.then(data=>{
line.imgUrls = data[0]
line.mediaUrls = data[1]
resolve(line)
})
.catch(err => {
// 行号和对应的上传结果
_this.resourceFileResult.push({
lineno: line.lineno,
error: err || '包含不允许的文件类型'
})
line.imgUrls = []
line.mediaUrls = []
reject()
})
})
}
}
然后将excel内容映射成上传函数,推入数组,接下来使用下标调用函数
核心代码
// promise 队列,按 excel 顺序上传内容
let asyncArr = _this.fileContentList.map(promiseFunc)
let idx = 1
// 直接调用第一个promise, 启动
let p = new Promise(resolve => {
resolve(asyncArr[0]())
})
// 若上传七牛或 写入数据库出错,返回resolve继续执行
while(idx <= asyncArr.length){
(function(idx){
p = p.then(data => {
_this.sendToDB(data)
.then(()=>{})
.catch(()=>{})
if(idx === asyncArr.length){
return
}
return asyncArr[idx]()
}).catch(() => {
if(idx === asyncArr.length){
return
}
// 忽略上传七牛错误,继续执行
return Promise.resolve()
})
})(idx)
idx += 1
}