一、Input File
使用 type='file' 的 <input> 元素可以選擇文件,基於此我們可以封裝自定義的上傳組件
<input type="file"> 可以接收四個附加屬性:
accept: 字符串,允許選擇的文件類型,如: ".jpg,.jpeg,.png,.gif"
multiple: 布爾值,是否允許多選
capture: 字符串,可以調用設備默認的相機或錄音機(移動端有效)
<input type="file" accept="image/*" capture="camera">
files: 已選擇文件對象列表,通過 HTMLInputElement.files 獲取和賦值
但 <input type="file"> 包含按鈕和文件列表兩部分,如果給它添加寬高和背景色...
<input type="file" class="upload-inner">
<style type="text/css">
.upload-inner {
width: 200px;
height: 80px;
background-color: #e3e3e3;
}
</style>
這個標籤的樣式基本沒救了...
所以爲了能用上更美觀的上傳控件,通常會選擇隱藏真正的文件上傳控件
然後用其他標籤來代替上傳按鈕,在點擊事件中觸發上傳事件
二、控件設計
這是一個常見的上傳控件樣式,它的 HTML 可以這麼設計:
<!-- wrapper 組件 -->
<template>
<div class="upload-wrapper">
<div class="upload-inputs">
<div class="upload-bg"></div>
<upload
:accept="uploadConfig.accept"
></upload>
<div class="tip">點擊上傳按鈕,或拖拽文件到框內上傳</div>
<div class="tiny-tip">請選擇不大於 10m 的文件</div>
</div>
</div>
</template>
上面是外層的 <wrapper /> 組件,關於上傳控件的具體邏輯可以單獨封裝到 <upload /> 組件中
<!-- upload 組件 -->
<template>
<div class="file-selector">
<button class="selector-btn" @click="handleUpClick">
<slot>選擇文件</slot>
</button>
<input
ref="input"
class="file-selector-input"
type="file"
:multiple="multiple"
:accept="accept"
@change="handleFiles"
/>
</div>
</template>
在 <upload /> 組件中,需要將 <input /> 控件隱藏掉,並用 <button class="selector-btn"> 模擬一個上傳按鈕樣式
<style lang="scss">
.file-selector {
margin-bottom: 16px;
.selector-btn {
background-color: #2976e6;
color: #fff;
padding: 6px 18px;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: rgba($color: #2976e6, $alpha: 0.8);
transition: background 180ms;
}
}
&-input {
display: none;
}
}
</style>
然後通過 <button class="selector-btn"> 的上傳事件來觸發 <input> 的上傳事件 (上面的 handleUpClick)
並監聽 <input> 的 change 事件,處理所選文件 (上面的 handleFiles)
// 調用上傳功能
handleUpClick() {
this.uploadFinished // 維護一個上傳狀態,上傳過程中禁用上傳按鈕
? this.$refs.input.click()
: console.warn('請等待當前文件全部上傳完成');
},
handleFiles(e) {
const files = e?.target?.files;
this.readFiles(files);
},
三、處理文件
上面的 hanldeFiles 調用了 readFiles 方法,這是處理文件的主要邏輯
// 上傳之前將文件處理爲對象
readFiles(files) {
if (!files || files.length <= 0) {
return;
}
for (const file of files) {
const url = window.URL.createObjectURL(file);
const obj = {
title: file.name.replace(/(\.[^\.]*$)|[\_]/gi, ''), // 去掉文件後綴
url,
file,
fileType: file.fileType,
status: 0, // 狀態 -> 0 等待中,1 完成, 2 正在上傳,3 上傳失敗
percent: 0, // 上傳進度
};
// 提前在 data 中定義 list,用來保存需要上傳的文件
this.list.push(obj);
}
// 在 data 中定義 startIndex 初始值爲 0,上傳完成後更新,用於追加上傳文件
this.startUpload(this.startIndex);
},
然後是上傳文件
// 上傳前需要校驗文件
checkFile(index) {
const file = this.list[index];
// 如果文件不存在,即全部文件上傳完成
if (!file) {
this.uploadFinished = true;
// 上傳完成,向父組件拋出 success 事件
this.$emit('success', this.list);
// 清空上傳控件中的值,保證 change 事件能正常觸發
this.$refs.input.value = null;
this.startIndex = index > 1 ? index - 1 : 0;
return false;
}
// 校驗是否已上傳
if (`${file.status}` === "1") {
this.startUpload(++index);
return false;
}
// 校驗文件大小
if (this.maxSize && file.file && file.file.size >= this.maxSize) {
this.startUpload(++index);
return false;
}
return true;
},
// 上傳單個文件
startUpload(index) {
if (!this.checkFile(index)) { return; }
// 開始上傳,維護上傳狀態
this.uploadFinished = false;
const file = this.list[index].file;
const fileObj = this.list[index];
// 創建 formData 用於提交
const data = new FormData();
data.append('userfile', file);
axios({
url: this.url, // 上傳接口,由 props 傳入
method: 'post',
data,
withCredentials: true,
cancelToken: this.source.token, // 用於取消接口請求
// 進度條
onUploadProgress: e => {
if (fileObj.status === 1) { return; } // 已上傳
// 限定最大值爲 99%
const p = parseInt((e.loaded / e.total) * 99);
if (e.total) {
fileObj.status = 2; // 正在上傳
fileObj.percent = p; // 更新上傳進度
} else {
fileObj.status = 3; // 上傳失敗
}
},
})
.then(response => {
if (`${response.code}` === "200") {
fileObj.status = 1;
fileObj.percent = 100;
} else {
fileObj.status = 3;
}
})
.catch(e => {
fileObj.status = 3;
})
.finally(e => {
this.startUpload(++index);
});
// 上傳完成
},
上傳的時候用到了 axios 的 onUploadProgress 來維護上傳進度和當前文件的上傳狀態,用於開發上傳中的樣式
還使用了 cancelToken 來取消上傳,不過取消上傳還需要提前在 data 中定義一個 source 變量
source: axios.CancelToken.source(), // axios 取消請求
所以最終 <upload /> 組件中的 data 爲:
data: () => ({
list: [], // 已選擇的文件對象
uploadFinished: true, // 上傳狀態
startIndex: 0, // 開始上傳的下標,用於追加文件
source: axios.CancelToken.source(), // axios 取消請求
}),
最後在組件中添加一個重置方法
reset() { // 重置
this.list = [];
this.source.cancel();
this.startIndex = 0;
this.uploadFinished = true;
this.$refs.input && (this.$refs.input.value = null);
},
上傳組件的核心邏輯就完成了
四、拖拽上傳
除了基本的點擊按鈕上傳之外,還需要支持拖拽上傳,這部分邏輯可以在 <wrapper /> 組件中完成
先回顧一下組件的 HTML 結構
<!-- wrapper 組件 -->
<template>
<div class="upload-wrapper">
<div
:class="['upload-inputs', dragging ? 'dragging' : '']"
ref="pickerArea"
>
<div class="upload-bg"></div>
<upload
ref="uploadBtn"
:accept="uploadConfig.accept"
></upload>
<div class="tip">點擊上傳按鈕,或拖拽文件到框內上傳</div>
<div class="tiny-tip">請選擇不大於 10m 的文件</div>
</div>
</div>
</template>
上面增加了一個 dragging 變量,用來控制拖拽文件時的樣式,需要在 data 中定義
當組件初始化的時候,需要對 ref="pickerArea" 綁定拖拽事件
在拖拽過程中,通過維護 dragging 狀態來更新熱區樣式,當拖拽結束後,調用 <upload /> 組件的上傳功能
bindEvents() {
const dropbox = this.$refs.pickerArea;
// 防止重複綁定事件,需要在 data 中初始化 bindDrop 爲 false
if (!dropbox || this.bindDrop) { return }
// 綁定拖拽事件,在組件銷燬時解綁
dropbox.addEventListener("drop", this.handleDrop, false);
dropbox.addEventListener("dragleave", this.handleDragLeave);
dropbox.addEventListener("dragover", this.handleDragOver);
this.bindDrop = true;
},
// 拖拽到上傳區域
handleDragOver(e) {
e.stopPropagation();
e.preventDefault();
this.dragging = true;
},
// 離開上傳區域
handleDragLeave(e) {
e.stopPropagation();
e.preventDefault();
this.dragging = false;
},
// 拖拽結束
handleDrop(e) {
e.stopPropagation();
e.preventDefault();
this.dragging = false;
const files = e.dataTransfer.files;
// 調用 <upload/> 組件的上傳功能
this.$refs.uploadBtn && this.$refs.uploadBtn.readFiles(files);
},
上面通過 addEventLister 添加了事件監聽,所以需要在 beforeDestroy 生命週期中註銷
beforeDestroy() {
// 組件銷燬前解綁拖拽事件
try {
const dropbox = this.$refs.pickerArea;
dropbox.removeEventListener("drop", this.handleDrop);
dropbox.removeEventListener("dragleave", this.handleDragLeave);
dropbox.removeEventListener("dragover", this.handleDragOver);
this.bindDrop = false;
} catch (e) {}
},
到這裏一個上傳組件就完成了,不過只有上傳功能,在樣式和交互上還有未盡之處
比如拖拽的時候熱區的樣式,上傳過程中拖拽按鈕的禁用樣式等。不過這些都維護了相應狀態,通過這些狀態來添加樣式即可
真正沒有涉及到的是已選中的文件樣式,包括上傳的進度條、文件操作等等
但在上面 <upload /> 組件中拋出了 this.list,其中的每個元素也維護了上傳狀態和上傳進度,可以基於此來開發對應的交互
收工~