本文目录导读:
我来详细讲解全栈项目文件上传的实现方案,从前端到后端的完整流程。
技术选型
前端方案
- 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
这个实现涵盖了文件上传的各个方面,你可以根据实际需求选择合适的方案进行实现。
标签: 文件上传