突破上传体验瓶颈:md-editor-v3 文件上传进度可视化全解析

突破上传体验瓶颈:md-editor-v3 文件上传进度可视化全解析

你是否还在为 Markdown 编辑器中文件上传进度不透明而困扰?用户反复点击上传按钮导致重复提交?大文件上传时页面假死引发用户焦虑?本文将深入剖析 md-editor-v3 的文件上传架构,从 0 到 1 实现进度可视化功能,彻底解决上传交互痛点。

读完本文你将获得:

  • 掌握基于 Vue3+TypeScript 的上传进度监听实现
  • 学习如何设计低侵入性的状态管理方案
  • 实现适配深色/浅色主题的进度展示组件
  • 理解大文件分片上传的前端处理策略

架构分析:md-editor-v3 的上传模块设计

核心组件协作流程

md-editor-v3 采用模块化设计,文件上传功能主要涉及三个核心模块:

mermaid

工具栏组件(Toolbar/index.tsx)通过事件总线触发上传流程,核心代码如下:

// 工具栏中的上传触发逻辑
<li 
  class={`${prefix}-menu-item ${prefix}-menu-item-image`}
  onClick={() => {
    (uploadRef.value as HTMLInputElement).click();
  }}
>
  {ult.value.imgTitleItem?.upload}
</li>

// 文件选择后触发上传
(uploadRef.value as HTMLInputElement).addEventListener('change', uploadHandler);
const uploadHandler = () => {
  bus.emit(editorId, UPLOAD_IMAGE, Array.from((uploadRef.value as HTMLInputElement).files || []));
};

上传相关 Props 配置

packages/MdEditor/props.ts 中定义了上传功能的核心配置项:

参数名类型默认值描述
onUploadImgFunctionundefined上传回调函数
noUploadImgBooleanfalse是否禁用上传功能
transformImgUrlFunctiont => t图片URL转换函数
sanitizeFunctionhtml => htmlHTML内容过滤函数

其中 onUploadImg 是实现进度显示的关键入口,组件通过该回调将原生 File 对象传递给业务层:

// 上传事件类型定义
export type UploadImgEvent = (files: Array<File>, callBack: UploadImgCallBack) => void;
export type UploadImgCallBackParam = string[] | Array<{ url: string; alt: string; title: string }>;
export type UploadImgCallBack = (urls: UploadImgCallBackParam) => void;

痛点诊断:现有上传流程的用户体验问题

通过分析 md-editor-v3 的上传实现,我们发现三个关键痛点:

  1. 状态不透明:上传过程中缺乏视觉反馈,用户无法判断操作是否生效
  2. 进度不可见:大文件上传时没有进度指示,导致用户反复尝试
  3. 错误无提示:上传失败后没有明确的错误原因说明

这些问题在 Editor.tsx 的内容组件中表现尤为明显,当前实现直接将文件传递给业务层处理,没有预留进度更新的接口:

// 现有实现缺乏进度反馈机制
onUploadImg(files, (urls) => {
  // 直接处理上传结果,无中间状态反馈
  insertImages(urls);
});

实现方案:三步打造完整进度可视化系统

1. 扩展上传事件接口

首先需要在上传回调中增加进度反馈通道,修改 packages/MdEditor/type.ts 中的事件定义:

// 新增进度回调类型定义
export type UploadProgressEvent = (file: File, progress: {
  percentage: number;
  loaded: number;
  total: number;
}) => void;

// 扩展上传事件接口
export interface EditorEmits {
  // ... 原有定义
  'onUploadProgress'?: UploadProgressEvent;
}

2. 实现进度监听逻辑

基于 XMLHttpRequest 的进度事件,实现带进度监听的上传函数:

const uploadWithProgress = (file: File, onProgress: UploadProgressEvent) => {
  return new Promise<string>((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    const formData = new FormData();
    
    xhr.upload.addEventListener('progress', (e) => {
      if (e.lengthComputable) {
        onProgress(file, {
          percentage: Math.round((e.loaded / e.total) * 100),
          loaded: e.loaded,
          total: e.total
        });
      }
    });
    
    xhr.open('POST', '/api/upload');
    formData.append('file', file);
    xhr.send(formData);
    
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(JSON.parse(xhr.responseText).url);
      } else {
        reject(new Error(`Upload failed: ${xhr.statusText}`));
      }
    };
  });
};

3. 设计进度展示组件

创建一个适配主题的进度指示器组件,在 packages/MdEditor/components/UploadProgress.vue 中实现:

<template>
  <div class="upload-progress" :class="theme">
    <div class="progress-bar" :style="{ width: `${percentage}%` }"></div>
    <div class="progress-text">{{ filename }} ({{ percentage }}%)</div>
  </div>
</template>

<script setup lang="ts">
import { PropType } from 'vue';
import { Themes } from '../../type';

const props = defineProps({
  filename: { type: String, required: true },
  percentage: { type: Number, default: 0 },
  theme: { type: String as PropType<Themes>, default: 'light' }
});
</script>

<style scoped>
.upload-progress {
  height: 24px;
  border-radius: 4px;
  margin: 8px 0;
  overflow: hidden;
  position: relative;
}

.light {
  background: #f5f5f5;
}

.dark {
  background: #333;
}

.progress-bar {
  height: 100%;
  transition: width 0.3s ease;
}

.light .progress-bar {
  background: #409eff;
}

.dark .progress-bar {
  background: #7cb305;
}

.progress-text {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
}

.light .progress-text {
  color: #303133;
}

.dark .progress-text {
  color: #e5e6eb;
}
</style>

集成方案:低侵入式架构设计

事件总线扩展

修改 packages/MdEditor/static/event-name.ts,增加进度事件类型:

// 新增进度事件常量
export const UPLOAD_PROGRESS = 'uploadProgress';

在内容编辑组件中监听进度事件并更新 UI:

// 在Content/index.tsx中添加进度监听
bus.on(editorId, {
  name: UPLOAD_PROGRESS,
  callback: (file, progress) => {
    // 更新进度状态
    const index = uploads.value.findIndex(item => item.file.name === file.name);
    if (index >= 0) {
      uploads.value[index].progress = progress;
    }
  }
});

状态管理设计

采用 Vue3 的响应式 API 设计上传状态管理:

// 在Content组件中管理上传状态
const uploads = ref<Array<{
  file: File;
  progress: { percentage: number; loaded: number; total: number };
  status: 'uploading' | 'success' | 'error';
}>>([]);

// 处理上传队列
const handleFiles = async (files: FileList) => {
  Array.from(files).forEach(file => {
    uploads.value.push({
      file,
      progress: { percentage: 0, loaded: 0, total: file.size },
      status: 'uploading'
    });
    
    uploadWithProgress(file, (file, progress) => {
      bus.emit(editorId, UPLOAD_PROGRESS, file, progress);
    }).then(url => {
      // 上传成功处理
      const index = uploads.value.findIndex(item => item.file.name === file.name);
      uploads.value[index].status = 'success';
      insertImages([{ url, alt: file.name, title: file.name }]);
    }).catch(() => {
      // 错误处理
      const index = uploads.value.findIndex(item => item.file.name === file.name);
      uploads.value[index].status = 'error';
    });
  });
};

高级优化:大文件上传的前端策略

分片上传实现

对于超过 10MB 的文件,实现分片上传可以显著提升成功率:

const chunkSize = 2 * 1024 * 1024; // 2MB分片

const uploadLargeFile = async (file: File, onProgress: UploadProgressEvent) => {
  const totalChunks = Math.ceil(file.size / chunkSize);
  let uploadedChunks = 0;
  
  const uploadChunk = (chunk: Blob, index: number) => {
    return new Promise<string>((resolve) => {
      const formData = new FormData();
      formData.append('file', chunk, `${file.name}.part${index}`);
      formData.append('totalChunks', totalChunks.toString());
      formData.append('chunkIndex', index.toString());
      
      // 单个分片上传逻辑...
      xhr.onload = () => {
        uploadedChunks++;
        onProgress(file, {
          percentage: Math.round((uploadedChunks / totalChunks) * 100),
          loaded: uploadedChunks * chunkSize,
          total: file.size
        });
        resolve(xhr.responseText);
      };
    });
  };
  
  // 分片处理
  const chunks = [];
  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    chunks.push(file.slice(start, end));
  }
  
  // 并行上传所有分片
  const results = await Promise.all(chunks.map((chunk, index) => 
    uploadChunk(chunk, index)
  ));
  
  // 合并分片请求
  return await fetch('/api/merge', {
    method: 'POST',
    body: JSON.stringify({
      filename: file.name,
      totalChunks,
      chunks: results
    })
  }).then(res => res.json()).then(data => data.url);
};

断点续传设计

通过 LocalStorage 记录上传进度,实现断点续传:

// 保存上传进度到本地存储
const saveUploadProgress = (file: File, progress: { percentage: number; loaded: number }) => {
  const key = `upload_${file.name}_${file.size}`;
  localStorage.setItem(key, JSON.stringify({
    lastModified: file.lastModified,
    progress,
    timestamp: Date.now()
  }));
};

// 恢复上传进度
const restoreUploadProgress = (file: File) => {
  const key = `upload_${file.name}_${file.size}`;
  const data = localStorage.getItem(key);
  if (data) {
    const { lastModified, progress } = JSON.parse(data);
    if (lastModified === file.lastModified) {
      return progress;
    }
  }
  return null;
};

主题适配:进度组件的视觉一致性

为确保进度组件在不同主题下的显示效果,需要在 packages/MdEditor/styles/vars.less 中定义主题变量:

// 浅色主题变量
@progress-bg-light: #f5f5f5;
@progress-bar-light: #409eff;
@progress-text-light: #303133;

// 深色主题变量
@progress-bg-dark: #333;
@progress-bar-dark: #7cb305;
@progress-text-dark: #e5e6eb;

在组件样式中使用主题变量:

// 进度组件主题适配
.upload-progress {
  &.light {
    background: @progress-bg-light;
    .progress-bar {
      background: @progress-bar-light;
    }
    .progress-text {
      color: @progress-text-light;
    }
  }
  
  &.dark {
    background: @progress-bg-dark;
    .progress-bar {
      background: @progress-bar-dark;
    }
    .progress-text {
      color: @progress-text-dark;
    }
  }
}

最佳实践:进度功能的集成建议

API 使用示例

在使用 md-editor-v3 时,通过 onUploadImg 回调实现进度监听:

<template>
  <MdEditor 
    v-model="content"
    @onUploadImg="handleUpload"
  />
</template>

<script setup>
const handleUpload = async (files, callback) => {
  const urls = [];
  for (const file of files) {
    const url = await myUploadService.upload(file, (progress) => {
      console.log(`Uploading ${file.name}: ${progress.percentage}%`);
      // 这里可以更新自定义进度UI
    });
    urls.push({ url, alt: file.name });
  }
  callback(urls);
};
</script>

性能优化建议

  1. 限制并发上传数量:通过队列控制同时上传的文件数量,避免浏览器连接限制

    const uploadQueue = async (files: File[], concurrency = 3) => {
      const queue = files.map(file => () => handleSingleFile(file));
      // 使用Promise.all限制并发
      while (queue.length) {
        const batch = queue.splice(0, concurrency);
        await Promise.all(batch.map(task => task()));
      }
    };
    
  2. 使用 Web Workers 处理大文件:避免分片处理阻塞主线程

  3. 实现上传取消功能:通过 AbortController 终止正在进行的上传请求

总结与展望

本文详细介绍了如何在 md-editor-v3 中实现文件上传进度可视化功能,通过扩展事件接口、设计响应式状态管理和优化大文件处理策略,显著提升了上传体验。关键收获包括:

  • 基于 Vue3 组合式 API 构建低耦合的上传模块
  • 使用 XMLHttpRequest 实现细粒度的进度监听
  • 设计适配多主题的进度展示组件
  • 实现分片上传和断点续传提升大文件上传可靠性

未来可以进一步探索:

  • 基于 WebRTC 的 P2P 文件传输功能
  • 集成 AI 辅助的图片压缩优化
  • 实现上传内容的实时预览功能

希望本文的方案能够帮助你构建更优质的编辑器体验,如果你有更好的实现思路,欢迎在评论区交流讨论!

点赞 + 收藏 + 关注,获取更多 Markdown 编辑器深度优化技巧,下期将带来「编辑器性能优化:从 10000 行文本到流畅编辑」。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值