广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何在Axios中模拟大文件上传进度而不依赖真实文件

文件上传组件在真实业务中往往要处理几百MB甚至几个GB的大文件,但开发阶段每次都手动选择大文件非常低效。好在浏览器的Blob和File API允许我们凭空构造一个指定大小的内存对象,再配合Axios的进度回调机制,可以完整模拟出大文件上传的进度变化。下面先展示一张模拟上传流程的示意图。

如何在Axios中模拟大文件上传进度而不依赖真实文件

模拟大文件上传的核心思路有两个:一是构造一个足够大的Blob对象让上传过程持续一段时间;二是用Axios的onUploadProgress回调或者自定义适配器来人为制造进度数据。Blob可以直接从内存中生成,不需要读写磁盘,也不会真的消耗网络带宽。在Chrome、Firefox等浏览器中,new Blob([new ArrayBuffer(1024 * 1024 * 100)])可以快速创建一个100MB的虚拟文件,把它包装成File对象后放进FormData,Axios就能像处理真实文件一样发送请求。不过要注意,有些浏览器对单个Blob块的大小有限制,建议把大块拆成多个小块拼接,例如用循环生成多个new Uint8Array(1024 * 1024)再组合。

用Blob和File构造虚拟大文件

想要模拟大文件,我们不需要真的在磁盘上创建一个文件,只需要在内存里生成一个Blob,再包装成File对象即可。Blob支持通过数组传入多个部分,这意味着我们可以用很小的代码生成任意大小的数据块。比如要模拟一个512MB的文件,可以先创建一个512MB的ArrayBuffer,但更稳妥的方式是分块填充,避免一次分配过大内存导致页面卡顿。下面的代码演示了如何生成一个256MB的虚拟文件,并把它放入FormData。

// 生成指定大小的虚拟文件
function createVirtualFile(fileName, sizeInMB) {
  const chunkSize = 1024 * 1024; // 每个块1MB
  const chunks = [];
  const totalChunks = Math.ceil(sizeInMB);
  for (let i = 0; i < totalChunks; i++) {
    // 用Uint8Array填充0,实际可以用随机数据
    chunks.push(new Uint8Array(chunkSize));
  }
  const blob = new Blob(chunks, { type: 'application/octet-stream' });
  return new File([blob], fileName, { lastModified: Date.now() });
}
const virtualFile = createVirtualFile('test-large.bin', 256);
const formData = new FormData();
formData.append('file', virtualFile);

这段代码使用1MB的块依次填充,最后生成的Blob大小为256MB。虽然数据内容全部为0,但上传时Axios仍然会按照真实字节数发送,浏览器的网络请求也会分配相应的内存。如果你希望模拟更真实的内容,可以把new Uint8Array(chunkSize)换成随机数填充,不过对于进度测试来说内容并不重要。需要注意的是,File构造函数在Node环境中不可用,如果要在Node里测试,可以直接使用Blob配合form-data库,或者用Buffer.alloc生成数据。

通过Axios的onUploadProgress获取模拟进度

Axios提供了onUploadProgress回调,它在浏览器中底层依赖于XMLHttpRequest的upload.onprogress事件。当请求体正在发送时,浏览器会根据实际发送的字节数触发进度事件。因此,只要我们把上一步构造的虚拟文件放进去,就能得到真实的进度反馈。下面的例子展示了如何在React或Vue组件中更新进度条。

import axios from 'axios';
async function uploadVirtualFile(file, onProgress) {
  const formData = new FormData();
  formData.append('file', file);
  const response = await axios.post('/api/upload', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
    onUploadProgress: (progressEvent) => {
      if (progressEvent.total) {
        const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
        onProgress(percent);
      }
    }
  });
  return response.data;
}
// 在组件中使用
uploadVirtualFile(virtualFile, (percent) => {
  console.log('上传进度:' + percent + '%');
});

进度回调中的loaded表示已经发送的字节数,total表示总字节数。对于虚拟文件来说,total就是我们构造的文件大小。不过需要注意,Axios的onUploadProgress只有在请求体较大、上传时间够长时才会多次触发,如果文件太小,进度可能直接跳到100%。为了让模拟过程更明显,可以在上传前人为限制网络速度——这可以通过自定义适配器实现,后面会详细说明。

还有一个常见问题是跨域请求时浏览器会先发送一个OPTIONS预检请求,预检请求不会触发上传进度。真正上传时进度才回调。另外,如果你在Node环境中使用Axios,onUploadProgress默认不会触发,因为Node的http模块没有上传进度事件,需要使用axios-progress-bar或者自己包装流式上传。

自定义适配器伪造分片上传进度

有时候我们并不想真的把虚拟大文件发送给服务器,只想测试前端上传组件对进度事件、暂停、取消等操作的响应。这时可以编写一个自定义Axios适配器,拦截上传请求并模拟出规律的进度变化。适配器是Axios请求管道的核心,它接收config对象并返回Promise。我们可以在适配器内部利用定时器逐步增加loaded值,并手动触发config.onUploadProgress。

// 自定义适配器:模拟分片上传进度
function mockUploadAdapter(config) {
  return new Promise((resolve, reject) => {
    const total = config.mockTotal || 100;
    const step = config.mockStep || 10;
    const interval = config.mockInterval || 200;
    let loaded = 0;
    const timer = setInterval(() => {
      loaded += step;
      if (loaded >= total) {
        loaded = total;
        clearInterval(timer);
        // 触发最终进度
        if (config.onUploadProgress) {
          config.onUploadProgress({ loaded, total, lengthComputable: true });
        }
        resolve({
          data: { success: true, message: '模拟上传完成' },
          status: 200,
          statusText: 'OK',
          headers: {},
          config
        });
      } else {
        if (config.onUploadProgress) {
          config.onUploadProgress({ loaded, total, lengthComputable: true });
        }
      }
    }, interval);
    // 支持取消
    if (config.cancelToken) {
      config.cancelToken.promise.then((cancel) => {
        clearInterval(timer);
        reject(cancel);
      });
    }
  });
}
// 使用该适配器
axios.post('/mock/upload', null, {
  adapter: mockUploadAdapter,
  mockTotal: 500,
  mockStep: 50,
  mockInterval: 300,
  onUploadProgress: (e) => {
    console.log('模拟进度:' + Math.round((e.loaded / e.total) * 100) + '%');
  }
});

这个适配器完全绕过了网络请求,纯粹在内存中模拟进度。它可以模拟分片上传的过程,每一步增加固定字节数并触发进度回调。如果需要模拟分片之间的停顿,可以在每个分片之间添加不同的延时。通过cancelToken我们还能测试取消上传的逻辑。这种方案非常适合单元测试和组件开发阶段,不需要后端配合,也不用在本地起一个真正的上传服务。

如果需要模拟网络抖动或者部分失败,可以在适配器中加入随机错误或延迟。比如在进度达到80%时故意抛出一个错误,检查前端是否正确展示重试按钮。相比真实上传,这种方式能覆盖更多边界情况,而且执行速度极快。

使用Service Worker拦截请求实现持久化模拟

除了自定义适配器,另一种思路是在Service Worker中拦截上传请求,返回伪造的响应。这种方式的好处是组件代码完全不用改动,依然使用标准的Axios请求接口。Service Worker可以监听fetch事件,识别上传请求,然后返回一个合成响应。由于Service Worker运行在独立线程,它可以通过ReadableStream逐步返回响应体,模拟真实的上传过程。

// service-worker.js
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  if (url.pathname === '/api/upload') {
    event.respondWith(mockUploadResponse(event.request));
  }
});
async function mockUploadResponse(request) {
  const total = Number(request.headers.get('x-mock-total') || 200);
  const encoder = new TextEncoder();
  let loaded = 0;
  const stream = new ReadableStream({
    async start(controller) {
      // 这里实际上无法直接控制请求体上传进度,但可以模拟响应体
      for (let i = 0; i < total; i += 20) {
        controller.enqueue(encoder.encode('chunk'));
        await new Promise(r => setTimeout(r, 100));
      }
      controller.close();
    }
  });
  const response = new Response(stream, {
    status: 200,
    headers: { 'Content-Type': 'application/json' }
  });
  return response;
}

需要说明的是,Service Worker拦截fetch请求时,请求体已经完整到达了浏览器网络层,我们无法直接控制上传进度事件。但可以通过响应体的流式返回模拟服务器处理大文件的延迟,这对于测试上传后服务器处理进度(如转码、校验)非常有用。真正的上传进度依然要靠前面的Blob构造方案。Service Worker方案更适合模拟大文件上传后的后续处理流程,让前端在等待服务器响应时能看到持续的进度动画。

结合以上几种方法,你可以在不生成真实大文件的情况下,完整测试前端上传组件从选择文件、上传进度、暂停取消到最终完成的全部交互。Blob构造法用于真实发送大文件但避开磁盘IO;自定义适配器用于纯前端逻辑验证;Service Worker用于模拟后端处理延迟。开发者可以根据测试目标灵活选择,大幅提升上传功能的开发效率。

广告位 · 文章底部(后台-模块管理替换此区域)