返回

Vue3 图片上传与本地预览的常见写法

记录 Vue3 和 Element Plus 中图片上传的基础流程:文件类型和大小校验、本地预览、释放 Object URL,以及用 FormData 提交文件。

图片上传通常不只是把文件传到接口:用户还需要在提交前看到预览图,前端需要拦住错误格式和过大的文件,替换图片或离开页面时也要释放浏览器临时创建的地址。

下面用 el-upload 做一个单图上传的基础版本。

先在选择文件时做校验

把校验抽成一个函数,既可以在选择文件时调用,也可以在真正请求前再执行一次。前端校验主要是避免用户走到无效的提交步骤。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { ElMessage } from 'element-plus'
import type { UploadRawFile } from 'element-plus'

const validateImage = (rawFile: UploadRawFile) => {
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
  const isImage = allowedTypes.includes(rawFile.type)
  const isLt2M = rawFile.size / 1024 / 1024 < 2

  if (!isImage) {
    ElMessage.error('请上传 JPG、PNG 或 WebP 图片')
    return false
  }

  if (!isLt2M) {
    ElMessage.error('图片大小不能超过 2MB')
    return false
  }

  return true
}

前端校验只是体验层。后端仍然要校验文件类型、大小和实际内容,不能只相信文件扩展名。

用 Object URL 做本地预览

URL.createObjectURL 可以在不上传文件的情况下生成浏览器可访问的临时地址。替换图片时先释放旧地址,避免长时间操作后积累无用的内存引用。选择无效文件时,同时清空上传列表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue'
import type { UploadFile, UploadRawFile, UploadUserFile } from 'element-plus'

const previewUrl = ref('')
const fileList = ref<UploadUserFile[]>([])
let objectUrl = ''

const revokePreviewUrl = () => {
  if (!objectUrl) return
  URL.revokeObjectURL(objectUrl)
  objectUrl = ''
}

const handleChange = (uploadFile: UploadFile) => {
  const file = uploadFile.raw
  if (!file) return

  if (!validateImage(file)) {
    fileList.value = []
    previewUrl.value = ''
    return
  }

  revokePreviewUrl()
  objectUrl = URL.createObjectURL(file)
  previewUrl.value = objectUrl
}

onBeforeUnmount(revokePreviewUrl)
</script>

<template>
  <el-upload
    :auto-upload="false"
    :limit="1"
    v-model:file-list="fileList"
    accept="image/jpeg,image/png,image/webp"
    :on-change="handleChange"
  >
    <el-button type="primary">选择图片</el-button>
  </el-upload>

  <img v-if="previewUrl" :src="previewUrl" alt="上传图片预览" class="preview" />
</template>

<style scoped>
.preview {
  display: block;
  width: 160px;
  height: 160px;
  margin-top: 16px;
  object-fit: cover;
  border-radius: 8px;
}
</style>

如果页面只需要展示已有的远程头像,直接赋值接口返回的 URL 即可;只有本地刚选择的文件才需要 createObjectURL

用 FormData 提交文件

当表单同时包含文本和文件时,使用 FormData。浏览器会自动补充 multipart/form-data 的边界,因此不要手动给请求头设置 Content-Type

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const submitAvatar = async (file: File) => {
  const formData = new FormData()
  formData.append('avatar', file)
  formData.append('nickname', '林达')

  const response = await fetch('/api/profile/avatar', {
    method: 'POST',
    body: formData,
  })

  if (!response.ok) {
    throw new Error('上传失败,请稍后重试')
  }

  return response.json()
}

接口成功后,应使用服务端返回的正式图片地址替换本地预览地址。这样刷新页面后,用户仍然能看到已保存的图片。

容易忽略的细节

  • accept 限制文件选择器,但不要把它当成安全校验。
  • 单图上传要设置 limit="1",替换时同步清理旧预览。
  • 预览图建议设置固定宽高和 object-fit: cover,避免不同尺寸图片撑坏布局。
  • 上传过程中禁用重复提交,接口失败时给出明确提示。

小结

一个稳定的图片上传流程,至少包括选择前校验、选择后预览、提交时使用 FormData、成功后替换正式 URL,以及离开页面时释放临时地址。把这些细节补齐后,头像、封面图和后台图片字段都可以复用同一套思路。

使用 Hugo 构建
主题 StackJimmy 设计