前言
后后需要支持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
}