ElementUI?Upload源碼組件上傳流程解析
引言
很多時候我們都一直使用ElementUI的Upload上傳組件進行二次封裝, 但是否知道內(nèi)部是什么樣的一個上傳流程,事件在哪個時機觸發(fā),從獲取文件到上傳結(jié)束究竟經(jīng)歷什么樣的一個過程?希望通過分析該組件的核心邏輯 (不包括UI邏輯) 讓你在后續(xù)的開發(fā)中能夠快速定位問題所在
源文件
訪問packages/upload目錄可以看到如下內(nèi)容,其主要核心代碼在upload.vue和index.vue,單純一個文件一個文件看代碼理解雖然能看得懂,但是比較難把整個邏輯串通,所以我們從文件的獲取到上傳結(jié)束開始逐一分析
│ index.js
└─ src
ajax.js [默認上傳請求工具]
index.vue [管理FileList數(shù)據(jù),對外暴露操作文件列表的方法]
upload-dragger.vue [拖拽:對文件獲取的邏輯]
upload-list.vue [文件列表:純UI組件根據(jù)不同listType展示不同的樣式]
upload.vue [對單個文件上傳處理的過程],會涉及index.vue文件邏輯操作]
流程圖

1??. 獲取文件
??? upload.vue創(chuàng)建input組件同時設(shè)置display:none進行隱藏,只通過ref進行引用觸發(fā)$refs.input.click(),通過監(jiān)聽(13: Enter鍵) 32: (空格鍵)和點擊上傳容器觸發(fā)
handleClickhandleKeydown拖拽(只是在拖拽結(jié)束獲取文件觸發(fā)uploadFiles,具體邏輯在upload-dragger.vue比較簡單,所以不對其進行分析)
methods: {
handleClick() {
if (!this.disabled) {
// 處理選中文件之后,后續(xù)繼續(xù)選中重復(fù)文件無法觸發(fā)change問題
this.$refs.input.value = null;
this.$refs.input.click();
}
},
handleKeydown(e) {
if (e.target !== e.currentTarget) return;
if (e.keyCode === 13 || e.keyCode === 32) {
this.handleClick();
}
}
},
render(h) {
// ...
const data = {
on: {
click: handleClick,
keydown: handleKeydown
}
};
return (
<div {...data} tabindex="0" >
// ...
<input class="el-upload__input" type="file" ref="input" name={name} on-change={handleChange} multiple={multiple} accept={accept}></input>
</div>
);
}
2??. 文件個數(shù)校驗
在觸發(fā)input的handleChange后開始我們的校驗階段(uploadFiles方法):
?? 校驗文件最大個數(shù)
如果有設(shè)置limit個數(shù)限制時,判斷當(dāng)前選中的文件和已有的文件總和是否超出最大個數(shù),是的話則觸發(fā)onExceed事件同時退出
// ??? upload.vue uploadFiles
if (this.limit && this.fileList.length + files.length > this.limit) {
this.onExceed && this.onExceed(files, this.fileList);
return;
}
?? 非多文件情況處理
如果multiple未設(shè)置或者為false時,只獲取選中的第一個文件,如果沒有選中的文件則退出
// ??? upload.vue uploadFiles
let postFiles = Array.prototype.slice.call(files);
if (!this.multiple) { postFiles = postFiles.slice(0, 1); }
if (postFiles.length === 0) { return; }
3??. 構(gòu)造FileItem對象
onStart
handleStart
FileItem對象
| key | 描述 |
|---|---|
| status | 文件狀態(tài): ready, uploading, success, fail |
| name | 文件名稱 |
| size | 文件大小 |
| percentage | 上傳進度 |
| uid | 文件uid |
| raw | 原始文件對象 |
遍歷每一個選中的文件,根據(jù)我們需要的信息構(gòu)建我們所的文件對象同時放入fileList數(shù)組中,同時status狀態(tài)為ready準備上傳階段,判斷如果listType=picture-card | picture根據(jù)文件設(shè)置url: 生成blobURL進行回顯 (不需要等待上傳完成才能看到圖片內(nèi)容), 接著觸發(fā)onChange事件
// upload.vue (onStart) ---> index.vue (handleStart)
handleStart(rawFile) {
rawFile.uid = Date.now() + this.tempIndex++;
let file = {
status: 'ready',
name: rawFile.name,
size: rawFile.size,
percentage: 0,
uid: rawFile.uid,
raw: rawFile
};
if (this.listType === 'picture-card' || this.listType === 'picture') {
try {
file.url = URL.createObjectURL(rawFile);
} catch (err) {
console.error('[Element Error][Upload]', err);
return;
}
}
this.uploadFiles.push(file);
this.onChange(file, this.uploadFiles);
}
4??. 上傳階段
緊接著判斷auto-upload是否自動上傳
- true : 自動觸發(fā)
upload方法 - false: 通過外部手動觸發(fā)
$refs.upload.submit()手動觸發(fā)時通過過濾出status=ready準備上傳的文件遍歷觸發(fā)upload方法
// index.vue
submit() {
this.uploadFiles
.filter(file => file.status === 'ready')
.forEach(file => {
this.$refs['upload-inner'].upload(file.raw);
});
}
?? beforeUpload前置操作
根據(jù)外部是否傳入beforeUpload,可以對預(yù)備上傳的文件進行預(yù)處理或者校驗是否可以上傳:
- 不傳
beforeUpload直接觸發(fā)post方法上傳
// ??? upload.vue upload
if (!this.beforeUpload) {
return this.post(rawFile);
}
傳beforeUpload(同步和異步):
- 同步:傳入一個方法后返回false即終止上傳,其他非Promise類型結(jié)果的都通過上傳
- 異步:當(dāng)被reject則中斷上傳過程,當(dāng)
resolvef返回一個新的文件或者Blob類型數(shù)據(jù),根據(jù)原始的文件對象,重新構(gòu)建出新的文件對象進行上傳(resolve返回任意值都會觸發(fā)上傳)
// ??? upload.vue upload
const before = this.beforeUpload(rawFile);
// Promise
if (before && before.then) {
before.then(processedFile => {
const fileType = Object.prototype.toString.call(processedFile);
if (fileType === '[object File]' || fileType === '[object Blob]') {
if (fileType === '[object Blob]') {
processedFile = new File([processedFile], rawFile.name, {
type: rawFile.type
});
}
// 將原始值復(fù)制到新構(gòu)建的File對象中
for (const p in rawFile) {
if (rawFile.hasOwnProperty(p)) {
processedFile[p] = rawFile[p];
}
}
this.post(processedFile);
} else {
this.post(rawFile);
}
}, () => {
// 停止上傳并且從文件列表中刪除
this.onRemove(null, rawFile);
});
}
// 非false正常上傳
else if (before !== false) {
this.post(rawFile);
} else {
// 停止上傳并且從文件列表中刪除
this.onRemove(null, rawFile);
}
? beforeUpload中斷情況
onRemove
handleRemove
beforeRemove可選
doRemove
當(dāng)beforeUpload返回false或者reject時,會觸發(fā)onRemove方法(index.vue: handleRemove)
?? 這里觸發(fā)onRemove其實有個坑點,當(dāng)你beforeUpload被中斷時會觸發(fā)onRemove對文件進行刪除,如果你傳入了beforeRemove同時彈出確認框確認刪除操作,這會導(dǎo)致上傳中斷時顯示出來,這會讓用戶感覺到突兀,有個比較粗糙的方式就是在beforeRemove判斷status!=ready,即準備上傳的文件不需要走beforeRemove確認直接刪除
// index.vue
handleRemove(file, raw) {
if (raw) {
// 獲取當(dāng)前的FileItem對象
file = this.getFile(raw);
}
let doRemove = () => {
// 中斷正在上傳的文件
this.abort(file);
// 移除當(dāng)前FileItem
let fileList = this.uploadFiles;
fileList.splice(fileList.indexOf(file), 1);
// 觸發(fā)回調(diào)
this.onRemove(file, fileList);
};
// 外部沒有傳入beforeRemove直接操作刪除
if (!this.beforeRemove) {
doRemove();
} else if (typeof this.beforeRemove === 'function') {
const before = this.beforeRemove(file, this.uploadFiles);
// 和 beforeUpload類似邏輯
if (before && before.then) {
before.then(() => {
doRemove();
}, noop);
} else if (before !== false) {
doRemove();
}
}
}
?? 上傳
構(gòu)造HttpRequest的Options參數(shù)
發(fā)起請求并且緩存當(dāng)前請求實例以便后續(xù)可終止
- 構(gòu)造參數(shù),即
httpRequest需要的請求對象options
| key | 描述 |
|---|---|
| headers | 請求headers |
| withCredentials | 發(fā)送 cookie 憑證信息 |
| data | 請求體數(shù)據(jù) |
| filename | 文件名稱 |
| action | 上傳路徑 |
| onProgress | 上傳進度回調(diào) |
| onSuccess | 上傳成功回調(diào) |
| onError | 上傳錯誤回調(diào) |
當(dāng)外部默認不傳httRequest時,會通過內(nèi)部封裝的ajax.js進行上傳請求(內(nèi)部實現(xiàn)并沒有說什么復(fù)雜的地方,單純的實現(xiàn)原生XMLHttpRequest請求,這里就不對其內(nèi)容進行討論可自行了解),需要注意的是當(dāng)自定義httpRequest時,要對onProgress,onSuccess,onError進行回調(diào),保證結(jié)果在內(nèi)部能獲取到正常響應(yīng)
// ??? upload.vue post
// upload.vue post方法
const { uid } = rawFile;
const options = {
headers: this.headers,
withCredentials: this.withCredentials,
file: rawFile,
data: this.data,
filename: this.name,
action: this.action,
onProgress: e => {
this.onProgress(e, rawFile);
},
onSuccess: res => {
this.onSuccess(res, rawFile);
delete this.reqs[uid];
},
onError: err => {
this.onError(err, rawFile);
delete this.reqs[uid];
}
};
const req = this.httpRequest(options);
- 緩存當(dāng)前請求的每一個實例,同時在
http-request的options內(nèi)的onSuccess和onError回調(diào)時,對緩存的請求實例進行刪除.
??當(dāng)你使用自定義httpRequest注意點:
有返回值時,記得暴露abort方法,因為內(nèi)部默認ajax返回的實例是有abort方法可以中斷請求,而如果自定義時返回沒有abort方法時,點擊刪除會導(dǎo)致報錯
需要在自定義httpRequest內(nèi)部合理時機調(diào)用onSuccess,onProgress,onError,因為這是FileItem.status更新的時機
this.reqs[uid] = req;
if (req && req.then) {
// 在最后觸發(fā)成功之后,再次調(diào)用回調(diào)
req.then(options.onSuccess, options.onError);
}
?? Uploading
當(dāng)觸發(fā)onProgress回調(diào)時,對FileItem對象的status設(shè)置為uploading和percentage上傳進度值,觸發(fā)外部監(jiān)聽的onProgress回調(diào)
// index.vue
handleProgress(ev, rawFile) {
const file = this.getFile(rawFile);
this.onProgress(ev, file, this.uploadFiles);
file.status = 'uploading';
file.percentage = ev.percent || 0;
}
?? Success
當(dāng)觸發(fā)onSuccess回調(diào)時,對FileItem對象的status設(shè)置為success,添加response響應(yīng)值 ,與此同時觸發(fā)外部監(jiān)聽的onSuccess,onChange
handleSuccess(res, rawFile) {
const file = this.getFile(rawFile);
if (file) {
file.status = 'success';
file.response = res;
this.onSuccess(res, file, this.uploadFiles);
this.onChange(file, this.uploadFiles);
}
}
?? Fail
當(dāng)觸發(fā)onError回調(diào)時,對FileItem對象的status設(shè)置為fail,與此同時觸發(fā)外部監(jiān)聽的onError,onChange,當(dāng)報錯觸發(fā)handleError會對相應(yīng)報錯的FileItem從FileList中移除
handleError(err, rawFile) {
const file = this.getFile(rawFile);
const fileList = this.uploadFiles;
file.status = 'fail';
fileList.splice(fileList.indexOf(file), 1);
this.onError(err, file, this.uploadFiles);
this.onChange(file, this.uploadFiles);
}
?? Abort
由于組件對外提供了abort中斷請求的方法,可以通過傳入當(dāng)前正上傳的file對象或者文件的uid可中斷指定的文件上傳同時從FileList中移除,當(dāng)不傳任何參數(shù)時會對正在上傳的全部文件進行中斷
// index.vue
// 對外暴露的方法
abort(file) {
this.$refs['upload-inner'].abort(file);
},
// upload.vue
abort(file) {
const { reqs } = this;
// 傳了指定文件
if (file) {
// 這里支持傳入是一個uid
let uid = file;
if (file.uid) uid = file.uid;
if (reqs[uid]) {
// 將指定的請求進行中斷
reqs[uid].abort();
}
} else {
// 不傳參數(shù)則將正在上傳的全部文件進行中斷
Object.keys(reqs).forEach((uid) => {
if (reqs[uid]) reqs[uid].abort();
delete reqs[uid];
});
}
}
事件觸發(fā)時機圖

總結(jié)
以上就是對Upload組件上傳流程的基本分析。不管是在對組件進行二次開發(fā)或者單獨實現(xiàn)一個上傳組件,希望你能夠?qū)ζ淞鞒涕_發(fā)實現(xiàn)有更好的理解。 如果分析的不是很到位,希望能夠在評論留下你的想法和意見,你的評價和點贊是我學(xué)習(xí)和輸出的最大動力??
以上就是ElementUI Upload源碼組件上傳流程解析的詳細內(nèi)容,更多關(guān)于ElementUI Upload組件上傳的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
element-ui?tree?手動展開功能實現(xiàn)(異步樹也可以)
這篇文章主要介紹了element-ui?tree?手動進行展開(異步樹也可以),項目中用到了vue的element-ui框架,用到了el-tree組件,需要的朋友可以參考下2022-08-08

