Vant4源码阅读之Upload

前言

通过学习 Vant4 的 Upload 组件核心实现,可以帮助我们更好地理解其设计思路,并在实际项目中进行二次封装。


源码解析

1. renderUpload ------ 渲染上传 UI 并绑定事件

  • renderUpload 函数用于返回上传组件的 UI 结构,包括:

    • 文件上传的 <input> 元素
    • Icon 图标渲染
    • 自定义 slot 内容渲染
  • 其中,<input type="file"> 用于调用浏览器原生的文件选择能力:

带有 type="file"<input> 元素允许用户从本地设备中选择一个或多个文件。选择完成后,可以通过:

  • 表单提交的方式上传到服务器
  • 或通过 JavaScript + File API 对文件进行处理
ini 复制代码
const renderUpload = () => {
  const lessThanMax = props.modelValue.length < +props.maxCount;

  // 创建 input,用于文件上传
  const Input = props.readonly ? null : (
    <input
      ref={inputRef}
      type="file"
      class={bem('input')}
      accept={props.accept}
      capture={props.capture as unknown as boolean}
      multiple={props.multiple && reuploadIndex.value === -1}
      disabled={props.disabled}
      onChange={onChange}
      onClick={onInputClick}
    />
  );

  // 如果外部传入了 slot,则优先渲染 slot
  if (slots.default) {
    return (
      <div
        v-show={lessThanMax}
        class={bem('input-wrapper')}
        onClick={onClickUpload}
      >
        {slots.default()}
        {Input}
      </div>
    );
  }

  // 默认上传 UI
  return (
    <div
      v-show={props.showUpload && lessThanMax}
      class={bem('upload', { readonly: props.readonly })}
      style={getSizeStyle(props.previewSize)}
      onClick={onClickUpload}
    >
      <Icon name={props.uploadIcon} class={bem('upload-icon')} />
      {props.uploadText && (
        <span class={bem('upload-text')}>{props.uploadText}</span>
      )}
      {Input}
    </div>
  );
};

2. onInputClick ------ 点击 input 时触发

  • 用于初始化上传相关状态:

    • 如果不是重新上传,则重置 reuploadIndex
    • 标记当前不是重新上传状态
ini 复制代码
const onInputClick = () => {
  if (!isReuploading.value) {
    reuploadIndex.value = -1;
  }
  isReuploading.value = false;
};

3. onChange ------ 文件选择后的核心入口

  • 获取用户选择的文件
  • 执行 beforeRead 钩子(如存在)
  • 最终调用 readFile 进行文件处理
kotlin 复制代码
const onChange = (event: Event) => {
  const { files } = event.target as HTMLInputElement;

  // 无文件或禁用状态直接返回
  if (props.disabled || !files || !files.length) {
    return;
  }

  const file =
    files.length === 1 ? files[0] : ([].slice.call(files) as File[]);

  // 执行 beforeRead 钩子
  if (props.beforeRead) {
    const response = props.beforeRead(file, getDetail());

    // 返回 false,则终止流程并重置 input
    if (!response) {
      resetInput();
      return;
    }

    // 如果返回 Promise
    if (isPromise(response)) {
      response
        .then((data) => {
          readFile(data || file);
        })
        .catch(resetInput);
      return;
    }
  }

  // 默认执行
  readFile(file);
};

// 判断是否为 Promise
export const isPromise = <T = any>(val: unknown): val is Promise<T> =>
  isObject(val) && isFunction(val.then) && isFunction(val.catch);

4. readFile ------ 文件读取调度中心

  • 控制上传数量(maxCount
  • 调用 readFileContent 读取内容
  • 最终统一进入 onAfterRead
ini 复制代码
const readFile = (files: File | File[]) => {
  const { maxCount, modelValue, resultType } = props;

  if (Array.isArray(files)) {
    const remainCount = +maxCount - modelValue.length;

    // 超出数量限制则裁剪
    if (files.length > remainCount) {
      files = files.slice(0, remainCount);
    }

    Promise.all(
      files.map((file) => readFileContent(file, resultType)),
    ).then((contents) => {
      const fileList = (files as File[]).map((file, index) => {
        const result: UploaderFileListItem = {
          file,
          status: '',
          message: '',
          objectUrl: URL.createObjectURL(file),
        };

        if (contents[index]) {
          result.content = contents[index] as string;
        }

        return result;
      });

      onAfterRead(fileList);
    });
  } else {
    readFileContent(files, resultType).then((content) => {
      const result: UploaderFileListItem = {
        file: files,
        status: '',
        message: '',
        objectUrl: URL.createObjectURL(files),
      };

      if (content) {
        result.content = content;
      }

      onAfterRead(result);
    });
  }
};

5. readFileContent ------ 文件内容读取

  • 根据 resultType 决定读取方式:
说明
file 不读取内容,仅返回 File 对象(推荐大文件)
text 读取为文本
dataUrl 读取为 base64
typescript 复制代码
export function readFileContent(file: File, resultType: UploaderResultType) {
  return new Promise<string | void>((resolve) => {
    // file 类型不读取内容
    if (resultType === 'file') {
      resolve();
      return;
    }

    const reader = new FileReader();

    reader.onload = (event) => {
      resolve((event.target as FileReader).result as string);
    };

    if (resultType === 'dataUrl') {
      reader.readAsDataURL(file);
    } else if (resultType === 'text') {
      reader.readAsText(file);
    }
  });
}

6. onAfterRead ------ 文件读取后的统一处理

  • 校验文件大小(maxSize
  • 过滤无效文件
  • 处理重新上传逻辑
  • 更新 v-model
  • 触发用户自定义 afterRead
ini 复制代码
const onAfterRead = (
  items: UploaderFileListItem | UploaderFileListItem[],
) => {
  // 重置 input
  resetInput();

  // 校验文件大小
  if (isOversize(items, props.maxSize)) {
    if (Array.isArray(items)) {
      const result = filterFiles(items, props.maxSize);
      items = result.valid;

      emit('oversize', result.invalid, getDetail());

      if (!items.length) return;
    } else {
      emit('oversize', items, getDetail());
      return;
    }
  }

  items = reactive(items);

  // 处理重新上传
  if (reuploadIndex.value > -1) {
    const arr = [...props.modelValue];
    arr.splice(reuploadIndex.value, 1, items as UploaderFileListItem);
    emit('update:modelValue', arr);
    reuploadIndex.value = -1;
  } else {
    // 正常追加
    emit('update:modelValue', [...props.modelValue, ...toArray(items)]);
  }

  // 用户自定义回调
  if (props.afterRead) {
    props.afterRead(items, getDetail());
  }
};

总结

  • 本文梳理了 Upload 组件的核心流程:
    UI 渲染 → 文件选择 → 前置校验 → 文件读取 → 后置处理 → 数据回传

  • 组件的核心职责包括:

    • 上传 UI 的渲染与交互
    • 文件数据的读取与转换
    • 上传前后钩子的扩展能力
    • 通过 v-model 与父组件进行数据同步

👉 理解这一流程后,你可以更灵活地实现:

  • 自定义上传逻辑(如直传 OSS)
  • 文件预处理(压缩 / 加密)
  • 更复杂的上传状态管理
相关推荐
Highcharts.js2 小时前
经验值|React 实时数据图表性能为什么会越来越卡?
前端·javascript·react.js·数据可视化·实时数据
3秒一个大2 小时前
深入理解 Node.js:生态体系与事件循环机制详解
前端·后端·node.js
freewlt2 小时前
前端工程化进阶:Monorepo 架构实战指南
前端·架构
三翼鸟数字化技术团队2 小时前
DepSleuth - 前端依赖分析工具的技术原理与实践
前端
慧一居士2 小时前
pinia-plugin-persistedstate 在nuxt4项目中服务端渲染,不能使用window对象原因
前端·vue.js
子兮曰2 小时前
同样做中文平台自动化:为什么你越跑越贵,而 OpenCLI 越跑越稳
前端·github·命令行
小陈工2 小时前
2026年4月1日技术资讯洞察:AI芯片革命、数据库智能化与云原生演进
前端·数据库·人工智能·git·python·云原生·开源
酉鬼女又兒2 小时前
零基础快速入门前端深入掌握箭头函数、Promise 与 Fetch API —— 蓝桥杯 Web 考点全解析(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·css·职场和发展·蓝桥杯·es6·js
木斯佳2 小时前
前端八股文面经大全:字节广告交易前端一面(2026-03-31)·面经深度解析
前端·markdown·虚拟列表·流式数据