1.介绍
虽然前端UI框架大都提供文件上传的组件,以及很多插件可供选择,工作中可能不需要我们手写一个上传组件,但是从零封装组件对学习是很有助益的。下文为大家介绍使用Vue3+TypeScript实现的一个文件上传的功能,目前只实现上传等基本功能,后续会逐渐对功能进行扩展,持续更新。
效果如下图
2.思路
文件上传的两种实现方式
- From形式
<form
method="post"
enctype="multipart/from-data"
action="api/upload"
>
<input type="file name="file">
<button type="submit">Submit</button>
</form>
form的method属性指定为 "post" 请求,通过HTML表单发送数据给服务器,并返回服务器的修改结果,在这种情况下Content-Type是通过在<form>
元素中设置正确的enctype属性。
form的enctype属性规定在发送到服务器之前应该如何对表单数据进行编码。
- application/x-www-form-urlencoded(默认值):表示在发送前编码所有字符,数据被编码成以"&"分隔的键值对,同时以"="分隔键和值,(
"name=seven&age=19"
)。不支持二进制数据。 - multipart/form-data:支持二进制数据(上传文件时必须指定)
- JavaScript异步请求形式
我们知道 FormData 接口提供了一种表示表单数据的键值对 key/value 的构造方式,并且可以轻松的将数据通过XMLHttpRequest.send()
方法发送出去,本接口和此方法都相当简单直接。如果送出时的编码类型被设为 "multipart/form-data"
,它会使用和表单一样的格式。
var formdata = new FormData(); // 创建FormData对象
formdata.append("name","laotie"); // 通过append()方法添加新的属性值
... // 更多方法请点下面链接
FormData接口
3.生命周期
上传组件也有它的生命周期
beforeUpload --> uploading --> fileUploaded 或者 uploadedError
4.代码草稿
本例中采用js异步请求的方式开发上传组件
<input type="file" name="file" @change.prevent="handleFileChange">
// 创建一个file类型的input,用于触发文件上传,后面可以把input隐藏掉,自定义好看的样式
// 自定义样式的时候可以用slot区分不同上传状态的样式(loading,success,defult)
const handleFileChange = (e:Event)=>{
const target = e.target as HTMLInputElement
const files = Array.from(target.files)// 注意这里取得的是一个类数组
if(files){
// 取得文件
const uploadedFile = files[0]
if(!validateFormat) return
// ...这里只是提供一种思路,具体校验不再讲述
// 在这里做一些上传文件前的校验,比如文件格式,大小等,
// 不符合要求的话就不在继续发送请求
const formData = new FormData()
formData.append(uploadedFile.name,uploadedFile)
axios.post('/upload',formData,{
headers:{
// 注意设置编码类型
'Content-Type': 'multipart/form-data'
}
}).then(res=>{
console.log('上传成功')
}).catch(error =>{
// 文件上传失败
}).finally(()=>{
// 文件上传完成,无论成功还是失败
// 这里可以清除一下input.value
})
}
}
5.具体实现
// Upload.vue
<template>
<div class="upload-container">
<div class="upload-box" @click.prevent="triggerUpload" v-bind="$attrs">
<slot name="loading" v-if="fileStatus==='loading'">
<button class="btn btn-primary">上传中</button>
</slot>
<slot name="uploaded" v-else-if="fileStatus==='success'" :uploadedData="fileData">
<button class="btn btn-primary">上传成功</button>
</slot>
<slot v-else name="default">
<button class="btn btn-primary">点击上传</button>
</slot>
</div>
<input type="file" class="file-input d-none" name="file" ref="uploadInput" @change="hanldeInput"/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, PropType, watch } from 'vue'
import axios from 'axios'
type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
type FunctionProps = (file:File) => boolean
export default defineComponent({
name: 'Upload',
inheritAttrs: false,
props: {
// 上传的url
action: {
type: String,
required: true
},
// 上传之前的校验,是一个返回布尔值的函数
beforeUpload: {
type: Function as PropType<FunctionProps>
},
// 上传好的数据,用来判断状态或做初始化展示
uploadedData: {
type: Object
}
},
emits: ['file-uploaded-success', 'file-uploaded-error'],
setup(props, ctx) {
const uploadInput = ref<null | HTMLInputElement>(null)
const fileStatus = ref<UploadStatus>(props.uploadedData ? 'success' : 'ready')
const fileData = ref(props.uploadedData)
watch(() => props.uploadedData, (val) => {
if (val) {
fileStatus.value = 'success'
fileData.value = val
}
})
const triggerUpload = () => {
if (uploadInput.value) {
uploadInput.value.click()
}
}
const hanldeInput = (e:Event) => {
const target = e.target as HTMLInputElement
const files = target.files
console.log(target)
if (files) {
const uploadFile = Array.from(files)
const validateFormat = props.beforeUpload ? props.beforeUpload(uploadFile[0]) : true
if (!validateFormat) return
fileStatus.value = 'loading'
const formData = new FormData()
formData.append('file', uploadFile[0])
axios.post(props.action, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(res => {
console.log('文件上传成功', res)
fileStatus.value = 'success'
fileData.value = res.data
ctx.emit('file-uploaded-success', res.data)
}).catch(error => {
console.log('文件上传失败', error)
fileStatus.value = 'error'
ctx.emit('file-uploaded-error', error)
}).finally(() => {
console.log('文件上传完成')
if (uploadInput.value) {
uploadInput.value.value = ''
}
})
}
}
return {
uploadInput,
triggerUpload,
hanldeInput,
fileStatus,
fileData
}
}
})
</script>
使用示例:
<template>
<div class="create-post-page">
<upload
action="/upload"
:beforeUpload="beforeUpload"
:uploadedData="uploadedData"
@file-uploaded-success="hanldeUploadSuccess"
class="d-flex align-items-center justify-content-center bg-light text-secondary w-100 my-4"
>
<template #uploaded="slotProps">
<div class="uploaded-area">
<img :src="slotProps.uploadedData.data.url"/>
<h3>点击重新上传</h3>
</div>
</template>
<template #default>
<h2>点击上传头图</h2>
</template>
<template #loading>
<div class="d-flex">
<div class="spinner-border text-secondary" role="status">
<span class="sr-only"></span>
</div>
</div>
</template>
</upload>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'
import Upload from '../components/Upload.vue'
import createMessage from '../components/createMessage'
export default defineComponent({
name: 'CreatePost',
components: { Upload },
setup() {
const uploadedData = ref() //创建一个响应式数据
let imageId = ''
onMounted(() => {
....
// 这里有逻辑省略了,取到初始化数据image
if (image) {
uploadedData.value = { data: image }
}
})
// 上传前校验,返回布尔值
const beforeUpload = (file:File) => {
const res = beforeUploadCheck(file, {
format: ['image/jpeg', 'image/png'],
size: 1
})
const { error, passed } = res
if (error === 'format') {
createMessage('上传图片只能是JPG/PNG格式!', 'error')
}
if (error === 'size') {
createMessage('上传图片大小不能超过1MB', 'error')
}
return passed
}
// 上传成功后拿到imageId就可以进行后续处理,创建表单啥的
const hanldeUploadSuccess = (res:ResponeseProps<ImageProps>) => {
createMessage(`上传图片ID ${res.data._id}`, 'success')
if (res.data._id) {
imageId = res.data._id
}
}
return {
beforeUpload,
hanldeUploadSuccess,
uploadedData
}
}
})
</script>
<style>
.create-post-page{
padding:0 20px 20px;
}
.create-post-page .upload-box{
height:200px;
cursor: pointer;
overflow: hidden;
}
.create-post-page .upload-box img{
width: 100%;
height: 100%;
object-fit: cover;
}
.uploaded-area{
position: relative;
}
.uploaded-area:hover h3{
display: block;
}
.uploaded-area h3{
display: none;
position: absolute;
color: #999;
text-align: center;
width: 100%;
top:50%
}
</style>
6 git地址
还没上传,后续补充
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!