在Express路由中实现“副作用”的规范方案咨询
这确实是个很常见的架构问题——谁都不想让路由控制器里塞满WebSocket相关的逻辑,破坏代码的职责单一性对吧?我给你分享几个Express生态里社区公认的规范实现思路,都是实际项目里常用的:
1. 事件驱动模式(最推荐,解耦性最强)
这个思路的核心是把HTTP业务逻辑和WebSocket广播逻辑完全解耦:路由控制器只负责处理HTTP请求、返回响应,然后触发一个自定义业务事件;专门的WebSocket监听器负责监听这些事件,处理广播逻辑。
实现步骤:
首先创建一个全局的事件发射器(用Node原生的events模块即可):
// utils/eventEmitter.js const EventEmitter = require('events'); const appEventEmitter = new EventEmitter(); module.exports = appEventEmitter;
然后在路由控制器里,处理完业务后触发事件:
// routes/message.js const express = require('express'); const router = express.Router(); const appEventEmitter = require('../utils/eventEmitter'); const { saveMessageToDB, getUserUpdatedData, getLatestMessageList } = require('../services/messageService'); router.post('/message', async (req, res) => { // 1. 处理核心业务:保存消息到数据库 const newMessage = await saveMessageToDB(req.body); // 2. 返回HTTP响应 res.status(201).json({ success: true, data: newMessage }); // 3. 触发自定义业务事件,携带需要广播的更新数据 const updatePayload = { updatedUserData: await getUserUpdatedData(newMessage.userId), updatedMessageList: await getLatestMessageList() }; appEventEmitter.emit('message.created', updatePayload); });
最后在WebSocket的初始化文件里,监听这个事件并处理广播:
// websocket/server.js const WebSocket = require('ws'); const appEventEmitter = require('../utils/eventEmitter'); // 初始化WebSocket服务器 const wss = new WebSocket.Server({ port: 8080 }); // 监听业务事件,处理广播 function initWebSocketEventListeners() { appEventEmitter.on('message.created', (payload) => { // 遍历所有在线客户端发送更新 wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ type: 'MODEL_UPDATES', payload: payload })); } }); }); // 后续新增其他业务事件(比如user.updated)也可以在这里添加监听 } initWebSocketEventListeners(); module.exports = wss;
优势:完全解耦HTTP和WebSocket逻辑,控制器只关注HTTP请求处理,WebSocket逻辑集中管理,后续扩展或修改广播规则时,不需要改动路由代码,维护成本极低。
2. 后置中间件+上下文传递
如果你更习惯Express的中间件思维,可以通过res.locals传递广播数据,然后用一个后置中间件统一处理WebSocket发送逻辑。
实现步骤:
首先在路由控制器里,把需要广播的数据存入res.locals:
// routes/message.js router.post('/message', async (req, res, next) => { const newMessage = await saveMessageToDB(req.body); res.status(201).json({ success: true, data: newMessage }); // 将广播数据存入res.locals,供后续中间件使用 res.locals.wsBroadcast = { type: 'MODEL_UPDATES', payload: { updatedUserData: await getUserUpdatedData(newMessage.userId), updatedMessageList: await getLatestMessageList() } }; next(); // 调用后置中间件 });
然后创建专门的广播中间件:
// middleware/wsBroadcast.js const WebSocket = require('ws'); const wss = require('../websocket/server'); // 导入你的WebSocket服务器实例 module.exports = function wsBroadcastMiddleware(req, res, next) { // 检查是否有需要广播的数据 if (res.locals.wsBroadcast) { const broadcastData = JSON.stringify(res.locals.wsBroadcast); // 遍历客户端发送 wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(broadcastData); } }); } next(); };
最后在Express应用中注册这个中间件(注意要放在所有路由之后):
// app.js const express = require('express'); const app = express(); const wsBroadcastMiddleware = require('./middleware/wsBroadcast'); // 先注册所有API路由 app.use('/api', require('./routes')); // 再注册后置广播中间件 app.use(wsBroadcastMiddleware); app.listen(3000);
优势:符合Express的中间件设计哲学,把广播逻辑抽成可复用的中间件,控制器只需要传递数据,代码风格统一。
3. 服务层封装(适合中小项目)
如果不想引入事件驱动,也可以把WebSocket广播逻辑封装成一个独立的服务,控制器通过调用服务方法来触发广播——虽然还是在控制器里调用,但逻辑被封装,后续修改广播规则只需要改动服务代码。
实现步骤:
创建WebSocket广播服务:
// services/wsBroadcastService.js const WebSocket = require('ws'); class WsBroadcastService { constructor(wss) { this.wss = wss; } // 封装广播模型更新的方法 broadcastModelUpdates(payload) { const message = JSON.stringify({ type: 'MODEL_UPDATES', payload: payload }); this.wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(message); } }); } } // 初始化服务,传入WebSocket服务器实例 const wss = new WebSocket.Server({ port: 8080 }); const wsBroadcastService = new WsBroadcastService(wss); module.exports = wsBroadcastService;
然后在控制器里调用服务方法:
// routes/message.js const wsBroadcastService = require('../services/wsBroadcastService'); router.post('/message', async (req, res) => { const newMessage = await saveMessageToDB(req.body); res.status(201).json({ success: true, data: newMessage }); // 调用广播服务的方法 const updatePayload = { updatedUserData: await getUserUpdatedData(newMessage.userId), updatedMessageList: await getLatestMessageList() }; wsBroadcastService.broadcastModelUpdates(updatePayload); });
优势:相比直接在控制器写WS逻辑更清晰,封装了重复的广播代码,适合中小项目快速实现。
内容的提问来源于stack exchange,提问作者Chuckatron

