加载中...

图片批量上传


一、使用场景

1. 电商平台

  • 商家批量上传商品图片,如不同角度的商品展示图、规格图等。
  • 在编辑商品时上传多张图片方便商品展示,提升用户体验。

2. 社交媒体和内容平台

  • 用户上传多张照片、视频或文件分享个人动态或相册。
  • 在图片分享和内容管理平台中,批量上传便于用户一次性上传多张图片,提升操作便>捷性。

3. 内容管理系统(CMS)

  • 站点管理者上传多媒体资源(图片、视频、文件等)以供网站使用。
  • 批量上传可以帮助管理员高效地更新图片库和资源库。

4. 企业内部管理系统

  • 批量上传员工证件照、身份证扫描件等,方便快速生成档案。
  • 上传多个项目文件(如图纸、照片等)用于项目汇报、记录等需求。

5. 教育平台

  • 教师上传多张课件图片、讲义或作业参考材料供学生参考。
  • 学生上传多个作业文件,如笔记、照片、实验图片等。

6. 房地产、旅游等行业平台

  • 房地产平台上传房源照片,如房间的各个角度、社区环境等。
  • 旅游平台上传景点照片、活动照片等,展示更丰富的视觉内容。

二、难点

批量上传的目的是提高用户上传大量文件的效率,并通过适当的并发控制避免对服务器造成负担

1. 并发控制和性能优化

  • 并发限制:批量上传如果没有控制上传并发数量,容易引发请求暴增,导致服务>器负载过高,可能造成服务器响应慢甚至宕机。
  • 文件大小限制:大图片文件的传输时间较长,会导致上传耗时过久,影响用户体>验。需要对大文件进行压缩或分片处理。

2. 文件预处理

  • 图片压缩:在上传前对图片进行压缩,以减少上传时间和服务器存储空间。但压>缩处理会占用前端资源,可能造成卡顿。
  • 格式转换:不同的图片格式可能需要在上传前进行转换(如将HEIC转换为>JPEG),以提高兼容性,这通常需要使用Web Worker来避免阻塞UI线程。

3. 上传进度管理

  • 进度展示:用户体验中实时显示上传进度是非常重要的,特别是批量上传的场>景。需要管理多个图片的进度,更新UI展示上传百分比或进度条。
  • 断点续传:如果上传中断(如网络不稳定或中途关闭页面),需支持断点续传,>以确保用户不会因一次失败重新上传所有文件。

4. 错误处理与重试机制

  • 失败重试:上传过程中可能遇到网络波动、请求失败、文件格式不符合等问题。>批量上传需要提供合理的重试机制或失败提示,让用户选择是否重新上传。
  • 错误回调:批量上传涉及多个文件时,每个文件可能会遇到不同的上传错误,需>要有精细的错误回调来区分错误类型并进行相应处理。

三、具体的代码

第一点优化

  • 并发限制:控制同时上传的图片数量,避免服务器压力过大。
  • 文件大小限制:在上传前检查文件大小,超出限制则阻止上传。
<template>
  <div>
    <el-upload
      ref="uploadRef"
      :http-request="customHttpRequest"
      :on-change="handleFileChange"
      :before-upload="beforeUpload"
      :file-list="fileList"
      multiple
      :limit="10"
      :auto-upload="false"
      >
      <el-button type="primary">批量上传图片</el-button>
    </el-upload>
    <div>
      <el-button type="primary" @click="startUpload">开始上传</el-button>
    </div>
  </div>
</template>

<script setup>
  import { ref } from 'vue';
  import { ElButton, ElUpload, ElMessage } from 'element-plus';

  const fileList = ref([]);
  const maxConcurrentUploads = 3; // 最大并发上传数量
  const maxSizeInMB = 2; // 文件大小限制,单位:MB
  let uploadQueue = [];
  let currentUploads = 0;

  const handleFileChange = (file, files) => {
    // 将文件加入到队列中
    uploadQueue.push(file);
  };

  // 文件大小限制检查
  const beforeUpload = (file) => {
    const isUnderLimit = file.size / 1024 / 1024 < maxSizeInMB;
    if (!isUnderLimit) {
      ElMessage.error(`文件 ${file.name} 超出大小限制(最大 ${maxSizeInMB} MB)`);
    }
    return isUnderLimit;
  };

  // 控制并发上传,限制同时上传数量
  const customHttpRequest = (options) => {
    if (currentUploads >= maxConcurrentUploads) return;

    currentUploads++;
    const { file, onProgress, onSuccess, onError } = options;

    // 创建XMLHttpRequest并配置上传进度
    const xhr = new XMLHttpRequest();
    xhr.open('POST', options.action, true);

    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        const progress = (event.loaded / event.total) * 100;
        onProgress({ percent: progress });
      }
    };

    xhr.onload = () => {
      currentUploads--;
      processQueue();
      onSuccess(xhr.response);
    };

    xhr.onerror = () => {
      currentUploads--;
      processQueue();
      onError(xhr.response);
    };

    const formData = new FormData();
    formData.append('file', file);
    xhr.send(formData);
  };

  // 处理队列,限制同时上传数量
  const processQueue = () => {
    while (uploadQueue.length > 0 && currentUploads < maxConcurrentUploads) {
      const file = uploadQueue.shift();
      customHttpRequest({
        action: 'https://your-upload-endpoint',
        file,
        onProgress: (event) => console.log('progress:', event.percent),
        onSuccess: () => ElMessage.success(`文件 ${file.name} 上传成功`),
        onError: () => ElMessage.error(`文件 ${file.name} 上传失败`),
      });
    }
  };

  // 开始上传
  const startUpload = () => {
    processQueue();
  };
</script>
  • 文件大小检查beforeUpload函数会在文件加入队列前判断文件大小是否超过maxSizeInMB限制,如果超出则阻止上传,并给出提示信息。
  • 并发上传限制
  • 定义了maxConcurrentUploads限制同时上传的文件数量。
  • uploadQueue用于保存待上传的文件队列,currentUploads记录当前上传中的>文件数量。
  • customHttpRequest是自定义的上传请求,在每次上传完毕后递减>currentUploads,并调用processQueue继续处理队列中的文件。
  • processQueue函数会检查uploadQueue并发起新的上传请求,确保不会超过并发上传限制。

第二点优化

使用Web Worker对图片文件在上传前进行压缩格式转换。通过Web Worker来处理这些耗时操作,避免主线程阻塞,提升用户体验。

创建一个Web Worker文件 imageWorker.js,用于处理图片的压缩和格式转换操作。使用Canvas API实现图片压缩,并将图片格式转换为JPEGPNG等常见格式。

// imageWorker.js
self.onmessage = async function (e) {
  const { file, quality, targetFormat } = e.data;

  const compressImage = async (file, quality, format) => {
    return new Promise((resolve) => {
      const img = new Image();
      img.src = URL.createObjectURL(file);
      img.onload = () => {
        const canvas = document.createElement('canvas');
        canvas.width = img.width;
        canvas.height = img.height;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, img.width, img.height);

        // 设置格式和质量,转换图片
        canvas.toBlob(
          (blob) => {
            const compressedFile = new File([blob], file.name, {
              type: `image/${format}`,
              lastModified: Date.now(),
            });
            resolve(compressedFile);
          },
          `image/${format}`,
          quality
        );
      };
    });
  };

  // 调用压缩方法
  const processedFile = await compressImage(file, quality, targetFormat);
  self.postMessage(processedFile);
};

Vue 组件中使用 Web Worker 进行图片预处理

<template>
  <div>
    <el-upload
      ref="uploadRef"
      :http-request="customHttpRequest"
      :on-change="handleFileChange"
      :before-upload="beforeUpload"
      :file-list="fileList"
      multiple
      :limit="10"
      :auto-upload="false"
    >
      <el-button type="primary">批量上传图片</el-button>
    </el-upload>
    <div>
      <el-button type="primary" @click="startUpload">开始上传</el-button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';

const fileList = ref([]);
const maxConcurrentUploads = 3; // 最大并发上传数
let uploadQueue = [];
let currentUploads = 0;
let worker;

const handleFileChange = (file, files) => {
  uploadQueue.push(file);
};

// 初始化 Web Worker
onMounted(() => {
  worker = new Worker(new URL('./imageWorker.js', import.meta.url));
});

onUnmounted(() => {
  if (worker) worker.terminate();
});

// 图片预处理,压缩和格式转换
const beforeUpload = (file) => {
  return new Promise((resolve) => {
    worker.postMessage({
      file,
      quality: 0.7, // 图片压缩质量,0到1之间
      targetFormat: 'jpeg', // 目标格式,可以是 'jpeg' 或 'png'
    });

    // 监听 Web Worker 返回的压缩文件
    worker.onmessage = (e) => {
      const processedFile = e.data;
      resolve(processedFile); // 返回压缩后的文件
    };
  });
};

// 自定义上传请求,限制并发数量
const customHttpRequest = (options) => {
  if (currentUploads >= maxConcurrentUploads) return;

  currentUploads++;
  const { file, onProgress, onSuccess, onError } = options;

  const xhr = new XMLHttpRequest();
  xhr.open('POST', options.action, true);

  xhr.upload.onprogress = (event) => {
    if (event.lengthComputable) {
      const progress = (event.loaded / event.total) * 100;
      onProgress({ percent: progress });
    }
  };

  xhr.onload = () => {
    currentUploads--;
    processQueue();
    onSuccess(xhr.response);
  };

  xhr.onerror = () => {
    currentUploads--;
    processQueue();
    onError(xhr.response);
  };

  const formData = new FormData();
  formData.append('file', file);
  xhr.send(formData);
};

// 处理队列,限制同时上传数量
const processQueue = () => {
  while (uploadQueue.length > 0 && currentUploads < maxConcurrentUploads) {
    const file = uploadQueue.shift();
    customHttpRequest({
      action: 'https://your-upload-endpoint',
      file,
      onProgress: (event) => console.log('progress:', event.percent),
      onSuccess: () => ElMessage.success(`文件 ${file.name} 上传成功`),
      onError: () => ElMessage.error(`文件 ${file.name} 上传失败`),
    });
  }
};

// 开始上传
const startUpload = () => {
  processQueue();
};
</script>
  • Web Worker压缩和格式转换imageWorker.js文件中,使用Canvas API>压缩图片并转换格式,通过postMessage返回处理后的文件。
  • compressImage函数将图片绘制到Canvas上,并将其转换为指定的格式和质量。
  • Vue组件中使用 Web Worker
  • beforeUpload钩子中,图片会传入Web Worker进行预处理(压缩和格式转>换)。
  • worker.onmessage监听预处理完成后的文件,并将其加入到上传队列。
  • 自定义上传和并发控制:通过customHttpRequestprocessQueue方法,>控制同时上传的数量,确保不会超出maxConcurrentUploads的限制。

第三点优化

实现上传进度管理的UI展示断点续传多文件进度管理,我们可以做以下几项优化:

  1. 上传进度展示:为每张图片添加进度条,实时显示上传进度。
  2. 断点续传:对已上传的数据做断点标记,当上传中断时可以从中断的部分继续上传。
  3. 多文件进度管理:管理每个文件的上传进度状态,并在UI上展示。
<template>
  <div>
    <el-upload
      ref="uploadRef"
      :http-request="customHttpRequest"
      :on-change="handleFileChange"
      :before-upload="beforeUpload"
      :file-list="fileList"
      multiple
      :auto-upload="false"
    >
      <el-button type="primary">批量上传图片</el-button>
    </el-upload>
    <div>
      <el-button type="primary" @click="startUpload">开始上传</el-button>
    </div>
    <div v-for="(file, index) in uploadStatus" :key="file.uid" class="upload-item">
      <span>{{ file.name }}</span>
      <el-progress :percentage="file.progress" v-if="file.status === 'uploading'" />
      <span v-if="file.status === 'completed'">上传完成</span>
      <span v-if="file.status === 'failed'">上传失败</span>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';

const fileList = ref([]);
const uploadStatus = ref([]);
const maxConcurrentUploads = 3; // 最大并发上传数
let uploadQueue = [];
let currentUploads = 0;
let worker;

const handleFileChange = (file) => {
  uploadQueue.push(file);
  uploadStatus.value.push({
    uid: file.uid,
    name: file.name,
    progress: 0,
    status: 'pending',
  });
};

// 文件预处理,压缩和格式转换
const beforeUpload = (file) => {
  return new Promise((resolve) => {
    worker.postMessage({
      file,
      quality: 0.7,
      targetFormat: 'jpeg',
    });

    worker.onmessage = (e) => {
      const processedFile = e.data;
      resolve(processedFile);
    };
  });
};

// 自定义上传请求,限制并发数量,支持断点续传
const customHttpRequest = (options) => {
  if (currentUploads >= maxConcurrentUploads) return;

  currentUploads++;
  const { file, onProgress, onSuccess, onError } = options;
  const storedProgress = localStorage.getItem(`upload-progress-${file.uid}`) || 0;
  let uploadedBytes = parseInt(storedProgress, 10);

  // 上传进度更新
  const updateProgress = (event) => {
    const progress = ((uploadedBytes + event.loaded) / file.size) * 100;
    const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
    if (fileStatus) fileStatus.progress = progress;
    onProgress({ percent: progress });
    localStorage.setItem(`upload-progress-${file.uid}`, uploadedBytes + event.loaded);
  };

  // 自定义分块上传实现断点续传
  const chunkSize = 1024 * 1024; // 1MB的分块大小
  const totalChunks = Math.ceil(file.size / chunkSize);
  let currentChunk = Math.floor(uploadedBytes / chunkSize);

  const uploadChunk = () => {
    if (currentChunk >= totalChunks) {
      localStorage.removeItem(`upload-progress-${file.uid}`);
      currentUploads--;
      processQueue();
      onSuccess();
      const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
      if (fileStatus) fileStatus.status = 'completed';
      return;
    }

    const start = currentChunk * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    const formData = new FormData();
    formData.append('file', chunk);
    formData.append('filename', file.name);
    formData.append('chunkNumber', currentChunk);
    formData.append('totalChunks', totalChunks);

    const xhr = new XMLHttpRequest();
    xhr.open('POST', options.action, true);

    xhr.upload.onprogress = updateProgress;
    xhr.onload = () => {
      uploadedBytes += chunk.size;
      currentChunk++;
      uploadChunk();
    };
    xhr.onerror = () => {
      currentUploads--;
      const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
      if (fileStatus) fileStatus.status = 'failed';
      onError();
    };

    xhr.send(formData);
  };

  uploadChunk();
};

// 处理队列,限制同时上传数量
const processQueue = () => {
  while (uploadQueue.length > 0 && currentUploads < maxConcurrentUploads) {
    const file = uploadQueue.shift();
    const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
    if (fileStatus) fileStatus.status = 'uploading';

    customHttpRequest({
      action: 'https://your-upload-endpoint',
      file,
      onProgress: (event) => console.log('progress:', event.percent),
      onSuccess: () => ElMessage.success(`文件 ${file.name} 上传成功`),
      onError: () => ElMessage.error(`文件 ${file.name} 上传失败`),
    });
  }
};

// 开始上传
const startUpload = () => {
  processQueue();
};

// 初始化 Web Worker
onMounted(() => {
  worker = new Worker(new URL('./imageWorker.js', import.meta.url));
});

onUnmounted(() => {
  if (worker) worker.terminate();
});
</script>

<style>
.upload-item {
  display: flex;
  align-items: center;
  margin-top: 10px;
}
</style>
  • 断点续传:我们将每个文件按1MB大小分块上传。localStorage中记录上传进>度,当网络中断或页面关闭后,可以从最后一次成功上传的分块继续。
  • customHttpRequest方法根据存储的进度决定从哪个分块开始上传。
  • 每完成一块上传,更新uploadedBytes并保存到localStorage中,以便下次继>续上传。
  • 上传进度管理
  • uploadStatus数组用于跟踪每个文件的上传状态和进度。
  • 每次分块上传进度通过onProgress事件更新。
  • 通过el-progress展示每个文件的上传进度,更新UI。
  • 错误处理
  • 如果某个分块上传失败,标记为failed并展示在UI中,用户可以选择手动重试。

第四点优化

  • 错误提示:为每个文件记录错误信息并展示在UI上。
  • 错误分类和重试:实现精细化的错误回调,通过retryCount控制每个文件的重试次数。如果超过最大重试次数,则提供手动重试选项。
  • 重试按钮:在上传失败的文件上显示“重试”按钮,让用户在手动点击时可以重新上传该文件。
<template>
  <div>
    <el-upload
      ref="uploadRef"
      :http-request="customHttpRequest"
      :on-change="handleFileChange"
      :before-upload="beforeUpload"
      :file-list="fileList"
      multiple
      :auto-upload="false"
    >
      <el-button type="primary">批量上传图片</el-button>
    </el-upload>
    <div>
      <el-button type="primary" @click="startUpload">开始上传</el-button>
    </div>
    <div v-for="(file, index) in uploadStatus" :key="file.uid" class="upload-item">
      <span>{{ file.name }}</span>
      <el-progress :percentage="file.progress" v-if="file.status === 'uploading'" />
      <span v-if="file.status === 'completed'">上传完成</span>
      <span v-if="file.status === 'failed'" class="error-message">
        上传失败:{{ file.error }}&nbsp;
        <el-button type="text" @click="retryUpload(file)">重试</el-button>
      </span>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';

const fileList = ref([]);
const uploadStatus = ref([]);
const maxConcurrentUploads = 3;
const maxRetries = 3; // 最大重试次数
let uploadQueue = [];
let currentUploads = 0;
let worker;

const handleFileChange = (file) => {
  uploadQueue.push(file);
  uploadStatus.value.push({
    uid: file.uid,
    name: file.name,
    progress: 0,
    status: 'pending',
    error: null,
    retryCount: 0,
  });
};

const beforeUpload = (file) => {
  return new Promise((resolve) => {
    worker.postMessage({
      file,
      quality: 0.7,
      targetFormat: 'jpeg',
    });

    worker.onmessage = (e) => {
      const processedFile = e.data;
      resolve(processedFile);
    };
  });
};

// 自定义上传请求,限制并发数量,支持断点续传和错误处理
const customHttpRequest = (options) => {
  if (currentUploads >= maxConcurrentUploads) return;

  currentUploads++;
  const { file, onProgress, onSuccess, onError } = options;
  const storedProgress = localStorage.getItem(`upload-progress-${file.uid}`) || 0;
  let uploadedBytes = parseInt(storedProgress, 10);

  // 上传进度更新
  const updateProgress = (event) => {
    const progress = ((uploadedBytes + event.loaded) / file.size) * 100;
    const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
    if (fileStatus) fileStatus.progress = progress;
    onProgress({ percent: progress });
    localStorage.setItem(`upload-progress-${file.uid}`, uploadedBytes + event.loaded);
  };

  // 自定义分块上传实现断点续传
  const chunkSize = 1024 * 1024; // 1MB的分块大小
  const totalChunks = Math.ceil(file.size / chunkSize);
  let currentChunk = Math.floor(uploadedBytes / chunkSize);

  const uploadChunk = () => {
    if (currentChunk >= totalChunks) {
      localStorage.removeItem(`upload-progress-${file.uid}`);
      currentUploads--;
      processQueue();
      onSuccess();
      const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
      if (fileStatus) {
        fileStatus.status = 'completed';
        fileStatus.error = null;
      }
      return;
    }

    const start = currentChunk * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    const formData = new FormData();
    formData.append('file', chunk);
    formData.append('filename', file.name);
    formData.append('chunkNumber', currentChunk);
    formData.append('totalChunks', totalChunks);

    const xhr = new XMLHttpRequest();
    xhr.open('POST', options.action, true);

    xhr.upload.onprogress = updateProgress;
    xhr.onload = () => {
      uploadedBytes += chunk.size;
      currentChunk++;
      uploadChunk();
    };
    xhr.onerror = () => handleUploadError(file);
    xhr.send(formData);
  };

  uploadChunk();
};

// 处理上传错误,重试或记录错误
const handleUploadError = (file) => {
  const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
  if (fileStatus.retryCount < maxRetries) {
    fileStatus.retryCount++;
    ElMessage.warning(`文件 ${file.name} 上传失败,重试第 ${fileStatus.retryCount} 次`);
    customHttpRequest({
      action: 'https://your-upload-endpoint',
      file,
      onProgress: (event) => console.log('progress:', event.percent),
      onSuccess: () => ElMessage.success(`文件 ${file.name} 上传成功`),
      onError: () => handleUploadError(file),
    });
  } else {
    fileStatus.status = 'failed';
    fileStatus.error = '网络错误或服务器问题,上传失败';
    ElMessage.error(`文件 ${file.name} 上传失败,请检查网络或稍后重试`);
  }
};

// 重试上传
const retryUpload = (file) => {
  const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
  fileStatus.retryCount = 0;
  fileStatus.status = 'uploading';
  fileStatus.error = null;
  customHttpRequest({
    action: 'https://your-upload-endpoint',
    file,
    onProgress: (event) => console.log('progress:', event.percent),
    onSuccess: () => ElMessage.success(`文件 ${file.name} 上传成功`),
    onError: () => handleUploadError(file),
  });
};

// 处理队列,限制同时上传数量
const processQueue = () => {
  while (uploadQueue.length > 0 && currentUploads < maxConcurrentUploads) {
    const file = uploadQueue.shift();
    const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
    if (fileStatus) fileStatus.status = 'uploading';

    customHttpRequest({
      action: 'https://your-upload-endpoint',
      file,
      onProgress: (event) => console.log('progress:', event.percent),
      onSuccess: () => ElMessage.success(`文件 ${file.name} 上传成功`),
      onError: () => handleUploadError(file),
    });
  }
};

// 开始上传
const startUpload = () => {
  processQueue();
};

// 初始化 Web Worker
onMounted(() => {
  worker = new Worker(new URL('./imageWorker.js', import.meta.url));
});

onUnmounted(() => {
  if (worker) worker.terminate();
});
</script>

<style>
.upload-item {
  display: flex;
  align-items: center;
  margin-top: 10px;
}
.error-message {
  color: red;
  font-weight: bold;
}
</style>
  1. 错误处理和重试逻辑
    • handleUploadError 方法对上传失败的文件进行错误处理。若retryCount小于maxRetries,则自动重试。
    • 超过最大重试次数时,将文件状态更新为failed并记录错误信息。
  2. 重试按钮
    • 在文件状态为failed时,显示“重试”按钮,用户可手动点击重新上传。
    • retryUpload 方法重置重试次数,并重新调用customHttpRequest来重新上传失败文件。
  3. 上传进度和状态管理
    • 每个文件的状态在uploadStatus中管理,状态包括pendinguploadingcompletedfailed,UI根据状态更新显示。
  4. 精细的错误信息展示
    • 每个文件错误类型独立记录并展示在UI中,避免因多个文件错误导致混淆。

这样可以更好地支持批量上传过程中各个文件的精细管理、进度监控以及错误处理。

四、node后端接口服务

mkdir koa-file-upload-demo
cd koa-file-upload-demo
npm init -y

pnpm install koa koa-router koa-body fs-extra

在项目文件夹中创建 app.js 文件并添加以下代码。

// app.js
const Koa = require('koa');
const Router = require('koa-router');
const koaBody = require('koa-body');
const fs = require('fs-extra');
const path = require('path');

const app = new Koa();
const router = new Router();

const UPLOAD_DIR = path.resolve(__dirname, 'uploads'); // 上传文件存储目录
fs.ensureDirSync(UPLOAD_DIR); // 确保上传目录存在

// 合并分块文件
async function mergeChunks(filename, totalChunks) {
  const filePath = path.join(UPLOAD_DIR, filename);
  const chunkDir = path.join(UPLOAD_DIR, `${filename}_chunks`);

  await fs.ensureFile(filePath); // 确保目标文件存在
  const writeStream = fs.createWriteStream(filePath);

  for (let i = 0; i < totalChunks; i++) {
    const chunkPath = path.join(chunkDir, `${filename}-chunk-${i}`);
    const data = await fs.readFile(chunkPath);
    writeStream.write(data);
    await fs.remove(chunkPath); // 清理分块文件
  }

  writeStream.end();
  await fs.rmdir(chunkDir); // 删除分块文件夹
}

// 文件上传接口
router.post('/upload', koaBody({ multipart: true }), async (ctx) => {
  const { chunkNumber, totalChunks, filename } = ctx.request.body;
  const file = ctx.request.files.file;
  const chunkDir = path.join(UPLOAD_DIR, `${filename}_chunks`);

  await fs.ensureDir(chunkDir); // 确保分块目录存在

  // 将文件分块存储在指定目录下
  const chunkPath = path.join(chunkDir, `${filename}-chunk-${chunkNumber}`);
  await fs.move(file.path, chunkPath);

  // 当所有分块上传完毕后进行合并
  if (parseInt(chunkNumber) + 1 === parseInt(totalChunks)) {
    await mergeChunks(filename, totalChunks);
    ctx.body = { message: 'File upload complete', url: `/uploads/${filename}` };
  } else {
    ctx.body = { message: `Chunk ${chunkNumber} uploaded` };
  }
});

// 静态文件服务
router.get('/uploads/:filename', async (ctx) => {
  const filePath = path.join(UPLOAD_DIR, ctx.params.filename);
  if (fs.existsSync(filePath)) {
    ctx.set('Content-Disposition', `attachment; filename=${ctx.params.filename}`);
    ctx.body = fs.createReadStream(filePath);
  } else {
    ctx.status = 404;
    ctx.body = { message: 'File not found' };
  }
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});
  1. 合并分块
    • mergeChunks 方法将所有上传的分块按照顺序合并成完整的文件。
    • 合并后删除临时的分块文件和分块目录。
  2. 文件上传
    • /upload 路由接收文件分块上传请求,处理 chunkNumbertotalChunks 信息。
    • 每个分块被存储在一个临时文件夹中,命名格式为 ${filename}_chunks
    • 每上传一个分块后,后端会返回上传的分块编号。如果检测到当前分块是最后一个分块,后端将调用 mergeChunks 方法进行文件合并。
  3. 静态文件服务
    • /uploads/:filename 路由用于访问上传后的文件,可用于测试文件是否上传成功。

在终端中运行以下命令启动服务器:

node app.js

服务器启动后,访问 http://localhost:3000

前端代码中的上传接口地址设置为 http://localhost:3000/upload


文章作者: LYF
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LYF !
评论
  目录