很多人在 Vant Uploader 上踩的第一个坑,是以为组件会帮你把图片传到服务器——官方文档写得很清楚:Uploader 只负责选文件、读文件、预览、展示上传状态,真正的上传接口要自己实现。本文按「数据结构 → 前置校验 → 图片压缩 → 后端对接 → 预览交互」的顺序,给出一套可直接落地的 Vue 3 + TypeScript 方案,后端用 PHP 示例接收。
先认清数据结构
Uploader 的 v-model 绑定的是 UploaderFileListItem[] 数组,而不是单个文件。列表项里常用的字段如下:
| 字段 | 说明 | 典型用途 |
|---|---|---|
url / content | 预览图地址(远程 URL 或 base64) | 回显已上传图片 |
file | 原始 File 对象 | 构造 FormData 上传 |
status | uploading / failed / 空 | 驱动遮罩与进度文案 |
message | 状态文案 | 失败时展示原因 |
isImage | 是否按图片预览 | 非图片文件强制图片样式 |
deletable / beforeDelete | 单项删除控制 | 删除前二次确认或调接口 |
after-read 回调的 file 参数在单选时是对象、多选时是数组;result-type 默认是 dataUrl(会把整张图读成 base64),上传场景建议显式设为 file,避免大图把内存撑爆。
最小可用示例:拿到原始 File
<script setup lang="ts">
import { ref } from 'vue';
import { showToast } from 'vant';
import type { UploaderFileListItem } from 'vant';
const fileList = ref<UploaderFileListItem[]>([]);
const afterRead = (item: UploaderFileListItem | UploaderFileListItem[]) => {
const items = Array.isArray(item) ? item : [item];
items.forEach(async (one) => {
console.log(one.file); // 原始 File,可直接 append 进 FormData
one.status = 'uploading';
one.message = '上传中...';
await uploadOne(one);
});
};
</script>
<template>
<van-uploader
v-model="fileList"
:after-read="afterRead"
result-type="file"
:max-count="3"
multiple
/>
</template>
上传前:三道校验
校验尽量前置到「读文件之前」,能省掉无谓的 base64 转换与网络请求。Vant 提供了 before-read(可返回布尔值或 Promise)、max-size(字节数或函数)与 oversize 事件三层能力:
<script setup lang="ts">
import { showToast } from 'vant';
// 1) 类型校验:同步返回 false 即中断读取
const beforeRead = (file: File) => {
if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {
showToast('仅支持 JPG / PNG / WebP');
return false;
}
return true;
};
// 2) 按类型给不同体积上限:返回 true 表示超限
const isOverSize = (file: File) =>
file.size >= (file.type === 'image/jpeg' ? 5 : 8) * 1024 * 1024;
const onOversize = () => showToast('图片不能超过限制大小');
</script>
<template>
<van-uploader
:before-read="beforeRead"
:max-size="isOverSize"
:max-count="3"
accept="image/*"
@oversize="onOversize"
/>
</template>
beforeRead 返回 Promise,resolve(newFile) 传入替换后的 File,reject() 则中断本次读取。这是把压缩挂进上传链路的正规入口。图片压缩:Canvas 一把梭
Vant Uploader 本身不带压缩能力,用 Canvas 等比缩放 + toBlob 重编码即可。下面这个函数限制最长边并控制 JPEG 质量,返回可直接上传的新 File:
interface CompressOptions {
maxWidth?: number;
maxHeight?: number;
quality?: number;
}
export function compressImage(file: File, opts: CompressOptions = {}): Promise<File> {
const { maxWidth = 1280, maxHeight = 1280, quality = 0.8 } = opts;
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
const ratio = Math.min(maxWidth / img.width, maxHeight / img.height, 1);
const canvas = document.createElement('canvas');
canvas.width = Math.round(img.width * ratio);
canvas.height = Math.round(img.height * ratio);
const ctx = canvas.getContext('2d');
if (!ctx) { URL.revokeObjectURL(url); reject(new Error('Canvas 不可用')); return; }
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
URL.revokeObjectURL(url);
if (!blob) { reject(new Error('压缩失败')); return; }
resolve(new File([blob], file.name, { type: 'image/jpeg' }));
}, 'image/jpeg', quality);
};
img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('图片解码失败')); };
img.src = url;
});
}
把它接到 before-read 上,用户选完图后组件拿到的就是压缩后的文件,预览与上传共用同一份数据:
const beforeRead = (file: File) =>
new Promise<File>((resolve, reject) => {
compressImage(file, { maxWidth: 1280, quality: 0.8 })
.then(resolve)
.catch(() => { showToast('图片处理失败'); reject(); });
});
上传到后端:FormData + 状态回写
上传函数负责构造 FormData、发起请求,并逐项更新 status / message / url,让遮罩自动切换。成功时把服务端返回的 URL 写回列表项,删除时才能对应到服务器文件。
import type { UploaderFileListItem } from 'vant';
export async function uploadOne(item: UploaderFileListItem): Promise<void> {
if (!item.file) return;
const form = new FormData();
form.append('file', item.file); // 与后端 $_FILES 的键保持一致
form.append('biz', 'avatar');
try {
const res = await fetch('/api/upload.php', { method: 'POST', body: form });
const data = await res.json();
if (!res.ok || !data.url) throw new Error(data.msg || '上传失败');
item.status = '';
item.message = '';
item.url = data.url; // 换成服务端地址,便于后续提交表单
} catch (e) {
item.status = 'failed';
item.message = '上传失败,点击重试';
}
}
本站是 PHP 技术栈,服务端接收只需读 $_FILES 并做类型、大小校验后移动到可访问目录:
<?php
header('Content-Type: application/json; charset=utf-8');
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(['msg' => '文件接收失败']); exit;
}
$file = $_FILES['file'];
if ($file['size'] > 8 * 1024 * 1024) {
http_response_code(413);
echo json_encode(['msg' => '文件过大']); exit;
}
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'webp'], true)) {
http_response_code(415);
echo json_encode(['msg' => '不支持的格式']); exit;
}
$name = date('YmdHis') . '_' . bin2hex(random_bytes(6)) . '.' . $ext;
$dir = __DIR__ . '/../upload/' . date('Y-m');
if (!is_dir($dir)) mkdir($dir, 0755, true);
move_uploaded_file($file['tmp_name'], $dir . '/' . $name);
echo json_encode(['url' => '/upload/' . date('Y-m') . '/' . $name]);
fetch 提交 FormData 时千万不要手动设置 Content-Type,浏览器需要自动生成带 boundary 的请求头;手写了反而会导致后端拿不到 $_FILES。生产环境还应校验 MIME、限制目录执行权限、对图片做二次渲染。预览与交互细节
- 大图预览:
preview-full-image默认开启,点击缩略图即可全屏预览;preview-options可透传给 ImagePreview。 - 自定义遮罩:用
#preview-cover插槽在缩略图上叠文件名或进度文案。 - 删除拦截:列表项的
beforeDelete可返回false或 Promise,适合「先删服务器再删本地预览」。 - 重新上传:Vant 4.4.0 起提供
reupload属性,开启后点击预览项可覆盖原文件(会关闭大图预览)。 - 主动选图:通过 ref 拿到
UploaderInstance后调用chooseFile(),可用于自定义上传按钮;注意浏览器安全限制,必须在用户手势上下文中调用。
<van-uploader v-model="fileList" :before-delete="beforeDelete">
<van-button icon="plus" type="primary">选择图片</van-button>
<template #preview-cover="{ file }">
<div class="cover">{{ file.message || file.name || '已上传' }}</div>
</template>
</van-uploader>
常见坑速查
after-read 多选时是数组,别当成对象用;② result-type 默认 dataUrl,大图务必改 file;③ max-size 单位是字节,不是 KB;④ 上传成功要回写 url,否则表单提交时拿不到服务端地址;⑤ 相机 capture 在不同 WebView 下兼容性不一,需要真机验证。总结
把 Uploader 理解成「文件选择与状态展示层」,链路就清晰了:before-read 校验与压缩 → after-read 拿原始 File → 自己写 FormData 上传 → 回写 status / url → 删除时同步服务端。组件负责体验,上传负责可靠性,两者各司其职。
组件不替你发请求,但会替你把状态展示得明明白白。