Koa 核心不包含文件上传功能,需要通过中间件实现。最常用的文件上传中间件是 koa-body 或 koa-multer,它们提供了强大的文件上传处理能力。
1. 使用 koa-body 处理文件上传:
安装:
bashnpm install koa-body
基本配置:
javascriptconst koaBody = require('koa-body'); app.use(koaBody({ multipart: true, // 启用文件上传 formidable: { maxFileSize: 100 * 1024 * 1024, // 最大文件大小 100MB keepExtensions: true, // 保留文件扩展名 uploadDir: './uploads', // 上传目录 multiples: true // 支持多文件上传 } }));
单文件上传:
javascriptapp.use(async (ctx) => { const file = ctx.request.files.file; if (!file) { ctx.throw(400, 'No file uploaded'); } // 获取文件信息 const fileInfo = { name: file.name, size: file.size, path: file.path, type: file.type, lastModifiedDate: file.lastModifiedDate }; ctx.body = { message: 'File uploaded successfully', file: fileInfo }; });
多文件上传:
javascriptapp.use(async (ctx) => { const files = ctx.request.files.files; if (!files) { ctx.throw(400, 'No files uploaded'); } // 处理单个文件或多个文件 const fileList = Array.isArray(files) ? files : [files]; const uploadedFiles = fileList.map(file => ({ name: file.name, size: file.size, path: file.path, type: file.type })); ctx.body = { message: `${uploadedFiles.length} files uploaded`, files: uploadedFiles }; });
2. 使用 koa-multer 处理文件上传:
安装:
bashnpm install koa-multer
基本配置:
javascriptconst multer = require('koa-multer'); // 存储配置 const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, './uploads/'); }, filename: function (req, file, cb) { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); } }); const upload = multer({ storage: storage, limits: { fileSize: 100 * 1024 * 1024 // 100MB }, fileFilter: function (req, file, cb) { // 文件类型过滤 const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Invalid file type'), false); } } });
单文件上传:
javascriptapp.use(upload.single('file')); app.use(async (ctx) => { const file = ctx.req.file; ctx.body = { message: 'File uploaded successfully', file: { originalname: file.originalname, filename: file.filename, path: file.path, size: file.size, mimetype: file.mimetype } }; });
多文件上传:
javascript// 最多上传 10 个文件 app.use(upload.array('files', 10)); app.use(async (ctx) => { const files = ctx.req.files; ctx.body = { message: `${files.length} files uploaded`, files: files.map(file => ({ originalname: file.originalname, filename: file.filename, path: file.path, size: file.size, mimetype: file.mimetype })) }; });
混合上传(文件 + 字段):
javascriptapp.use(upload.fields([ { name: 'avatar', maxCount: 1 }, { name: 'documents', maxCount: 5 } ])); app.use(async (ctx) => { const files = ctx.req.files; const body = ctx.req.body; ctx.body = { message: 'Files uploaded successfully', avatar: files.avatar[0], documents: files.documents, data: body }; });
3. 文件上传安全措施:
javascriptconst path = require('path'); const fs = require('fs'); app.use(koaBody({ multipart: true, formidable: { maxFileSize: 10 * 1024 * 1024, // 限制文件大小 keepExtensions: true, uploadDir: './uploads', filter: function ({ name, originalFilename, mimetype }) { // 文件类型验证 const allowedTypes = [ 'image/jpeg', 'image/png', 'image/gif', 'application/pdf' ]; return allowedTypes.includes(mimetype); } } })); // 文件验证中间件 async function validateFile(ctx, next) { const file = ctx.request.files.file; if (!file) { ctx.throw(400, 'No file uploaded'); } // 验证文件大小 const maxSize = 10 * 1024 * 1024; // 10MB if (file.size > maxSize) { // 删除已上传的文件 fs.unlinkSync(file.path); ctx.throw(400, 'File size exceeds limit'); } // 验证文件类型 const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']; if (!allowedTypes.includes(file.type)) { fs.unlinkSync(file.path); ctx.throw(400, 'Invalid file type'); } // 验证文件扩展名 const ext = path.extname(file.name).toLowerCase(); const allowedExts = ['.jpg', '.jpeg', '.png', '.gif']; if (!allowedExts.includes(ext)) { fs.unlinkSync(file.path); ctx.throw(400, 'Invalid file extension'); } await next(); } app.use(validateFile);
4. 图片处理:
使用 sharp 库处理上传的图片。
bashnpm install sharp
javascriptconst sharp = require('sharp'); app.use(async (ctx) => { const file = ctx.request.files.file; if (!file) { ctx.throw(400, 'No file uploaded'); } // 生成缩略图 const thumbnailPath = file.path.replace(/(\.[\w\d]+)$/, '_thumb$1'); await sharp(file.path) .resize(200, 200, { fit: 'cover', position: 'center' }) .toFile(thumbnailPath); // 压缩图片 const compressedPath = file.path.replace(/(\.[\w\d]+)$/, '_compressed$1'); await sharp(file.path) .jpeg({ quality: 80 }) .toFile(compressedPath); ctx.body = { message: 'Image processed successfully', original: file.path, thumbnail: thumbnailPath, compressed: compressedPath }; });
5. 分片上传:
对于大文件,实现分片上传功能。
javascriptconst fs = require('fs'); const path = require('path'); app.use(async (ctx) => { const { chunkIndex, totalChunks, fileId } = ctx.request.body; const file = ctx.request.files.chunk; const chunkDir = path.join('./uploads', fileId); const chunkPath = path.join(chunkDir, `chunk_${chunkIndex}`); // 创建分片目录 if (!fs.existsSync(chunkDir)) { fs.mkdirSync(chunkDir, { recursive: true }); } // 保存分片 const reader = fs.createReadStream(file.path); const writer = fs.createWriteStream(chunkPath); reader.pipe(writer); // 检查是否所有分片都已上传 const uploadedChunks = fs.readdirSync(chunkDir).length; if (uploadedChunks === parseInt(totalChunks)) { // 合并分片 const finalPath = path.join('./uploads', `${fileId}${path.extname(file.name)}`); 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); fs.unlinkSync(chunkPath); } writeStream.end(); fs.rmdirSync(chunkDir); ctx.body = { message: 'File upload completed', path: finalPath }; } else { ctx.body = { message: `Chunk ${chunkIndex} uploaded`, progress: `${uploadedChunks}/${totalChunks}` }; } });
6. 文件上传最佳实践:
-
安全措施:
- 限制文件大小
- 验证文件类型
- 验证文件扩展名
- 使用随机文件名
- 存储在非 Web 可访问目录
-
性能优化:
- 使用流式处理大文件
- 实现分片上传
- 使用 CDN 存储文件
- 异步处理文件
-
用户体验:
- 提供上传进度
- 支持断点续传
- 显示上传状态
- 提供预览功能
-
错误处理:
- 捕获上传错误
- 清理失败的文件
- 提供友好的错误信息
- 记录上传日志