很多人在 Vant Uploader 上踩的第一个坑,是以为组件会帮你把图片传到服务器——官方文档写得很清楚:Uploader 只负责选文件、读文件、预览、展示上传状态,真正的上传接口要自己实现。本文按「数据结构 → 前置校验 → 图片压缩 → 后端对接 → 预览交互」的顺序,给出一套可直接落地的 Vue 3 + TypeScript 方案,后端用 PHP 示例接收。

先认清数据结构

Uploader 的 v-model 绑定的是 UploaderFileListItem[] 数组,而不是单个文件。列表项里常用的字段如下:

字段说明典型用途
url / content预览图地址(远程 URL 或 base64)回显已上传图片
file原始 File 对象构造 FormData 上传
statusuploading / failed / 空驱动遮罩与进度文案
message状态文案失败时展示原因
isImage是否按图片预览非图片文件强制图片样式
deletable / beforeDelete单项删除控制删除前二次确认或调接口

after-read 回调的 file 参数在单选时是对象、多选时是数组;result-type 默认是 dataUrl(会把整张图读成 base64),上传场景建议显式设为 file,避免大图把内存撑爆。

最小可用示例:拿到原始 File

vue src/components/PhotoUploader.vue
<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 事件三层能力:

vue src/components/PhotoUploader.vue(片段)
<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>
ℹ️
before-read 的 Promise 写法
需要在读取前做异步操作(例如先压缩、先换格式)时,可以让 beforeRead 返回 Promise,resolve(newFile) 传入替换后的 File,reject() 则中断本次读取。这是把压缩挂进上传链路的正规入口。

图片压缩:Canvas 一把梭

Vant Uploader 本身不带压缩能力,用 Canvas 等比缩放 + toBlob 重编码即可。下面这个函数限制最长边并控制 JPEG 质量,返回可直接上传的新 File:

typescript src/utils/compressImage.ts
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 上,用户选完图后组件拿到的就是压缩后的文件,预览与上传共用同一份数据:

typescript src/components/PhotoUploader.vue(片段)
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 写回列表项,删除时才能对应到服务器文件。

typescript src/api/upload.ts
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 api/upload.php
<?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(),可用于自定义上传按钮;注意浏览器安全限制,必须在用户手势上下文中调用。
vue 自定义上传区域
<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 → 删除时同步服务端。组件负责体验,上传负责可靠性,两者各司其职。

组件不替你发请求,但会替你把状态展示得明明白白。