Cool-Admin v8.x 中使用 Midway Socket.IO 的实践
# Cool-Admin v8.x 中使用 Midway Socket.IO 的实践
## 目标与结构
- 读者:需要在 Cool-Admin v8.x 项目中实现实时推送(公告、通知、状态同步等)的开发者。
- 核心内容:
1. 依赖与基础配置
2. WebSocket 控制器(鉴权、房间管理)
3. Service 广播流程
4. 客户端接入与调试建议
> 下文所有示例基于 `cool-admin-midway` 工程。
## 1. 依赖与基础配置
### 1.1 安装依赖
在 `cool-admin-midway/package.json` 中新增:
```json
"dependencies": {
"@midwayjs/socketio": "^3.20.11",
"@socket.io/redis-adapter": "^8.3.0",
"ioredis": "^5.8.1"
}
```
安装完成后执行 `npm run dev` 或 `npm run start`,Midway 会自动加载 Socket.IO 组件。
### 1.2 框架入口启用 Socket.IO
`src/configuration.ts` 中引入模块:
```ts
import * as socketio from '@midwayjs/socketio';
@Configuration({
imports: [
// ...其他组件
socketio,
],
})
export class MainConfiguration {}
```
### 1.3 Redis 适配器配置
`src/config/config.default.ts` 中统一维护 Redis 与 socket 配置,确保多进程/多实例时广播可跨进程同步:
```ts
import { createAdapter } from '@socket.io/redis-adapter';
import Redis from 'ioredis';
const socketRedisOptions = {
host: '127.0.0.1',
port: 6379,
password: '***',
db: 2,
};
const socketRedisPubClient = new Redis(socketRedisOptions);
const socketRedisSubClient = socketRedisPubClient.duplicate();
export default {
socketIO: {
cors: { origin: '*' },
transports: ['websocket'],
adapter: createAdapter(socketRedisPubClient, socketRedisSubClient),
},
// ...其余 Cool 配置
};
```
> 提示:如果项目只跑单进程,可暂时省略 adapter;但上线后为了支持 PM2/容器集群,建议始终启用。
## 2. WebSocket 控制器:鉴权与房间管理
按照 Cool-Admin 模块化约定,可在业务模块下新建 `controller/ws/announcement.ts`:
```ts
@Provide()
@WSController('/operation/announcement')
export class OperationAnnouncementWSController {
@Inject() ctx: Context;
@Inject() logger: ILogger;
@Config('module.user.jwt') userJwtConfig;
private verifyClientToken() {
const handshake: any = this.ctx.handshake || {};
const raw =
handshake.auth?.token ||
handshake.headers?.authorization ||
handshake.headers?.Authorization ||
handshake.query?.token;
if (!raw) throw new CoolCommException('未携带 token');
const token = String(raw).replace(/^Bearer\s+/i, '').trim();
const payload: any = jwt.verify(token, this.userJwtConfig.secret);
if (payload?.isRefresh) throw new CoolCommException('登录失效');
(this.ctx as any).user = payload;
return payload;
}
@OnWSConnection()
async onConnection() {
const user = this.verifyClientToken();
this.logger.info('[announcement-ws] client connected', {
socketId: this.ctx.id,
userId: user.id,
});
this.ctx.join('operation:announcement:all');
this.ctx.emit('announcement:connected', { socketId: this.ctx.id, user });
}
@OnWSDisConnection()
async onDisconnect(reason: string) {
this.logger.info('[announcement-ws] client disconnected', {
socketId: this.ctx.id,
reason,
});
}
}
```
关键点:
- **握手鉴权**:直接复用 `module.user.jwt.secret`,确保与 App REST 接口一致的 Token 体系。
- **房间命名**:使用 `operation:announcement:all` 等可读性强的房间名,方便后续服务层广播时定位。
- **日志可观测性**:在连接/断开/鉴权失败处输出日志,便于排查 token 或网络问题。
## 3. Service 层广播示例
在公告服务中注入 Socket 应用实例即可广播数据:
```ts
@Provide()
export class AnnouncementService extends BaseService {
@App('socketIO')
socketApp: Application;
@Inject()
logger: ILogger;
async add(data: any) {
const result = (await super.add(data)) as { id?: number | number[] };
const ids = (Array.isArray(result?.id) ? result.id : [result?.id]).filter(Boolean);
if (!ids.length || !this.socketApp) {
return result;
}
const nsp = this.socketApp.of('/operation/announcement');
const payloads = [];
for (const id of ids) {
const info = await this.info(id);
if (info) payloads.push(info);
}
if (payloads.length) {
nsp.to('operation:announcement:all').emit(
'announcement:new',
payloads.length === 1 ? payloads[0] : payloads
);
this.logger.info('[announcement-service] emit payload', payloads);
}
return result;
}
}
```
要点:
- `@App('socketIO')` 直接注入 Socket Server,避免手动维护单例。
- 通过 `this.info(id)` 获取数据库最新数据,保证推送内容与 REST 接口一致。
- 在日志中记录 emit 情况,方便定位“接口成功但前端没收到”的问题。
## 4. 客户端接入与调试建议
### 4.1 握手与事件约定
- 连接地址:`http(s)://{host}:{port}/operation/announcement`。
- 握手携带 token(与 App REST 接口一致):
```ts
const socket = io('http://127.0.0.1:8001/operation/announcement', {
transports: ['websocket'],
auth: { token: 'Bearer <JWT>' },
});
```
- 默认事件:
- `announcement:connected`:服务端确认连接成功。
- `announcement:new`:公告新增/更新后推送的数据。
- `announcement:error`:鉴权失败或其他异常。
### 4.2 Node/Mac 终端快速测试
```bash
npm install socket.io-client
cat <<'EOF' > socket-test.mjs
import { io } from 'socket.io-client';
const token = 'Bearer 你的APP_TOKEN';
const socket = io('http://127.0.0.1:8001/operation/announcement', {
transports: ['websocket'],
auth: { token },
});
socket.on('connect', () => console.log('[client] connected', socket.id));
socket.on('announcement:connected', payload => console.log('connected', payload));
socket.on('announcement:new', payload => console.log('announcement:new', payload));
socket.on('announcement:error', err => console.error('error', err));
socket.on('disconnect', reason => console.log('disconnect', reason));
EOF
node socket-test.mjs
```
此脚本可验证本地广播链路是否正常,适用于 CI 或调试场景。
### 4.3 Apifox / Postman
- Apifox 需选择 Socket.IO 协议,并在 Header 中添加 `Authorization: Bearer <JWT>`。
- 当前版本的消息面板不会自动监听事件,可手动添加 `announcement:new` 监听,或转而使用上述 Node 脚本。
### 4.4 多进程部署验证
- 当使用 PM2/K8s 等多实例部署时,可借助 `redis-cli monitor` 查看频道(如 `socket.io#/#operation/announcement#`)是否有消息,确认 adapter 正常工作。
- 若只在单实例上测试,可以先关闭 adapter,验证通过后再开启,避免干扰。
## 5. 小结与最佳实践
1. **统一配置中心**:把 socket、Redis、JWT 配置集中在 `config/*.ts` 中,便于多环境切换与团队协作。
2. **模块解耦**:每个业务模块自建 `controller/ws` 与服务,保持 HTTP 与 WebSocket 代码结构一致,降低维护成本。
3. **无状态鉴权**:沿用 App 端的 JWT 不入库策略,必要时在 payload 中增加版本号或设备标识,避免额外的 Redis 写入。
4. **日志与监控**:在控制器、Service、Redis adapter 层都输出关键日志,结合 `redis-cli monitor`、`socket.io-client` 测试脚本,快速定位“接口成功但无广播”的问题。
5. **客户端策略**:小程序/前端如需节省连接,可在 `onHide`/`onUnload` 时断开,在 `onShow` 或登录成功后重连,并加上重连提示。
通过以上步骤,即可在 Cool-Admin v8.x 项目中快速实现基于 Midway 官方 Socket.IO 的实时推送,同时保持与框架既有能力(模块化、权限、Redis 配置等)的一致性。
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 Uniquezx
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果