Skip to content

文件上传方案

概述

文件上传是前端开发中的常见需求,涉及文件选择、分片上传 / 断点续传 / 并发上传 / 秒传 / 进度条 / 安全与权限控制等技术点。

基础上传

1. 表单上传

html
<form action="/upload" method="POST" enctype="multipart/form-data">
  <input type="file" name="file" />
  <button type="submit">上传</button>
</form>

2. FormData 上传

javascript
async function uploadFile(file) {
  const formData = new FormData()
  formData.append('file', file)
  formData.append('userId', '123')

  const response = await fetch('/api/upload', {
    method: 'POST',
    body: formData
  })

  return response.json()
}

3. Axios 上传

javascript
import axios from 'axios'

async function uploadFile(file, onProgress) {
  const formData = new FormData()
  formData.append('file', file)

  const response = await axios.post('/api/upload', formData, {
    onUploadProgress: progressEvent => {
      const percent = Math.round(
        (progressEvent.loaded * 100) / progressEvent.total
      )
      onProgress(percent)
    }
  })

  return response.data
}

进度显示

上传进度条

vue
<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <div class="progress">
      <div class="progress-bar" :style="{ width: progress + '%' }"></div>
    </div>
    <span>{{ progress }}%</span>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const progress = ref(0)

function handleFileChange(e) {
  const file = e.target.files[0]
  uploadFile(file, percent => {
    progress.value = percent
  })
}
</script>

大文件上传

1. 文件分片

javascript
function createFileChunks(file, chunkSize = 5 * 1024 * 1024) {
  const chunks = []
  let cur = 0
  let index = 0

  while (cur < file.size) {
    const chunk = file.slice(cur, cur + chunkSize)
    chunks.push({
      index,
      chunk,
      start: cur,
      end: cur + chunkSize
    })
    cur += chunkSize
    index++
  }

  return chunks
}

2. 并发上传分片

javascript
async function uploadChunks(file, chunkSize = 5 * 1024 * 1024) {
  const chunks = createFileChunks(file, chunkSize)
  const hash = await calculateHash(file)

  const requests = chunks.map(({ index, chunk }) => {
    const formData = new FormData()
    formData.append('chunk', chunk)
    formData.append('hash', hash)
    formData.append('index', index)

    return fetch('/api/upload/chunk', {
      method: 'POST',
      body: formData
    })
  })

  await Promise.all(requests)

  // 合并分片
  await fetch('/api/upload/merge', {
    method: 'POST',
    body: JSON.stringify({ hash, total: chunks.length })
  })
}

3. 断点续传

javascript
async function resumeUpload(file) {
  const hash = await calculateHash(file)

  // 检查已上传的分片
  const response = await fetch(`/api/upload/check?hash=${hash}`)
  const { uploadedChunks } = await response.json()

  const chunks = createFileChunks(file)

  // 过滤掉已上传的分片
  const needUploadChunks = chunks.filter(
    chunk => !uploadedChunks.includes(chunk.index)
  )

  // 上传剩余分片
  for (const chunk of needUploadChunks) {
    await uploadChunk(chunk, hash)
  }
}

文件类型验证

1. 前端验证

javascript
function validateFile(file) {
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
  const maxSize = 10 * 1024 * 1024 // 10MB

  if (!allowedTypes.includes(file.type)) {
    throw new Error('不支持的文件类型')
  }

  if (file.size > maxSize) {
    throw new Error('文件大小超过限制')
  }

  return true
}

2. 文件预览

javascript
function previewFile(file) {
  return new Promise(resolve => {
    const reader = new FileReader()

    reader.onload = e => {
      resolve(e.target.result)
    }

    if (file.type.startsWith('image/')) {
      reader.readAsDataURL(file)
    } else if (file.type.startsWith('text/')) {
      reader.readAsText(file)
    }
  })
}

拖拽上传

实现拖拽区域

vue
<template>
  <div
    class="drop-zone"
    @drop.prevent="handleDrop"
    @dragover.prevent="isDragging = true"
    @dragleave="isDragging = false"
    :class="{ active: isDragging }"
  >
    <p>拖拽文件到此处上传</p>
    <input type="file" @change="handleFileChange" multiple />
  </div>
</template>

<script setup>
import { ref } from 'vue'

const isDragging = ref(false)

function handleDrop(e) {
  isDragging.value = false
  const files = e.dataTransfer.files
  uploadFiles(files)
}
</script>

<style scoped>
.drop-zone {
  border: 2px dashed #ccc;
  padding: 40px;
  text-align: center;
}

.drop-zone.active {
  border-color: #1890ff;
  background: #f0f8ff;
}
</style>

最佳实践

1. 用户体验

  • 显示上传进度
  • 支持取消上传
  • 提供错误提示
  • 支持文件预览

2. 安全性

  • 前端验证文件类型和大小
  • 后端二次验证
  • 防止恶意文件上传
  • 限制上传频率

3. 性能优化

  • 使用 Web Worker 计算文件 hash
  • 并发上传分片
  • 压缩图片后再上传
  • 使用 CDN 加速

常见问题

如何处理大文件上传超时?

  • 使用分片上传
  • 增加服务器超时时间
  • 实现断点续传

如何优化上传速度?

  • 并发上传多个分片
  • 使用 CDN 加速
  • 压缩文件后再上传

相关资源

基于 VitePress 的本地知识库