乐闻世界logo
搜索文章和话题

Koa

在前端技术的跃进浪潮中,一个框架脱颖而出 —— Koa.js,它是由Express原班人马打造的新一代Node.js框架。为什么它能成为时下热议的焦点?因为Koa.js以其简洁的设计,强大的功能和对现代JavaScript特性(如async/await)的天然支持,重新定义了后端的开发模式。 简洁:Koa.js提供了一个轻量的函数库,让你能够快速搭建服务器。 现代化:它采用最新的JS特性,使得代码更加直观且易于管理。 灵活:通过中间件机制,你可以轻松扩展功能,实现定制化的解决方案。 性能:Koa.js注重性能优化,可以建立更快、更稳定的网络应用。 不仅如此,Koa.js的优雅编程体验和提升的开发效率,让前端工程师的技能得到了全方位的提升。它不是简单的技术更迭,而是前端领域的一次革新旅程。
Koa
查看更多相关内容
Koa 性能优化的策略和最佳实践Koa 的性能优化是构建高性能应用的关键。通过合理配置和优化策略,可以显著提升应用的响应速度和并发处理能力。 **1. 中间件优化:** **减少中间件数量:** ```javascript // 不好的做法:过多的中间件 app.use(middleware1); app.use(middleware2); app.use(middleware3); app.use(middleware4); app.use(middleware5); // 好的做法:合并功能相似的中间件 app.use(compose([middleware1, middleware2])); app.use(compose([middleware3, middleware4])); ``` **优化中间件顺序:** ```javascript // 将高频使用的中间件放在前面 app.use(loggerMiddleware); // 日志 app.use(corsMiddleware); // CORS app.use(bodyParserMiddleware); // 解析请求体 app.use(authMiddleware); // 认证 app.use(routerMiddleware); // 路由 ``` **避免在中间件中执行耗时操作:** ```javascript // 不好的做法:在中间件中执行数据库查询 app.use(async (ctx, next) => { const user = await User.findById(ctx.session.userId); ctx.state.user = user; await next(); }); // 好的做法:按需加载数据 app.use(async (ctx, next) => { await next(); // 在需要时再加载数据 }); ``` **2. 异步处理优化:** **使用 Promise.all 并行处理:** ```javascript // 不好的做法:串行执行 app.use(async (ctx) => { const user = await User.findById(id); const posts = await Post.findByUserId(id); const comments = await Comment.findByUserId(id); ctx.body = { user, posts, comments }; }); // 好的做法:并行执行 app.use(async (ctx) => { const [user, posts, comments] = await Promise.all([ User.findById(id), Post.findByUserId(id), Comment.findByUserId(id) ]); ctx.body = { user, posts, comments }; }); ``` **使用缓存减少重复查询:** ```javascript const cache = new Map(); app.use(async (ctx) => { const cacheKey = `user:${ctx.params.id}`; // 检查缓存 if (cache.has(cacheKey)) { ctx.body = cache.get(cacheKey); return; } // 查询数据库 const user = await User.findById(ctx.params.id); cache.set(cacheKey, user); ctx.body = user; }); ``` **3. 数据库连接池优化:** ```javascript const { Pool } = require('pg'); // 配置连接池 const pool = new Pool({ host: 'localhost', port: 5432, database: 'mydb', user: 'user', password: 'password', max: 20, // 最大连接数 min: 5, // 最小连接数 idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000 }); app.use(async (ctx, next) => { const client = await pool.connect(); ctx.state.db = client; try { await next(); } finally { client.release(); } }); ``` **4. 响应压缩:** 使用 `koa-compress` 中间件压缩响应。 ```bash npm install koa-compress ``` ```javascript const compress = require('koa-compress'); app.use(compress({ filter: (contentType) => { return /text/i.test(contentType); }, threshold: 2048, // 大于 2KB 时压缩 gzip: { flush: require('zlib').constants.Z_SYNC_FLUSH }, deflate: { flush: require('zlib').constants.Z_SYNC_FLUSH }, br: false // 禁用 brotli })); ``` **5. 静态资源优化:** ```javascript const serve = require('koa-static'); const { createGzip } = require('zlib'); const { createReadStream, createWriteStream } = require('fs'); // 配置静态资源服务 app.use(serve('./public', { maxage: 365 * 24 * 60 * 60 * 1000, // 1 年缓存 gzip: true, brotli: true })); // 预压缩静态资源 function precompressStaticFiles() { const files = fs.readdirSync('./public'); files.forEach(file => { if (file.endsWith('.js') || file.endsWith('.css')) { const filePath = path.join('./public', file); const gzipPath = filePath + '.gz'; const readStream = createReadStream(filePath); const writeStream = createWriteStream(gzipPath); const gzip = createGzip(); readStream.pipe(gzip).pipe(writeStream); } }); } ``` **6. HTTP/2 支持:** ```javascript const http2 = require('http2'); const fs = require('fs'); const server = http2.createSecureServer({ key: fs.readFileSync('server.key'), cert: fs.readFileSync('server.crt') }, app.callback()); server.listen(3000); ``` **7. 集群模式:** 使用 Node.js 的 cluster 模块充分利用多核 CPU。 ```javascript const cluster = require('cluster'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`Worker ${worker.process.pid} died`); cluster.fork(); }); } else { app.listen(3000); console.log(`Worker ${process.pid} started`); } ``` **8. 监控和性能分析:** ```javascript const prometheus = require('prom-client'); // 创建指标 const httpRequestDuration = new prometheus.Histogram({ name: 'http_request_duration_seconds', help: 'Duration of HTTP requests in seconds', labelNames: ['method', 'route', 'code'] }); // 中间件收集指标 app.use(async (ctx, next) => { const start = Date.now(); await next(); const duration = (Date.now() - start) / 1000; httpRequestDuration.observe( { method: ctx.method, route: ctx.path, code: ctx.status }, duration ); }); // 指标端点 app.use(async (ctx) => { if (ctx.path === '/metrics') { ctx.set('Content-Type', prometheus.register.contentType); ctx.body = await prometheus.register.metrics(); } }); ``` **9. 代码分割和懒加载:** ```javascript // 懒加载路由 app.use(async (ctx, next) => { if (ctx.path.startsWith('/admin')) { const adminRoutes = await import('./routes/admin'); adminRoutes.default.routes()(ctx, next); } else if (ctx.path.startsWith('/api')) { const apiRoutes = await import('./routes/api'); apiRoutes.default.routes()(ctx, next); } else { await next(); } }); ``` **10. 性能优化最佳实践:** 1. **中间件优化:** - 减少中间件数量 - 合并功能相似的中间件 - 优化中间件执行顺序 - 避免在中间件中执行耗时操作 2. **异步处理:** - 使用 Promise.all 并行处理 - 实现缓存机制 - 使用连接池管理数据库连接 - 避免阻塞主线程 3. **资源优化:** - 压缩响应内容 - 启用静态资源缓存 - 使用 CDN 加速 - 预压缩静态资源 4. **架构优化:** - 使用集群模式 - 实现负载均衡 - 使用 HTTP/2 - 实现微服务架构 5. **监控和调优:** - 实现性能监控 - 使用性能分析工具 - 定期进行性能测试 - 持续优化和改进
服务端 · 2月21日 15:54
Koa 测试框架选择和测试最佳实践Koa 测试是保证应用质量的重要环节。通过合理的测试策略和工具,可以确保应用的稳定性和可靠性。 **1. 测试框架选择:** 常用的 Koa 测试框架包括: - **Jest**:Facebook 开发的测试框架,功能全面 - **Mocha + Chai**:灵活的测试框架组合 - **Supertest**:专门用于 HTTP 测试的库 **2. 基本测试设置:** **安装依赖:** ```bash npm install --save-dev jest supertest @types/jest @types/supertest ``` **Jest 配置:** ```javascript // jest.config.js module.exports = { testEnvironment: 'node', coverageDirectory: 'coverage', collectCoverageFrom: [ 'src/**/*.js', '!src/**/*.test.js' ], testMatch: [ '**/__tests__/**/*.js', '**/?(*.)+(spec|test).js' ] }; ``` **3. 基本路由测试:** ```javascript const request = require('supertest'); const app = require('../app'); describe('Basic routes', () => { test('GET / should return Hello Koa', async () => { const response = await request(app.callback()) .get('/') .expect(200); expect(response.text).toBe('Hello Koa'); }); test('GET /not-found should return 404', async () => { const response = await request(app.callback()) .get('/not-found') .expect(404); }); }); ``` **4. 中间件测试:** ```javascript const Koa = require('koa'); const loggerMiddleware = require('../middleware/logger'); describe('Logger middleware', () => { test('should log request information', async () => { const app = new Koa(); const logs = []; // Mock console.log const originalLog = console.log; console.log = (...args) => logs.push(args.join(' ')); app.use(loggerMiddleware); app.use(async (ctx) => { ctx.body = 'test'; }); await request(app.callback()).get('/test'); console.log = originalLog; expect(logs.length).toBeGreaterThan(0); expect(logs[0]).toContain('GET /test'); }); }); ``` **5. 认证中间件测试:** ```javascript const authMiddleware = require('../middleware/auth'); describe('Auth middleware', () => { test('should allow access with valid token', async () => { const ctx = { headers: { authorization: 'Bearer valid-token' }, state: {} }; const next = jest.fn(); await authMiddleware(ctx, next); expect(next).toHaveBeenCalled(); expect(ctx.state.user).toBeDefined(); }); test('should deny access without token', async () => { const ctx = { headers: {}, state: {}, throw: jest.fn() }; const next = jest.fn(); await authMiddleware(ctx, next); expect(next).not.toHaveBeenCalled(); expect(ctx.throw).toHaveBeenCalledWith(401, 'Unauthorized'); }); }); ``` **6. API 测试:** ```javascript describe('User API', () => { test('POST /api/users should create user', async () => { const userData = { name: 'John Doe', email: 'john@example.com', password: 'password123' }; const response = await request(app.callback()) .post('/api/users') .send(userData) .expect(201); expect(response.body).toHaveProperty('id'); expect(response.body.name).toBe(userData.name); expect(response.body.email).toBe(userData.email); expect(response.body).not.toHaveProperty('password'); }); test('GET /api/users/:id should return user', async () => { const userId = 1; const response = await request(app.callback()) .get(`/api/users/${userId}`) .expect(200); expect(response.body).toHaveProperty('id', userId); expect(response.body).toHaveProperty('name'); expect(response.body).toHaveProperty('email'); }); test('PUT /api/users/:id should update user', async () => { const userId = 1; const updateData = { name: 'Updated Name' }; const response = await request(app.callback()) .put(`/api/users/${userId}`) .send(updateData) .expect(200); expect(response.body.name).toBe(updateData.name); }); test('DELETE /api/users/:id should delete user', async () => { const userId = 1; await request(app.callback()) .delete(`/api/users/${userId}`) .expect(204); }); }); ``` **7. 错误处理测试:** ```javascript describe('Error handling', () => { test('should handle 404 errors', async () => { const response = await request(app.callback()) .get('/non-existent-route') .expect(404); expect(response.body).toHaveProperty('error'); expect(response.body).toHaveProperty('code', 'NOT_FOUND'); }); test('should handle validation errors', async () => { const invalidData = { name: '', email: 'invalid-email' }; const response = await request(app.callback()) .post('/api/users') .send(invalidData) .expect(400); expect(response.body).toHaveProperty('error'); expect(response.body).toHaveProperty('code', 'VALIDATION_ERROR'); }); test('should handle server errors', async () => { // Mock a database error jest.spyOn(User, 'create').mockRejectedValue(new Error('Database error')); const response = await request(app.callback()) .post('/api/users') .send({ name: 'Test', email: 'test@example.com' }) .expect(500); expect(response.body).toHaveProperty('error'); expect(response.body).toHaveProperty('code', 'INTERNAL_ERROR'); }); }); ``` **8. 集成测试:** ```javascript describe('Integration tests', () => { let authToken; beforeAll(async () => { // Setup: create test user and get token const response = await request(app.callback()) .post('/api/auth/login') .send({ email: 'test@example.com', password: 'password123' }); authToken = response.body.token; }); afterAll(async () => { // Cleanup: delete test data await request(app.callback()) .delete('/api/test/cleanup'); }); test('should create and retrieve post', async () => { // Create post const createResponse = await request(app.callback()) .post('/api/posts') .set('Authorization', `Bearer ${authToken}`) .send({ title: 'Test Post', content: 'Test content' }) .expect(201); const postId = createResponse.body.id; // Retrieve post const getResponse = await request(app.callback()) .get(`/api/posts/${postId}`) .expect(200); expect(getResponse.body.title).toBe('Test Post'); expect(getResponse.body.content).toBe('Test content'); }); }); ``` **9. 性能测试:** ```javascript describe('Performance tests', () => { test('should handle 1000 requests in reasonable time', async () => { const startTime = Date.now(); const requests = []; for (let i = 0; i < 1000; i++) { requests.push( request(app.callback()) .get('/api/users') .expect(200) ); } await Promise.all(requests); const duration = Date.now() - startTime; expect(duration).toBeLessThan(5000); // Should complete in < 5 seconds }); }); ``` **10. 测试最佳实践:** 1. **测试组织:** - 按功能模块组织测试文件 - 使用 describe 分组相关测试 - 使用有意义的测试名称 2. **测试隔离:** - 每个测试应该独立运行 - 使用 beforeAll/afterAll 设置和清理 - 使用 beforeEach/afterEach 重置状态 3. **Mock 和 Stub:** - Mock 外部依赖 - 使用测试数据库 - 避免测试真实网络请求 4. **覆盖率:** - 设置覆盖率目标(通常 > 80%) - 定期检查覆盖率报告 - 关注关键路径的覆盖率 5. **持续集成:** - 在 CI/CD 流程中运行测试 - 测试失败时阻止部署 - 自动生成覆盖率报告 6. **测试数据:** - 使用工厂模式创建测试数据 - 使用固定的测试数据 - 清理测试数据避免污染 ```javascript // 测试数据工厂 const userFactory = (overrides = {}) => ({ name: 'Test User', email: 'test@example.com', password: 'password123', ...overrides }); // 使用工厂 test('should create user', async () => { const userData = userFactory({ name: 'Custom Name' }); const response = await request(app.callback()) .post('/api/users') .send(userData) .expect(201); expect(response.body.name).toBe('Custom Name'); }); ```
服务端 · 2月21日 15:54
Koa 路由系统的使用方法和最佳实践Koa 核心不包含路由功能,需要通过中间件实现。最常用的路由中间件是 `@koa/router`,它提供了强大的路由定义和管理功能。 **安装路由中间件:** ```bash npm install @koa/router ``` **基本路由使用:** ```javascript const Koa = require('koa'); const Router = require('@koa/router'); const app = new Koa(); const router = new Router(); // 定义路由 router.get('/', async (ctx) => { ctx.body = 'Hello Koa'; }); router.get('/users/:id', async (ctx) => { const id = ctx.params.id; ctx.body = `User ${id}`; }); router.post('/users', async (ctx) => { const user = ctx.request.body; ctx.body = { success: true, user }; }); // 注册路由中间件 app.use(router.routes()); app.use(router.allowedMethods()); app.listen(3000); ``` **路由参数:** 1. **路径参数:** ```javascript router.get('/users/:id', async (ctx) => { const { id } = ctx.params; ctx.body = `User ID: ${id}`; }); // 多个参数 router.get('/posts/:postId/comments/:commentId', async (ctx) => { const { postId, commentId } = ctx.params; ctx.body = `Post: ${postId}, Comment: ${commentId}`; }); ``` 2. **查询参数:** ```javascript router.get('/search', async (ctx) => { const { keyword, page, limit } = ctx.query; ctx.body = { keyword, page, limit }; }); ``` 3. **正则表达式路由:** ```javascript router.get(/^\/users\/(\d+)$/, async (ctx) => { const id = ctx.params[0]; ctx.body = `User ID: ${id}`; }); ``` **路由方法:** ```javascript router.get('/resource', handler); // GET router.post('/resource', handler); // POST router.put('/resource', handler); // PUT router.delete('/resource', handler); // DELETE router.patch('/resource', handler); // PATCH router.all('/resource', handler); // 所有方法 ``` **路由前缀:** ```javascript const apiRouter = new Router({ prefix: '/api/v1' }); apiRouter.get('/users', handler); // 实际路径: /api/v1/users apiRouter.post('/login', handler); // 实际路径: /api/v1/login app.use(apiRouter.routes()); ``` **路由嵌套:** ```javascript const userRouter = new Router({ prefix: '/users' }); userRouter.get('/', async (ctx) => { ctx.body = 'User list'; }); userRouter.get('/:id', async (ctx) => { ctx.body = `User ${ctx.params.id}`; }); const commentRouter = new Router({ prefix: '/:userId/comments' }); commentRouter.get('/', async (ctx) => { ctx.body = `Comments for user ${ctx.params.userId}`; }); userRouter.use(commentRouter.routes()); app.use(userRouter.routes()); ``` **路由中间件:** ```javascript // 单个路由中间件 router.get('/protected', authMiddleware, async (ctx) => { ctx.body = 'Protected content'; }); // 多个路由中间件 router.post('/admin', authMiddleware, adminMiddleware, async (ctx) => { ctx.body = 'Admin content'; } ); // 路由级中间件 router.use(async (ctx, next) => { console.log('Router middleware'); await next(); }); ``` **路由分组和模块化:** ```javascript // routes/users.js const Router = require('@koa/router'); const router = new Router({ prefix: '/users' }); router.get('/', async (ctx) => { ctx.body = 'User list'; }); router.get('/:id', async (ctx) => { ctx.body = `User ${ctx.params.id}`; }); module.exports = router; // app.js const userRoutes = require('./routes/users'); app.use(userRoutes.routes()); app.use(userRoutes.allowedMethods()); ``` **路由命名和重定向:** ```javascript router.get('user', '/users/:id', async (ctx) => { ctx.body = `User ${ctx.params.id}`; }); router.redirect('/old-path', '/new-path'); router.redirect('/old-path', 'user', { id: 123 }); ``` **错误处理:** ```javascript router.get('/error', async (ctx) => { ctx.throw(404, 'User not found'); }); // 使用 allowedMethods 处理不允许的方法 app.use(router.allowedMethods({ throw: true, notImplemented: () => new NotImplemented(), methodNotAllowed: () => new MethodNotAllowed() })); ``` **最佳实践:** 1. 按功能模块划分路由文件 2. 使用路由前缀组织 API 版本 3. 合理使用路由中间件进行权限控制 4. 统一错误处理和响应格式 5. 为路由添加描述性注释 6. 使用 TypeScript 增强类型安全
服务端 · 2月21日 15:54
Koa 文件上传功能的实现方法和最佳实践Koa 核心不包含文件上传功能,需要通过中间件实现。最常用的文件上传中间件是 `koa-body` 或 `koa-multer`,它们提供了强大的文件上传处理能力。 **1. 使用 koa-body 处理文件上传:** **安装:** ```bash npm install koa-body ``` **基本配置:** ```javascript const koaBody = require('koa-body'); app.use(koaBody({ multipart: true, // 启用文件上传 formidable: { maxFileSize: 100 * 1024 * 1024, // 最大文件大小 100MB keepExtensions: true, // 保留文件扩展名 uploadDir: './uploads', // 上传目录 multiples: true // 支持多文件上传 } })); ``` **单文件上传:** ```javascript app.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 }; }); ``` **多文件上传:** ```javascript app.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 处理文件上传:** **安装:** ```bash npm install koa-multer ``` **基本配置:** ```javascript const 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); } } }); ``` **单文件上传:** ```javascript app.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 })) }; }); ``` **混合上传(文件 + 字段):** ```javascript app.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. 文件上传安全措施:** ```javascript const 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` 库处理上传的图片。 ```bash npm install sharp ``` ```javascript const 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. 分片上传:** 对于大文件,实现分片上传功能。 ```javascript const 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. 文件上传最佳实践:** 1. **安全措施:** - 限制文件大小 - 验证文件类型 - 验证文件扩展名 - 使用随机文件名 - 存储在非 Web 可访问目录 2. **性能优化:** - 使用流式处理大文件 - 实现分片上传 - 使用 CDN 存储文件 - 异步处理文件 3. **用户体验:** - 提供上传进度 - 支持断点续传 - 显示上传状态 - 提供预览功能 4. **错误处理:** - 捕获上传错误 - 清理失败的文件 - 提供友好的错误信息 - 记录上传日志
服务端 · 2月21日 15:54
如何开发自定义 Koa 中间件以及常见中间件类型Koa 中间件是一个 async 函数,接收两个参数:ctx(上下文对象)和 next(下一个中间件的调用函数)。开发自定义中间件需要遵循特定的模式和最佳实践。 **中间件基本结构:** ```javascript async function myMiddleware(ctx, next) { // 前置逻辑 console.log('请求进入'); // 调用下一个中间件 await next(); // 后置逻辑 console.log('请求处理完成'); } // 使用中间件 app.use(myMiddleware); ``` **常见中间件类型:** 1. **日志中间件:** ```javascript function loggerMiddleware(ctx, next) { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); } ``` 2. **认证中间件:** ```javascript async function authMiddleware(ctx, next) { const token = ctx.headers.authorization; if (!token) { ctx.throw(401, 'Unauthorized'); } try { const user = await verifyToken(token); ctx.state.user = user; await next(); } catch (error) { ctx.throw(401, 'Invalid token'); } } ``` 3. **错误处理中间件:** ```javascript async function errorHandler(ctx, next) { try { await next(); } catch (err) { ctx.status = err.status || 500; ctx.body = { error: err.message, code: err.code }; ctx.app.emit('error', err, ctx); } } ``` 4. **CORS 中间件:** ```javascript async function corsMiddleware(ctx, next) { ctx.set('Access-Control-Allow-Origin', '*'); ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (ctx.method === 'OPTIONS') { ctx.status = 204; return; } await next(); } ``` 5. **请求体解析中间件:** ```javascript async function bodyParser(ctx, next) { if (ctx.method !== 'POST' && ctx.method !== 'PUT') { return await next(); } const chunks = []; for await (const chunk of ctx.req) { chunks.push(chunk); } const body = Buffer.concat(chunks).toString(); ctx.request.body = JSON.parse(body); await next(); } ``` **中间件开发最佳实践:** 1. **命名规范:** 使用描述性的函数名,如 `authMiddleware`、`loggerMiddleware` 2. **错误处理:** 使用 try-catch 捕获错误,避免影响其他中间件 3. **性能优化:** 避免在中间件中执行耗时操作 4. **配置化:** 支持配置参数,提高中间件灵活性 5. **文档完善:** 提供清晰的使用文档和示例 **可配置中间件示例:** ```javascript function createLogger(options = {}) { const { format = 'default', includeQuery = false } = options; return async function logger(ctx, next) { const start = Date.now(); await next(); const ms = Date.now() - start; let log = `${ctx.method} ${ctx.url} - ${ms}ms`; if (includeQuery && Object.keys(ctx.query).length) { log += ` ${JSON.stringify(ctx.query)}`; } console.log(log); }; } // 使用配置化中间件 app.use(createLogger({ includeQuery: true })); ``` **中间件组合:** ```javascript const compose = require('koa-compose'); const middleware = compose([ loggerMiddleware, authMiddleware, errorHandler ]); app.use(middleware); ``` 开发高质量中间件的关键是理解洋葱模型的执行流程,合理使用前置和后置逻辑,并确保中间件的独立性和可复用性。
服务端 · 2月21日 15:54
Koa 与 Express 框架的详细对比和选择建议Koa 与 Express 是两个流行的 Node.js Web 框架,它们各有特点和适用场景。理解它们的差异有助于在实际项目中做出正确的选择。 **1. 核心设计理念:** **Express:** - 内置大量功能(路由、中间件、模板引擎等) - 提供开箱即用的解决方案 - 采用传统的回调函数模式 - 中间件链式调用 **Koa:** - 极简核心,只提供最基础的功能 - 通过中间件扩展功能 - 采用现代 async/await 模式 - 洋葱模型中间件机制 **2. 中间件机制对比:** **Express 中间件:** ```javascript const express = require('express'); const app = express(); app.use((req, res, next) => { console.log('Middleware 1'); next(); console.log('Middleware 1 after'); }); app.use((req, res, next) => { console.log('Middleware 2'); res.send('Hello Express'); }); // 执行顺序:Middleware 1 -> Middleware 2 -> Middleware 1 after ``` **Koa 中间件:** ```javascript const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next) => { console.log('Middleware 1 before'); await next(); console.log('Middleware 1 after'); }); app.use(async (ctx, next) => { console.log('Middleware 2 before'); await next(); console.log('Middleware 2 after'); ctx.body = 'Hello Koa'; }); // 执行顺序:Middleware 1 before -> Middleware 2 before -> // Middleware 2 after -> Middleware 1 after ``` **3. 代码风格对比:** **Express 回调风格:** ```javascript app.get('/users/:id', (req, res, next) => { User.findById(req.params.id, (err, user) => { if (err) return next(err); Post.findByUserId(user.id, (err, posts) => { if (err) return next(err); res.json({ user, posts }); }); }); }); ``` **Koa async/await 风格:** ```javascript app.get('/users/:id', async (ctx) => { const user = await User.findById(ctx.params.id); const posts = await Post.findByUserId(user.id); ctx.body = { user, posts }; }); ``` **4. 请求/响应处理对比:** **Express:** ```javascript app.get('/', (req, res) => { // 请求信息 const url = req.url; const method = req.method; const query = req.query; const body = req.body; // 响应设置 res.status(200); res.json({ message: 'Hello' }); // 或 res.send('Hello'); // 或 res.render('index', { title: 'Hello' }); }); ``` **Koa:** ```javascript app.get('/', async (ctx) => { // 请求信息 const url = ctx.url; const method = ctx.method; const query = ctx.query; const body = ctx.request.body; // 响应设置 ctx.status = 200; ctx.body = { message: 'Hello' }; // 或 ctx.type = 'text/html'; ctx.body = '<h1>Hello</h1>'; }); ``` **5. 错误处理对比:** **Express 错误处理:** ```javascript app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: err.message }); }); // 抛出错误 app.get('/error', (req, res, next) => { const err = new Error('Something went wrong'); err.status = 500; next(err); }); ``` **Koa 错误处理:** ```javascript app.use(async (ctx, next) => { try { await next(); } catch (err) { ctx.status = err.status || 500; ctx.body = { error: err.message }; ctx.app.emit('error', err, ctx); } }); // 抛出错误 app.get('/error', async (ctx) => { ctx.throw(500, 'Something went wrong'); }); ``` **6. 路由功能对比:** **Express 内置路由:** ```javascript const express = require('express'); const router = express.Router(); router.get('/users', getUsers); router.post('/users', createUser); router.get('/users/:id', getUser); router.put('/users/:id', updateUser); router.delete('/users/:id', deleteUser); app.use('/api', router); ``` **Koa 需要路由中间件:** ```javascript const Router = require('@koa/router'); const router = new Router(); router.get('/users', getUsers); router.post('/users', createUser); router.get('/users/:id', getUser); router.put('/users/:id', updateUser); router.delete('/users/:id', deleteUser); app.use(router.routes()); app.use(router.allowedMethods()); ``` **7. 性能对比:** **Express:** - 成熟稳定,经过大量生产环境验证 - 中间件链式调用,性能相对较低 - 回调函数,可能存在回调地狱 - 内存占用相对较高 **Koa:** - 更轻量级,核心只有约 2KB - async/await,代码更简洁 - 洋葱模型,中间件控制更灵活 - 内存占用相对较低 **8. 学习曲线对比:** **Express:** - 文档丰富,社区活跃 - 学习曲线平缓 - 大量教程和示例 - 适合初学者 **Koa:** - 需要理解 async/await - 需要理解洋葱模型 - 需要选择合适的中间件 - 适合有一定经验的开发者 **9. 适用场景对比:** **Express 适合:** - 快速开发原型 - 传统 Web 应用 - 需要大量内置功能的项目 - 团队成员对 async/await 不熟悉 - 需要稳定成熟的框架 **Koa 适合:** - 现代 Web 应用 - 需要精细控制中间件的项目 - 追求代码简洁和可维护性 - 团队熟悉现代 JavaScript - 需要更好的错误处理 **10. 迁移建议:** 从 Express 迁移到 Koa: ```javascript // Express app.get('/users/:id', async (req, res, next) => { try { const user = await User.findById(req.params.id); res.json(user); } catch (err) { next(err); } }); // Koa app.get('/users/:id', async (ctx) => { const user = await User.findById(ctx.params.id); ctx.body = user; }); ``` **总结:** | 特性 | Express | Koa | |------|---------|-----| | 核心大小 | 较大 | 极小(2KB) | | 中间件模式 | 链式调用 | 洋葱模型 | | 异步处理 | 回调函数 | async/await | | 路由 | 内置 | 需要中间件 | | 学习曲线 | 平缓 | 较陡 | | 社区生态 | 成熟 | 快速发展 | | 性能 | 良好 | 优秀 | | 适用场景 | 传统应用 | 现代应用 | 选择建议: - 如果追求快速开发和稳定性,选择 Express - 如果追求代码质量和现代化,选择 Koa - 如果团队熟悉 async/await,优先选择 Koa - 如果需要大量内置功能,选择 Express
服务端 · 2月21日 15:54
Koa 洋葱模型的执行机制和实际应用场景有哪些Koa 的洋葱模型(Onion Model)是其最核心的设计特性,通过 async/await 实现中间件的执行流程控制。在洋葱模型中,中间件按照注册顺序执行,形成类似洋葱的层级结构。 执行流程如下: 1. 请求从外层中间件进入,逐层向内传递 2. 每个中间件在 await next() 之前执行"前置逻辑" 3. 执行 await next() 进入下一层中间件 4. 到达最内层后,开始逐层向外返回 5. 每个中间件在 await next() 之后执行"后置逻辑" 6. 最终响应从最外层中间件返回客户端 代码示例: ```javascript const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next) => { console.log('1 - 前置'); await next(); console.log('1 - 后置'); }); app.use(async (ctx, next) => { console.log('2 - 前置'); await next(); console.log('2 - 后置'); }); app.use(async (ctx) => { console.log('3 - 核心处理'); ctx.body = 'Hello Koa'; }); // 执行顺序:1-前置 -> 2-前置 -> 3-核心处理 -> 2-后置 -> 1-后置 ``` 洋葱模型的优势: 1. 清晰的执行顺序:前置和后置逻辑分离,代码结构清晰 2. 灵活的控制:每个中间件可以决定是否继续执行下游 3. 统一的错误处理:通过 try-catch 可以捕获所有中间件的错误 4. 中间件复用:可以在不同位置复用中间件逻辑 5. 请求/响应处理:方便在请求进入和响应返回时执行不同逻辑 实际应用场景: - 日志记录:在前后置逻辑中记录请求和响应信息 - 错误处理:在外层中间件统一捕获和处理错误 - 认证授权:在前置逻辑中验证用户身份 - 响应时间统计:计算请求处理总耗时 - 响应格式化:在后置逻辑中统一处理响应格式
服务端 · 2月21日 15:53
Koa 错误处理机制和最佳实践详解Koa 的错误处理机制非常优雅,通过 try-catch 和事件系统提供了多种错误处理方式。正确处理错误是构建健壮应用的关键。 **1. 使用 ctx.throw() 抛出错误:** ```javascript app.use(async (ctx) => { if (!ctx.query.token) { ctx.throw(401, 'Token is required'); } ctx.body = 'Success'; }); ``` **2. 使用 try-catch 捕获错误:** ```javascript app.use(async (ctx, next) => { try { await next(); } catch (err) { ctx.status = err.status || 500; ctx.body = { error: err.message, code: err.code || 'INTERNAL_ERROR' }; ctx.app.emit('error', err, ctx); } }); ``` **3. 错误处理中间件:** ```javascript async function errorHandler(ctx, next) { try { await next(); } catch (err) { ctx.status = err.status || 500; // 开发环境返回详细错误信息 if (app.env === 'development') { ctx.body = { error: err.message, stack: err.stack, code: err.code }; } else { // 生产环境返回简化的错误信息 ctx.body = { error: 'Internal Server Error', code: 'INTERNAL_ERROR' }; } // 触发应用错误事件 ctx.app.emit('error', err, ctx); } } app.use(errorHandler); ``` **4. 自定义错误类:** ```javascript class AppError extends Error { constructor(status, message, code) { super(message); this.status = status; this.code = code; this.name = 'AppError'; } } class NotFoundError extends AppError { constructor(message = 'Resource not found') { super(404, message, 'NOT_FOUND'); this.name = 'NotFoundError'; } } class ValidationError extends AppError { constructor(message = 'Validation failed') { super(400, message, 'VALIDATION_ERROR'); this.name = 'ValidationError'; } } // 使用自定义错误 app.use(async (ctx) => { const user = await findUser(ctx.params.id); if (!user) { throw new NotFoundError('User not found'); } ctx.body = user; }); ``` **5. 全局错误监听:** ```javascript app.on('error', (err, ctx) => { // 记录错误日志 console.error('Server error:', err); // 发送错误通知(如邮件、Slack等) sendErrorNotification(err, ctx); // 上报错误到监控系统 reportToMonitoring(err, ctx); }); ``` **6. 404 处理:** ```javascript // 在所有路由之后添加 404 处理 app.use(async (ctx) => { ctx.status = 404; ctx.body = { error: 'Not Found', code: 'NOT_FOUND', path: ctx.url }; }); ``` **7. 异步错误处理:** ```javascript // Koa 会自动捕获 async 函数中的错误 app.use(async (ctx) => { const data = await fetchData(); // 如果这里抛出错误,会被捕获 ctx.body = data; }); // 对于 Promise 链,确保正确处理 app.use(async (ctx) => { try { const result = await someAsyncOperation() .then(data => processData(data)) .catch(err => { throw new AppError(400, 'Processing failed', 'PROCESS_ERROR'); }); ctx.body = result; } catch (err) { ctx.throw(err.status || 500, err.message); } }); ``` **8. 错误处理最佳实践:** ```javascript // 完整的错误处理示例 const Koa = require('koa'); const app = new Koa(); // 自定义错误类 class AppError extends Error { constructor(status, message, code) { super(message); this.status = status; this.code = code; } } // 错误处理中间件 app.use(async (ctx, next) => { try { await next(); } catch (err) { // 设置状态码 ctx.status = err.status || 500; // 构建错误响应 const errorResponse = { error: err.message, code: err.code || 'INTERNAL_ERROR', timestamp: new Date().toISOString() }; // 开发环境包含堆栈信息 if (app.env === 'development') { errorResponse.stack = err.stack; } ctx.body = errorResponse; // 触发错误事件 ctx.app.emit('error', err, ctx); } }); // 全局错误监听 app.on('error', (err, ctx) => { console.error(`[${new Date().toISOString()}] Error:`, err.message); console.error('Path:', ctx.url); console.error('Stack:', err.stack); }); // 业务路由 app.use(async (ctx) => { if (ctx.path === '/error') { throw new AppError(500, 'Something went wrong', 'SERVER_ERROR'); } if (ctx.path === '/not-found') { ctx.throw(404, 'Resource not found'); } ctx.body = 'Hello Koa'; }); app.listen(3000); ``` **9. 常见错误处理场景:** ```javascript // 数据库错误处理 app.use(async (ctx, next) => { try { await next(); } catch (err) { if (err.code === '23505') { // PostgreSQL 唯一约束冲突 ctx.throw(409, 'Resource already exists'); } else if (err.code === '23503') { // 外键约束冲突 ctx.throw(400, 'Invalid reference'); } else { throw err; } } }); // 验证错误处理 app.use(async (ctx, next) => { try { await next(); } catch (err) { if (err.name === 'ValidationError') { ctx.throw(400, err.message); } throw err; } }); ``` **错误处理要点:** 1. 使用 try-catch 包裹可能出错的代码 2. 创建自定义错误类,提供更清晰的错误信息 3. 区分开发环境和生产环境的错误响应 4. 实现全局错误监听,统一记录和上报 5. 为不同类型的错误提供适当的 HTTP 状态码 6. 确保错误不会泄露敏感信息 7. 在洋葱模型的最外层添加错误处理中间件
服务端 · 2月21日 15:53
Koa 中 Cookie 和 Session 管理的实现方法Koa 的 Cookie 和 Session 管理是构建 Web 应用的基础功能,Koa 核心提供了 Cookie 操作,而 Session 需要通过中间件实现。 **1. Cookie 操作:** Koa 核心内置了 Cookie 功能,通过 `ctx.cookies` 对象进行操作。 **设置 Cookie:** ```javascript app.use(async (ctx) => { // 基本设置 ctx.cookies.set('name', 'value'); // 带选项的设置 ctx.cookies.set('username', 'john', { maxAge: 3600000, // 有效期(毫秒) expires: new Date('2025-12-31'), // 过期时间 path: '/', // 路径 domain: '.example.com', // 域名 secure: true, // 仅 HTTPS httpOnly: true, // 仅 HTTP,防止 XSS sameSite: 'strict', // CSRF 保护 signed: true // 签名 Cookie }); ctx.body = 'Cookie set'; }); ``` **获取 Cookie:** ```javascript app.use(async (ctx) => { const username = ctx.cookies.get('username'); ctx.body = `Hello ${username}`; }); ``` **删除 Cookie:** ```javascript app.use(async (ctx) => { ctx.cookies.set('username', null, { maxAge: 0, path: '/' }); ctx.body = 'Cookie deleted'; }); ``` **2. Session 管理:** 使用 `koa-session` 中间件实现 Session 功能。 **安装:** ```bash npm install koa-session ``` **基本配置:** ```javascript const session = require('koa-session'); const sessionConfig = { key: 'koa.sess', // Cookie 名称 maxAge: 86400000, // 有效期(毫秒) autoCommit: true, // 自动提交 overwrite: true, // 覆盖 httpOnly: true, // 仅 HTTP signed: true, // 签名 rolling: false, // 每次请求更新过期时间 renew: false, // 快过期时自动续期 secure: false, // 仅 HTTPS sameSite: null, // SameSite 策略 }; app.keys = ['your-secret-key']; // 必须设置用于签名 app.use(session(sessionConfig, app)); ``` **Session 使用:** ```javascript // 设置 Session app.use(async (ctx) => { if (ctx.path === '/login') { ctx.session.user = { id: 1, name: 'John', role: 'admin' }; ctx.body = 'Logged in'; } }); // 获取 Session app.use(async (ctx) => { if (ctx.path === '/profile') { const user = ctx.session.user; if (user) { ctx.body = `Welcome ${user.name}`; } else { ctx.throw(401, 'Not logged in'); } } }); // 删除 Session app.use(async (ctx) => { if (ctx.path === '/logout') { ctx.session = null; ctx.body = 'Logged out'; } }); ``` **3. Redis Session 存储:** 对于生产环境,建议使用 Redis 存储 Session。 **安装:** ```bash npm install koa-session koa-redis ``` **配置 Redis Session:** ```javascript const session = require('koa-session'); const RedisStore = require('koa-redis'); const redisStore = RedisStore({ host: 'localhost', port: 6379, password: 'your-password', db: 0 }); const sessionConfig = { store: redisStore, key: 'koa.sess', maxAge: 86400000, httpOnly: true, signed: true }; app.keys = ['your-secret-key']; app.use(session(sessionConfig, app)); ``` **4. 认证中间件示例:** ```javascript // 认证中间件 async function authMiddleware(ctx, next) { if (!ctx.session.user) { ctx.throw(401, 'Unauthorized'); } await next(); } // 使用认证中间件 router.get('/protected', authMiddleware, async (ctx) => { ctx.body = `Welcome ${ctx.session.user.name}`; }); ``` **5. JWT Token 认证:** 使用 `jsonwebtoken` 和 `koa-jwt` 实现 JWT 认证。 **安装:** ```bash npm install jsonwebtoken koa-jwt ``` **生成 Token:** ```javascript const jwt = require('jsonwebtoken'); app.use(async (ctx) => { if (ctx.path === '/login') { const { username, password } = ctx.request.body; // 验证用户 const user = await authenticateUser(username, password); // 生成 Token const token = jwt.sign( { id: user.id, name: user.name }, 'your-secret-key', { expiresIn: '24h' } ); ctx.body = { token }; } }); ``` **验证 Token:** ```javascript const jwt = require('koa-jwt'); app.use(jwt({ secret: 'your-secret-key' }).unless({ path: [/^\/public/, '/login', '/register'] })); // 访问用户信息 app.use(async (ctx) => { ctx.body = ctx.state.user; }); ``` **6. 完整的认证流程示例:** ```javascript const Koa = require('koa'); const Router = require('@koa/router'); const session = require('koa-session'); const jwt = require('jsonwebtoken'); const koaJwt = require('koa-jwt'); const app = new Koa(); const router = new Router(); // Session 配置 app.keys = ['secret-key']; app.use(session({ key: 'koa.sess', maxAge: 86400000 }, app)); // JWT 中间件 app.use(koaJwt({ secret: 'jwt-secret' }).unless({ path: [/^\/api\/auth/] })); // 登录路由 router.post('/api/auth/login', async (ctx) => { const { username, password } = ctx.request.body; // 验证用户 const user = await User.findOne({ username }); if (!user || !await user.comparePassword(password)) { ctx.throw(401, 'Invalid credentials'); } // 设置 Session ctx.session.user = { id: user.id, name: user.name }; // 生成 JWT Token const token = jwt.sign( { id: user.id, name: user.name }, 'jwt-secret', { expiresIn: '24h' } ); ctx.body = { token, user: { id: user.id, name: user.name } }; }); // 受保护的路由 router.get('/api/user/profile', async (ctx) => { ctx.body = ctx.state.user; }); // 登出路由 router.post('/api/auth/logout', async (ctx) => { ctx.session = null; ctx.body = { message: 'Logged out' }; }); app.use(router.routes()); ``` **7. 安全最佳实践:** 1. **Cookie 安全:** - 始终设置 `httpOnly: true` 防止 XSS - 生产环境使用 `secure: true` 仅 HTTPS - 设置 `sameSite: 'strict'` 防止 CSRF - 使用签名 Cookie 防止篡改 2. **Session 安全:** - 使用强随机密钥 - 设置合理的过期时间 - 生产环境使用 Redis 存储 - 登出时清除 Session 3. **JWT 安全:** - 使用强密钥 - 设置合理的过期时间 - 使用 HTTPS 传输 - 实现 Token 刷新机制 4. **其他安全措施:** - 限制登录尝试次数 - 实现密码强度验证 - 记录认证日志 - 定期更新密钥
服务端 · 2月21日 15:53