全栈项目文件上传怎么实现?

访客 全栈框架 1

本文目录导读:

  1. 技术选型
  2. 前端实现示例
  3. 后端实现
  4. 大文件分片上传实现
  5. 最佳实践建议
  6. 完整项目结构

我来详细讲解全栈项目文件上传的实现方案,从前端到后端的完整流程。

技术选型

前端方案

  • HTTP表单上传: 传统方式,适合小文件
  • Ajax上传: 使用FormData,可以异步上传
  • 文件切片上传: 大文件分片,支持断点续传
  • 拖拽上传: 提升用户体验

后端方案

  • 直接存储: 存储到服务器本地
  • 云存储: 阿里云OSS、AWS S3等
  • 分布式存储: FastDFS、MinIO等

前端实现示例

基础HTML表单上传

<!-- 简单表单上传 -->
<form action="/api/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file" multiple>
    <button type="submit">上传</button>
</form>

现代化的Vue上传组件

<template>
  <div class="upload-container">
    <!-- 拖拽区域 -->
    <div 
      class="drop-zone"
      @drop.prevent="handleDrop"
      @dragover.prevent
      @dragenter.prevent
    >
      <p>拖拽文件到此处或点击上传</p>
      <input 
        type="file" 
        ref="fileInput" 
        @change="handleFileSelect" 
        multiple
        hidden
      >
      <button @click="$refs.fileInput.click()">选择文件</button>
    </div>
    <!-- 文件列表 -->
    <div class="file-list">
      <div v-for="file in files" :key="file.id" class="file-item">
        <span>{{ file.name }}</span>
        <div class="progress-bar">
          <div :style="{ width: file.progress + '%' }"></div>
        </div>
        <span>{{ file.progress }}%</span>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
const files = ref([])
// 选择文件处理
const handleFileSelect = (event) => {
  const selectedFiles = Array.from(event.target.files)
  selectedFiles.forEach(file => {
    files.value.push({
      id: Date.now() + Math.random(),
      file: file,
      name: file.name,
      progress: 0,
      size: file.size
    })
  })
  uploadFiles()
}
// 拖拽文件处理
const handleDrop = (event) => {
  const droppedFiles = Array.from(event.dataTransfer.files)
  droppedFiles.forEach(file => {
    files.value.push({
      id: Date.now() + Math.random(),
      file: file,
      name: file.name,
      progress: 0,
      size: file.size
    })
  })
  uploadFiles()
}
// 文件上传
const uploadFiles = async () => {
  for (let fileItem of files.value) {
    if (fileItem.progress === 0) {
      await uploadSingleFile(fileItem)
    }
  }
}
// 单文件上传
const uploadSingleFile = async (fileItem) => {
  const formData = new FormData()
  formData.append('file', fileItem.file)
  try {
    const response = await axios.post('/api/upload', formData, {
      headers: {
        'Content-Type': 'multipart/form-data'
      },
      onUploadProgress: (progressEvent) => {
        const percentCompleted = Math.round(
          (progressEvent.loaded * 100) / progressEvent.total
        )
        fileItem.progress = percentCompleted
      }
    })
    console.log('上传成功:', response.data)
    return response.data
  } catch (error) {
    console.error('上传失败:', error)
    throw error
  }
}
</script>

后端实现

Node.js (Express) 后端

// server.js
const express = require('express')
const multer = require('multer')
const path = require('path')
const fs = require('fs')
const app = express()
// 配置文件存储
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const uploadDir = 'uploads/'
    // 确保目录存在
    if (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir, { recursive: true })
    }
    cb(null, uploadDir)
  },
  filename: (req, file, cb) => {
    // 生成唯一文件名
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
    cb(null, uniqueSuffix + path.extname(file.originalname))
  }
})
// 文件过滤器
const fileFilter = (req, file, cb) => {
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true)
  } else {
    cb(new Error('不支持的文件类型'), false)
  }
}
// 创建上传中间件
const upload = multer({
  storage: storage,
  limits: {
    fileSize: 10 * 1024 * 1024 // 10MB
  },
  fileFilter: fileFilter
})
// 单文件上传接口
app.post('/api/upload', upload.single('file'), (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: '没有文件上传' })
    }
    res.json({
      message: '上传成功',
      file: {
        id: req.file.filename,
        name: req.file.originalname,
        path: req.file.path,
        size: req.file.size,
        mimeType: req.file.mimetype,
        url: `/uploads/${req.file.filename}`
      }
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})
// 多文件上传接口
app.post('/api/upload/multiple', upload.array('files', 10), (req, res) => {
  try {
    if (!req.files || req.files.length === 0) {
      return res.status(400).json({ error: '没有文件上传' })
    }
    const filesInfo = req.files.map(file => ({
      id: file.filename,
      name: file.originalname,
      size: file.size,
      url: `/uploads/${file.filename}`
    }))
    res.json({
      message: '上传成功',
      files: filesInfo
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})
// 错误处理中间件
app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    if (err.code === 'LIMIT_FILE_SIZE') {
      return res.status(400).json({ error: '文件大小超过限制' })
    }
    return res.status(400).json({ error: err.message })
  }
  res.status(500).json({ error: '服务器内部错误' })
})
app.listen(3000, () => {
  console.log('服务器启动在端口 3000')
})

Python (Flask) 后端

# app.py
from flask import Flask, request, jsonify
from werkzeug.utils import secure_filename
import os
import uuid
app = Flask(__name__)
# 配置文件上传
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB
# 确保上传目录存在
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/api/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return jsonify({'error': '没有文件上传'}), 400
    file = request.files['file']
    if file.filename == '':
        return jsonify({'error': '没有选择文件'}), 400
    if file and allowed_file(file.filename):
        # 生成唯一文件名
        filename = str(uuid.uuid4()) + '.' + file.filename.rsplit('.', 1)[1].lower()
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)
        return jsonify({
            'message': '上传成功',
            'file': {
                'id': filename,
                'name': file.filename,
                'size': os.path.getsize(filepath),
                'url': f'/uploads/{filename}'
            }
        }), 200
    return jsonify({'error': '不支持的文件类型'}), 400
if __name__ == '__main__':
    app.run(debug=True, port=3000)

大文件分片上传实现

前端切片上传

// chunkUpload.js
class ChunkUploader {
  constructor(options) {
    this.file = options.file
    this.chunkSize = options.chunkSize || 1024 * 1024 // 默认1MB
    this.chunks = []
    this.uploadedChunks = []
  }
  // 文件切片
  createChunks() {
    const chunks = []
    let start = 0
    while (start < this.file.size) {
      const end = Math.min(start + this.chunkSize, this.file.size)
      const chunk = this.file.slice(start, end)
      chunks.push({
        chunk: chunk,
        start: start,
        end: end,
        index: chunks.length
      })
      start = end
    }
    this.chunks = chunks
    return chunks
  }
  // 上传单个切片
  async uploadChunk(chunkData) {
    const formData = new FormData()
    formData.append('chunk', chunkData.chunk)
    formData.append('index', chunkData.index)
    formData.append('total', this.chunks.length)
    formData.append('filename', this.file.name)
    formData.append('fileId', this.fileId)
    try {
      const response = await axios.post('/api/upload/chunk', formData, {
        onUploadProgress: (progressEvent) => {
          // 计算总体进度
          const totalUploaded = this.uploadedChunks.reduce((sum, c) => sum + c.size, 0)
          const currentProgress = progressEvent.loaded
          const totalProgress = ((totalUploaded + currentProgress) / this.file.size) * 100
          console.log(`上传进度: ${totalProgress.toFixed(2)}%`)
        }
      })
      return response.data
    } catch (error) {
      console.error('切片上传失败:', error)
      throw error
    }
  }
  // 执行上传
  async upload() {
    this.fileId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
    // 检查是否已上传
    const checkResponse = await axios.get('/api/upload/check', {
      params: { fileId: this.fileId, filename: this.file.name }
    })
    this.uploadedChunks = checkResponse.data.uploadedChunks || []
    // 创建切片
    const chunks = this.createChunks()
    // 上传未完成的切片
    for (let chunkData of chunks) {
      if (!this.uploadedChunks.includes(chunkData.index)) {
        await this.uploadChunk(chunkData)
        this.uploadedChunks.push(chunkData.index)
      }
    }
    // 合并文件
    const mergeResponse = await axios.post('/api/upload/merge', {
      fileId: this.fileId,
      filename: this.file.name,
      totalChunks: chunks.length
    })
    return mergeResponse.data
  }
}
// 使用示例
const uploader = new ChunkUploader({
  file: selectedFile,
  chunkSize: 1024 * 1024 // 1MB
})
uploader.upload().then(result => {
  console.log('文件上传成功:', result)
}).catch(error => {
  console.error('文件上传失败:', error)
})

后端分片处理

// server-chunk.js
const express = require('express')
const multer = require('multer')
const path = require('path')
const fs = require('fs')
const router = express.Router()
// 临时存储切片
const chunkStorage = multer.diskStorage({
  destination: (req, file, cb) => {
    const chunkDir = `temp/${req.body.fileId}`
    fs.mkdirSync(chunkDir, { recursive: true })
    cb(null, chunkDir)
  },
  filename: (req, file, cb) => {
    cb(null, `chunk-${req.body.index}`)
  }
})
const uploadChunk = multer({ storage: chunkStorage })
// 检查已上传的切片
router.get('/upload/check', (req, res) => {
  const { fileId, filename } = req.query
  const chunkDir = `temp/${fileId}`
  let uploadedChunks = []
  if (fs.existsSync(chunkDir)) {
    uploadedChunks = fs.readdirSync(chunkDir).map(file => {
      return parseInt(file.split('-')[1])
    })
  }
  res.json({ uploadedChunks })
})
// 上传切片
router.post('/upload/chunk', uploadChunk.single('chunk'), (req, res) => {
  res.json({
    message: '切片上传成功',
    index: req.body.index
  })
})
// 合并切片
router.post('/upload/merge', async (req, res) => {
  const { fileId, filename, totalChunks } = req.body
  const chunkDir = `temp/${fileId}`
  const finalPath = `uploads/${fileId}-${filename}`
  try {
    // 创建写流
    const writeStream = fs.createWriteStream(finalPath)
    // 按顺序合并切片
    for (let i = 0; i < totalChunks; i++) {
      const chunkPath = path.join(chunkDir, `chunk-${i}`)
      const chunkData = fs.readFileSync(chunkPath)
      writeStream.write(chunkData)
    }
    writeStream.end()
    // 清理临时文件
    fs.rmSync(chunkDir, { recursive: true, force: true })
    res.json({
      message: '文件合并成功',
      file: {
        path: finalPath,
        name: filename,
        size: fs.statSync(finalPath).size
      }
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})
module.exports = router

最佳实践建议

安全措施

// 1. 文件类型验证
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
// 2. 文件大小限制
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
// 3. 病毒扫描(生产环境)
// 使用 ClamAV 等工具扫描上传文件
// 4. 文件名安全处理
const safeFilename = (filename) => {
  // 移除危险字符
  return filename.replace(/[^\w.-]/g, '_')
}
// 5. 访问控制
const authMiddleware = (req, res, next) => {
  // 验证用户是否有上传权限
  next()
}

性能优化

// 1. 大文件压缩
const compressImage = async (file) => {
  // 使用 sharp 或 imageMagick 压缩图片
}
// 2. CDN加速
// 上传到 CDN 而不是应用服务器
// 3. 异步处理
// 使用消息队列处理大文件
// 4. 缓存策略
// 对常用文件设置缓存

错误处理

// 统一的错误处理
const uploadErrorHandler = (error, req, res, next) => {
  console.error('上传错误:', error)
  if (error.code === 'LIMIT_FILE_SIZE') {
    return res.status(400).json({
      error: '文件大小超出限制',
      maxSize: '10MB'
    })
  }
  if (error.code === 'INVALID_FILE_TYPE') {
    return res.status(400).json({
      error: '不支持的文件类型',
      allowedTypes: ['jpg', 'png', 'pdf']
    })
  }
  res.status(500).json({
    error: '上传失败,请重试'
  })
}

完整项目结构

project/
├── frontend/
│   ├── src/
│   │   ├── components/
│   │   │   ├── FileUpload.vue
│   │   │   └── ProgressBar.vue
│   │   ├── utils/
│   │   │   └── upload.js
│   │   └── App.vue
│   └── package.json
├── backend/
│   ├── server.js
│   ├── routes/
│   │   └── upload.js
│   ├── middleware/
│   │   ├── auth.js
│   │   └── upload.js
│   ├── models/
│   │   └── File.js
│   └── package.json
└── docker-compose.yml

这个实现涵盖了文件上传的各个方面,你可以根据实际需求选择合适的方案进行实现。

标签: 文件上传

抱歉,评论功能暂时关闭!