前端上传下载文件的实现

7 阅读3分钟

前端上传下载文件处理

文件的上传和下载是前端开发中常见的操作,本文将介绍如何实现文件的上传和下载,并提供一些实用的技巧和注意事项。包括小文件上传、大文件上传、断点续传、文件下载等功能。请求使用fetch作为示例,fetch是浏览器内置的API,可用于发送HTTP请求且支持formData格式,不需要设置Content-Type,也可使用axios等库。

基础文件上传实现

基础且最常用的文件上传实现,通过input[type=file]获取文件,然后使用FormData进行上传。单个文件大小限制为5MB,支持多文件上传。使用时,需要在input标签上添加accept属性,指定支持的文件类型。使用fileFormData函数对文件进行处理,包括验证文件大小和构建FormData对象,返回的FormData对象可以直接用于上传请求,可使用fetch或axios等库进行上传。

<input type="file" id="uploader" accept=".pdf,.docx,.png" multiple />
// 获取文件
const uploader = document.getElementById("uploader");
// 触发上传
uploader.addEventListener('change', async (e) => {
  try {
    const files = Array.from(e.target.files);
    if (files.length === 0) {
      throw new Error("请选择文件");
    }
    // 处理文件
    const formData = fileFormData(files);
    // 上传逻辑...
     fetch('/api/upload', {
      method: 'POST',
      body: formData
    });
  } catch (error) {
    // 错误处理
    console.error('上传出错:', error);
    alert(`上传失败: ${error.message}`);
  }
});

// 文件处理函数
const fileFormData = (files) => {
  // 基础验证
  if (files.some((file) => file.size > 5 * 1024 * 1024)) {
    throw new Error("单个文件不能超过5MB");
  }

  // 构建FormData对象
  const formData = new FormData();
  files.forEach((file) => formData.append("files[]", file));
  return formData;
};

大文件上传实现

大文件分片上传

大文件上传通常需要进行分片处理,将大文件分成多个小文件进行上传,然后在服务端进行合并。这种方式可以有效避免大文件上传时的网络阻塞问题,提高上传效率。考虑网络中断,物理硬件出故障等问题需支持断点续传。使用时,需要在input标签上添加accept属性,指定支持的文件类型。使用chunkedUpload函数对文件进行分片处理,包括计算文件hash、获取已上传分片、上传分片、合并请求等操作。使用时,需要传入文件对象和进度回调函数。 使用checkUploadedChunks函数获取已上传分片,使用mergeChunks函数进行分片合并请求。

<input type="file" id="uploader" accept=".pdf,.docx,.png" multiple />

// 大文件分片上传

async function chunkedUpload(file, onProgress) {
  const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB分片
  const fileHash = await calculateFileHash(file); // 文件唯一标识
  
  // 获取已上传分片(断点续传)
  const uploaded = await checkUploadedChunks(fileHash);
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
  
  for (let i = 0; i < totalChunks; i++) {
    if (uploaded.includes(i)) {
     
      onProgress?.((i / totalChunks) * 100); // 更新进度
      continue; // 跳过已传分片
    }
    
    const chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
    const formData = new FormData();
    
    // 添加分片元数据 
    formData.append('file', chunk);
    formData.append('chunkNumber', i);
    formData.append('totalChunks', totalChunks);
    formData.append('identifier', fileHash);

    // 分片上传请求
    await fetch('/api/upload-chunk', {
      method: 'POST',
      body: formData
    });
    
    onProgress?.(((i + 1) / totalChunks) * 100);
  }
  
  // 合并请求
  await mergeChunks(file.name, fileHash);
}

计算文件hash是为了保证文件的唯一性,避免重复上传。使用Web Worker进行计算,可以避免阻塞主线程,提高用户体验。需要创建一个hash-worker.js文件,用于计算文件hash。

// 计算文件hash(使用Web Worker)
function calculateFileHash(file) {
  return new Promise((resolve) => {
    const worker = new Worker('/hash-worker.js');
    worker.postMessage(file);
    worker.onmessage = (e) => resolve(e.data);
    worker.onerror = (e) => {
      console.error('Hash计算错误:', e);
      resolve('fallback-hash-' + Date.now());
    };
  });
}

hash-worker.js计算文件hash

// hash-worker.js
self.onmessage = function(e) {
    const fileData = e.data;
    const reader = new FileReader();
    reader.onload = function(event) {
        const buffer = event.target.result;
        const hashBuffer = crypto.subtle.digest('SHA-256', buffer);
        hashBuffer.then(function(hash) {
            const hashArray = Array.from(new Uint8Array(hash));
            const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
            self.postMessage({hash: hashHex});
        }).catch(function(err) {
            self.postMessage({error: err});
        });
    };
    reader.readAsArrayBuffer(fileData);
};

断点续传检查,获取已上传分片。

// 断点续传检查
async function checkUploadedChunks(fileHash) {
  try {
    
    const response = await fetch(`/api/upload-status?hash=${fileHash}`);
    return await response.json();
  } catch (error) {
    console.error('获取上传状态失败:', error);
    return [];
  }
}

分片合并请求。

// 分片合并请求
async function mergeChunks(filename, fileHash) {
  try {
    await fetch('/api/merge', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        filename,
        fileHash
      })
    });
  } catch (error) {
    console.error('分片合并失败:', error);
    throw new Error('文件合并失败,请稍后重试');
  }
}

文件下载实现

一、Blob方式下载(适合小文件)

Blob方式下载适合小文件,将文件内容转换为Blob对象,然后创建一个URL对象,将Blob对象作为URL的源,然后创建一个a标签,将URL设置为a标签的href属性,设置下载文件名,然后将a标签添加到文档中,最后触发a标签的click事件,完成下载。

// Blob方式下载(适合小文件)
function downloadByBlob(content, filename) {
  try {
    const blob = new Blob([content], { type: 'application/octet-stream' });
    const url = URL.createObjectURL(blob);

    const link = document.createElement('a');
    link.href = url;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    
    // 清理
    setTimeout(() => {
      document.body.removeChild(link);
      URL.revokeObjectURL(url);
    }, 100);
  } catch (error) {
    console.error('下载失败:', error);
    throw new Error('文件下载失败');
  }
}

二、直连下载(适合大文件)

直连下载适合大文件,通过创建一个iframe元素,将文件URL设置为iframe的src属性,然后将iframe添加到文档中,完成下载。

// 直连下载(适合大文件)
function downloadDirect(url) {
  return new Promise((resolve, reject) => {
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = url;
    
    iframe.onload = () => {
      setTimeout(() => {
        document.body.removeChild(iframe);
        resolve();
      }, 5000);
    };
    
    iframe.onerror = () => {
      document.body.removeChild(iframe);
      reject(new Error('下载失败'));
    };
    
    document.body.appendChild(iframe);
  });
}

文件验证

文件类型白名单验证是指在上传文件时,对文件的类型进行验证,只允许上传指定类型的文件。这种方式可以有效防止恶意文件的上传,提高系统的安全性。使用时,需要在input标签上添加accept属性,指定支持的文件类型。使用validateFile函数对文件进行验证,包括文件类型验证和文件头验证。

// 文件类型白名单验证
const ALLOWED_TYPES = {
  'image/png': true,
  'application/pdf': true
};

const FILE_SIGNATURES = {
  '89504e47': 'image/png', // PNG
  '25504446': 'application/pdf' // PDF
};

async function validateFile(file) {
  // MIME类型验证
  if (!ALLOWED_TYPES[file.type]) {
    throw new Error('不支持的文件格式');
  }

  // 文件头验证(防御文件扩展名伪造)
  const isValid = await checkFileSignature(file);
  if (!isValid) {
    throw new Error('文件格式与扩展名不符');
  }
  return true;
}

async function checkFileSignature(file) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      const arr = new Uint8Array(e.target.result).subarray(0, 4);
      const header = Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
      resolve(!!FILE_SIGNATURES[header]);
    };
    reader.onerror = () => resolve(false);
    reader.readAsArrayBuffer(file.slice(0, 4));
  });
}