使用Vue3實現(xiàn)一個Upload組件的示例代碼
通用上傳組件開發(fā)
開發(fā)上傳組件前我們需要了解:
首先實現(xiàn)一個最基本的上傳流程:
基本上傳流程,點擊按鈕選擇,完成上傳
代碼如下:
<template>
<div class="app-container">
<!--使用change事件-->
<input type="file" @change="handleFileChange">
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'
export default defineComponent({
name: 'App',
setup() {
const handleFileChange = (e: Event) => {
// 斷言為HTMLInputElement
const target = e.target as HTMLInputElement
const files = target.files
if(files) {
const uploadedFile = files[0]
const formData = new FormData()
formData.append('file', uploadedFile)
// 使用node模擬上傳接口
axios.post('http://localhost:3001/upload', formData, {
headers: {
"Content-Type": 'multipart/form-data'
}
}).then(resp=> {
console.log('resp', resp)
}).catch(error=> {
})
}
}
return {
handleFileChange
}
}
})
</script>
<style>
.page-title {
color: #fff;
}
</style>
結(jié)果如下:

到這里我們基本的上傳已經(jīng)處理完成了,相對來說還是比較簡單的,接下來我們創(chuàng)建Uploader.vue文件用來封裝Upload組件。
我們需要實現(xiàn)如下功能
自定義模版
我們知道使用<input type="file">系統(tǒng)自帶樣式比較難看所用我們需要如下處理:
- 對樣式進行優(yōu)化,隱藏input
- 點擊<div class="upload-area" @click="triggerUpload"></div>使用js出發(fā)input的click事件
- 處理上傳狀態(tài)
代碼如下:
<template>
<div class="file-upload">
<div class="upload-area" @click="triggerUpload"></div>
<span v-if="fileStatus==='loading'">正在上傳</span>
<span v-else-if="fileStatus==='success'">上傳成功</span>
<span v-else-if="fileStatus==='error'">上傳失敗</span>
<span v-else>點擊上傳</span>
<input ref="fileInput" type="file" name="file" style="display: none" />
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, reactive, ref } from 'vue'
import axios from 'axios'
type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
export default defineComponent({
props: {
action: { // url地址
type: String,
required: true
}
},
setup(props) {
// input實例對象,通過與ref="fileInput"的關聯(lián)獲取到input實例對象
const fileInput = ref<null | HTMLInputElement>(null)
const fileStatus = ref<UploadStatus>('ready')
// 1.div點擊事件
const triggerUpload = () => {
if(fileInput.value) {
fileInput.value.click()
}
}
// 通過div來觸發(fā)input的change事件
const handleFileChange = (e: Event) => {
const target = e.target as HTMLInputElement
const files = target.files
if(files) {
const uploadedFile = files[0]
const formData = new FormData()
formData.append('file', uploadedFile)
readyFile.status = 'loading' // 上傳之前將狀態(tài)設置為loading
axios.post(props.actions, formData, {
headers: {
"Content-Type": 'multipart/form-data'
}
}).then(resp=> {
console.log('resp', resp)
readyFile.status = 'success' // 上傳成功將狀態(tài)設置為success
}).catch(error=> {
readyFile.status = 'error' // // 上傳失敗將狀態(tài)設置為error
})
}
}
return {
fileInput,
triggerUpload,
handleFileChange,
fileStatus
}
}
})
</script>
現(xiàn)在我們已經(jīng)完成了對上傳組件樣式的優(yōu)化,和上傳狀態(tài)的處理,接下來我們需要處理文件上傳列表
支持文件上傳列表
處理文件上傳列表我們需要有如下實現(xiàn):
- 顯示文件名稱
- 狀態(tài)
- 可刪除
- 顯示上傳進度
- 有可能有更豐富的顯示
在上一步代碼的基礎上做一些修改:
<template>
<div class="file-upload">
<div class="upload-area" @click="triggerUpload"></div>
<!-- 點擊上傳態(tài) -->
<slot v-if="isUploading" name='loading'>
<button disabled>正在上傳</button>
</slot>
<!-- 上傳完畢態(tài) -->
<slot name="uploaded" v-else-if="lastFileData && lastFileData.loaded" :uploadData="lastFileData.data">
<button disabled>點擊上傳</button>
</slot>
<!-- 默認態(tài) -->
<slot v-else name='default'>
<!-- <button disabled>點擊上傳</button> -->
<span>點擊上傳</span>
</slot>
<input ref="fileInput" type="file" name="file" style="display: none" />
<!--展示fileList-->
<ul>
<li
v-for="file in filesList"
:key="file.uid"
:class="`uploaded-file upload-${file.status}`"
>
<span class="filname">
{{ file.name }}
</span>
<button class="delete-icon" @click="reomveFile(file.uid)">Del</button>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { last } from 'lodash-es'
import { v4 as uuidv4 } from 'uuid';
// 定義上傳狀態(tài)
type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
// step1 定義上傳文件對象接口類
export interface UploadFile {
uid: string; // 文件唯一id
size: number; // 文件大小
name: string; // 文件名稱
status: UploadStatus; // 上傳狀態(tài)
raw: File; // 文件
progress?: string; // 文件上傳進度
resp?: any; // 服務端返回數(shù)據(jù)
url?: string // 對應展示的url
}
export default defineComponent({
props: {
action: { // url地址
type: String,
required: true
}
},
setup(props) {
// input實例對象,通過與ref="fileInput"的關聯(lián)獲取到input實例對象
const fileInput = ref<null | HTMLInputElement>(null)
// step2 上傳文件列表
const filesList = ref<UploadFile[]>([])
// step4-1 判斷是否正在上傳中
const isUploading = computed(()=> {
return filesList.value.some((file)=>file.status==='loading')
})
//step4-2 獲取上傳文件的最后一項
const lastFileData = computed(()=>{
const lastFile = last(filesList.value)
if(!lastFile) return false
return {
loaded: lastFile?.status === 'success',
data: lastFile?.resp
}
})
// 1.div點擊事件
const triggerUpload = () => {
if(fileInput.value) {
fileInput.value.click()
}
}
// 通過div來觸發(fā)input的change事件
const handleFileChange = (e: Event) => {
const target = e.target as HTMLInputElement
const files = target.files
if(files) {
const uploadedFile = files[0]
const formData = new FormData()
formData.append('file', uploadedFile)
// step3 設置響應式對象,存儲到filesList中,以便在頁面中展示
const fileObj = reactive<UploadFile>({
uid: uuid(); // 文件唯一id
size: uploadedFile.size,
name: uploadedFile.name,
status: 'loading',
raw: uploadedFile
})
filesList.value.push(fileObj)
axios.post(props.actions, formData, {
headers: {
"Content-Type": 'multipart/form-data'
},
//step6 處理上傳進度
onUploadProgress: (progressEvent)=> {
const complete = (progressEvent.loaded / progressEvent.total * 100 | 0) + '%'
fileObj.progress = complete
}
}).then(resp=> {
console.log('resp', resp)
fileObj.status = 'success'
}).catch(error=> {
fileObj.status = 'error'
}).finally(()=> {
// 一張圖片重復上傳時無法繼續(xù)上傳bug
if(fileInput.value) {
fileInput.value.value = ''
}
})
}
}
// step7 處理刪除
const reomveFile = (uid: string) => {
filesList.value = filesList.value.filter(file=>file.uid!==uid)
}
return {
fileInput,
triggerUpload,
handleFileChange,
fileStatus,
isUploading,
filesList,
lastFileData
}
}
})
</script>
- 首先我們定義上傳文件對象接口類UploadFile
- 創(chuàng)建了一個filesList響應式對象
- 去掉了fileStatus,因為在UploadFile接口中我們已經(jīng)有定義status
- 修改模版中的狀態(tài),利用computed來判讀是否是處于上傳狀態(tài),并增加slot進行自定義
- 展示上傳的圖片
- 處理上傳進度
- 處理刪除
注意點:如果選擇相同的圖片不會進行上傳操作,因為我們使用的是input的change事件,所以我們需要在上傳后將input的value設置為null
支持一系列生命周期鉤子事件,上傳事件
beforeUpload
<template>
...
</template>
<script lang="ts">
import { last } from 'lodash-es'
import { v4 as uuidv4 } from 'uuid';
// 定義上傳狀態(tài)
type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
// 定義上傳文件為boolean或promsie且接受一個File
type CheckUpload = ()=> boolean | Promise<File>
export interface UploadFile {
...
}
export default defineComponent({
props: {
action: { // url地址
type: String,
required: true
},
beforeUpload: {
type: Function as PropType<CheckUpload>
}
},
setup(props) {
const fileInput = ref<null | HTMLInputElement>(null)
const filesList = ref<UploadFile[]>([])
// step4-1 判斷是否正在上傳中
const isUploading = computed(()=> {
return filesList.value.some((file)=>file.status==='loading')
})
const lastFileData = computed(()=>{
const lastFile = last(filesList.value)
if(!lastFile) return false
return {
loaded: lastFile?.status === 'success',
data: lastFile?.resp
}
})
const triggerUpload = () => {
if(fileInput.value) {
fileInput.value.click()
}
}
const handleFileChange = (e: Event) => {
const target = e.target as HTMLInputElement
const files = target.files
const uploadedFile = files[0]
if(props.beforeUpload) {
const result = props.beforeUpload(uploadedFile)
if(result && result instanceof Promise) {
result.then((processFile)=> {
if(uploadedFile instanceof File) {
postFile(uploadedFile)
}
}).catch(error=>{
console.error(error)
})
} esle if(result===true) {
postFile(uploadedFile)
}
}else{
postFile(uploadedFile)
}
}
const reomveFile = (uid: string) => {
filesList.value = filesList.value.filter(file=>file.uid!==uid)
}
// 處理文件上傳
const postFile = (readyFile:UploadFile)=> {
const formData = new FormData()
formData.append('file', readyFile.raw)
readyFile.status = 'loading'
axios.post(props.action, formData, {
headers: {
"Content-Type": 'multipart/form-data'
},
onUploadProgress: (progressEvent)=> {
const complete = (progressEvent.loaded / progressEvent.total * 100 | 0) + '%'
// console.log('上傳 ' + complete)
readyFile.progress = complete
}
}, ).then(resp=> {
console.log('resp', resp)
// fileStatus.value = 'success'
readyFile.status = 'success'
readyFile.resp = resp.data
}).catch(error=> {
// fileStatus.value = 'error'
readyFile.status = 'error'
})
.finally(()=> {
// 一張圖片重復上傳時無法繼續(xù)上傳bug
if(fileInput.value) {
fileInput.value.value = ''
}
})
}
return {
fileInput,
triggerUpload,
handleFileChange,
fileStatus,
isUploading,
filesList,
lastFileData
}
}
})
</script>
實現(xiàn)步驟:
- 在poops處定義屬性beforeUpload,同時定義上傳文件為boolean或promsie且接受一個File
- 將原上傳方法進行封裝為postFile
- 根據(jù)beforeUpload返回結(jié)果,進行接下來的流程處理
onProgress,onSuccess,onError,onChange與之類似
拖拽支持
大致流程如下圖:

- dragover和dragleave添加和刪除對應的class
- drop拿到正在被拖拽的文件,刪除class,并且觸發(fā)上傳
- 只有在屬性drag為true才能觸發(fā)
需要注意v-on的使用具體可參考vue官方文檔
上代碼:
<template>
<div class="file-upload">
<div
:class="['upload-area', drag && isDragOver ? 'is-dragover': '' ]"
v-on="events"
>
<!-- 點擊上傳態(tài) -->
<slot v-if="isUploading" name='loading'>
<button disabled>正在上傳</button>
</slot>
<!-- 上傳完畢態(tài) -->
<slot name="uploaded" v-else-if="lastFileData && lastFileData.loaded" :uploadData="lastFileData.data">
<button disabled>點擊上傳</button>
</slot>
<!-- 默認態(tài) -->
<slot v-else name='default'>
<!-- <button disabled>點擊上傳</button> -->
<span>點擊上傳</span>
</slot>
</div>
<input ref="fileInput" type="file" name="file" style="display: none" @change="handleFileChange" />
<ul>
<li
v-for="file in filesList"
:key="file.uid"
:class="`uploaded-file upload-${file.status}`"
>
<img
v-if="file.url && listType === 'picture'"
class="upload-list-thumbnail"
:src="file.url"
:alt="file.name"
>
<span class="filname">
{{ file.name }}
</span>
<span class="progress">
{{ file.progress }}
</span>
<button class="delete-icon" @click="reomveFile(file.uid)">Del</button>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, reactive, ref } from 'vue'
import axios from 'axios'
import { v4 as uuidv4 } from 'uuid';
import { last } from 'lodash-es'
type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
type fileListType = 'text' | 'picture'
type CheckUpload = ()=> boolean | Promise<File>
export interface UploadFile {
uid: string; // 文件唯一id
size: number; // 文件大小
name: string; // 文件名稱
status: UploadStatus; // 上傳狀態(tài)
raw: File; // 文件
progress?: string;
resp?: any; // 服務端返回數(shù)據(jù)
url?: string // 對應展示的url
}
// dragOver 文件拖拽到區(qū)域時觸發(fā)
// dragLeave 文件離開拖動區(qū)域
// drop 事件拿到正在被拖拽的文件刪除class并且觸發(fā)上傳
// 只有設置drag的時候才有效
export default defineComponent({
name: 'Uploader',
props: {
action: { // url地址
type: String,
required: true
},
beofreUpload:{ // 上傳之前的處理
type: Function as PropType<CheckUpload>
},
drag: { // 是否拖拽
type: Boolean,
default: false
},
autoUpload: { // 自動上傳
type: Boolean,
default: true
},
listType: {
type: String as PropType<fileListType>,
default: 'text'
}
},
setup(props) {
const fileInput = ref<null | HTMLInputElement>(null)
const fileStatus = ref<UploadStatus>('ready')
// 存儲上傳的文件
const filesList = ref<UploadFile[]>([])
// 定義了一個isDragOver標識,處理拖動后樣式.upload-area顯示
const isDragOver = ref<boolean>(false)
const triggerUpload = () => {
if(fileInput.value) {
fileInput.value.click()
}
}
let events: {[key:string]: (e: any)=>void} = {
'click': triggerUpload,
}
// 只要有一個文件狀態(tài)是loading態(tài)就說明是正在上傳
const isUploading = computed(()=>{
return filesList.value.some((file)=> file.status==='loading')
})
// 獲取上傳文件的最后一項
const lastFileData = computed(()=>{
const lastFile = last(filesList.value)
if(!lastFile) return false
return {
loaded: lastFile?.status === 'success',
data: lastFile?.resp
}
})
// 處理dragover,dragleave
const handleDrag = (e: DragEvent,over: boolean)=> {
// 阻止默認事件
e.preventDefault()
// dragover為true,dragleave為fasle
isDragOver.value = over
}
// 處理drop
const handleDrop = (e: DragEvent) => {
e.preventDefault()
isDragOver.value = false
if(e.dataTransfer) {
beforeUploadCheck(e.dataTransfer.files)
}
}
if(props.drag){
events = {
...events,
'dragover': (e: DragEvent) => { handleDrag(e, true)},
'dragleave': (e: DragEvent) => { handleDrag(e, false)},
'drop': handleDrop
}
// console.log(events)
}
// 刪除文件
const reomveFile = (uid: string)=> {
filesList.value = filesList.value.filter(file=>file.uid!==uid)
}
const postFile = (readyFile:UploadFile) => {
const formData = new FormData()
formData.append('file', readyFile.raw)
readyFile.status = 'loading'
// 選擇文件后push到存儲對象里面
axios.post(props.action, formData, {
headers: {
"Content-Type": 'multipart/form-data'
},
onUploadProgress: (progressEvent)=> {
const complete = (progressEvent.loaded / progressEvent.total * 100 | 0) + '%'
// console.log('上傳 ' + complete)
readyFile.progress = complete
}
}, ).then(resp=> {
console.log('resp', resp)
// fileStatus.value = 'success'
readyFile.status = 'success'
readyFile.resp = resp.data
}).catch(error=> {
// fileStatus.value = 'error'
readyFile.status = 'error'
})
.finally(()=> {
// 一張圖片重復上傳時無法繼續(xù)上傳bug
if(fileInput.value) {
fileInput.value.value = ''
}
})
}
const addFileToList = (uploadedFile: File) => {
const fileObj = reactive<UploadFile>({
uid: uuidv4(),
size: uploadedFile.size,
name: uploadedFile.name,
status: 'ready',
raw: uploadedFile
})
// 處理圖片格式并顯示
if(props.listType==='picture') {
// try {
// fileObj.url = URL.createObjectURL(uploadedFile)
// }catch(err) {
// console.error('upload file err', err)
// }
const fileReader = new FileReader()
fileReader.readAsDataURL(uploadedFile)
fileReader.addEventListener('load', ()=> {
fileObj.url = fileReader.result as string
})
}
filesList.value.push(fileObj)
if(props.autoUpload) {
postFile(fileObj)
}
}
const uploadFiles = ()=> {
// filesList.value.filter(file => file.status === 'ready').forEach(readyFile => postFile(readyFile))
filesList.value.filter(file => file.status === 'ready').forEach(readyFile=>postFile(readyFile))
}
const beforeUploadCheck = (files: null | FileList ) => {
if(files) {
fileStatus.value = 'loading'
const uploadedFile = files[0]
if(props.beofreUpload) {
const result = props.beofreUpload()
// 如果有返回值
if(result && result instanceof Promise) {
result.then(processedFile=> {
if(processedFile instanceof File) {
addFileToList(processedFile)
} else {
throw new Error('beforeUpload promise should return file object')
}
}).catch(err=> {
console.log(err)
})
} else if(result === true) {
addFileToList(uploadedFile)
}
} else {
addFileToList(uploadedFile)
}
}
}
const handleFileChange = (e: Event) => {
const target = e.target as HTMLInputElement
const files = target.files
beforeUploadCheck(files)
}
return {
fileInput,
triggerUpload,
handleFileChange,
isUploading,
filesList,
reomveFile,
lastFileData,
beforeUploadCheck,
isDragOver,
events
}
}
})
</script>
以上就是基本的基于vue3實現(xiàn)的上傳通用組件
另附上上傳接口代碼:
// 引入模塊
const Koa = require('koa');
const fs = require('fs');
const path = require('path');
const router = require('koa-router')();
const koaBody = require('koa-body');
const static = require('koa-static');
const cors = require('koa2-cors')
// 實例化
const app = new Koa();
app.use(koaBody({
multipart: true, // 支持文件上傳
formidable: {
maxFieldsSize: 2 * 1024 * 1024, // 最大文件為2兆
multipart: true // 是否支持 multipart-formdate 的表單
}
}));
const uploadUrl = "http://localhost:3001/static/upload";
// 配置路由
router.get('/', (ctx) => {
// 設置頭類型, 如果不設置,會直接下載該頁面
ctx.type = 'html';
// 讀取文件
const pathUrl = path.join(__dirname, '/static/upload.html');
ctx.body = fs.createReadStream(pathUrl);
});
// 上傳文件
router.post('/upload', (ctx) => {
// 獲取上傳文件
const file = ctx.request.files.file;
console.log(file);
// 讀取文件流
const fileReader = fs.createReadStream(file.path);
// 設置文件保存路徑
const filePath = path.join(__dirname, '/static/upload/');
// 組裝成絕對路徑
const fileResource = filePath + `/${file.name}`;
/**
* 使用 createWriteStream 寫入數(shù)據(jù),然后使用管道流pipe拼接
*/
const writeStream = fs.createWriteStream(fileResource);
// 判斷 /static/upload 文件夾是否存在,如果不在的話就創(chuàng)建一個
if (!fs.existsSync(filePath)) {
fs.mkdir(filePath, (err) => {
if (err) {
throw new Error(err);
} else {
fileReader.pipe(writeStream);
ctx.body = {
url: uploadUrl + `/${file.name}`,
code: 0,
message: '上傳成功1'
};
}
});
} else {
fileReader.pipe(writeStream);
ctx.body = {
url: uploadUrl + `/${file.name}`,
code: 0,
message: '上傳成功1'
};
}
});
// 配置靜態(tài)資源路徑
app.use(static(path.join(__dirname)));
// 開啟跨域
app.use(cors())
// 啟動路由
app.use(router.routes()).use(router.allowedMethods());
// 監(jiān)聽端口號
app.listen(3001, () => {
console.log('server is listen in 3001');
});
寫在最后
已上只是一個簡單的實現(xiàn),當然了我們也可以添加自定義headers,自定義傳遞數(shù)據(jù)data,accecpt等等
到此這篇關于使用Vue3實現(xiàn)一個Upload組件的示例代碼的文章就介紹到這了,更多相關Vue3 Upload組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
移動端H5開發(fā) Turn.js實現(xiàn)很棒的翻書效果
這篇文章主要為大家詳細介紹了Turn.js實現(xiàn)很棒的翻書效果,對Turn.js翻書效果的實現(xiàn)進行總結(jié),感興趣的小伙伴們可以參考一下2016-06-06
javascript showModalDialog 多層模態(tài)窗口實現(xiàn)頁面提交及刷新的代碼
javascript 多層模態(tài)窗口showModalDialog頁面提交及刷新2009-11-11
詳細分析Javascript中創(chuàng)建對象的四種方式
這篇文章詳細介紹了Javascript中創(chuàng)建對象的幾種方式與每種方式的優(yōu)缺點,其中包括工廠模式、構(gòu)造函數(shù)模式、原型模式和組合使用構(gòu)造函數(shù)模式和原型模式,有需要的小伙伴們一起來學習學習吧。2016-08-08
javascript tips提示框組件實現(xiàn)代碼
一個簡單的類似title的提示效果,但現(xiàn)實內(nèi)容可以很豐富,以上js另存為tip.js,下面是使用的demo。2010-11-11
深入理解JavaScript系列(7) S.O.L.I.D五大原則之開閉原則OCP
本章我們要講解的是S.O.L.I.D五大原則JavaScript語言實現(xiàn)的第2篇,開閉原則OCP(The Open/Closed Principle )。2012-01-01

