从 HTTP 到 WebSocket:我用 Koa + uni-app 搭了一套轻量实时通信服务(附完整源码)
后台管理、工控终端、设备交互,最怕两件事:
- 频繁轮询,接口被刷爆
- 后端状态变了,前端不知道
HTTP 一来一回就结束了,而 WebSocket 只要不断开,就是长通话。
这篇文章不聊概念,直接上可运行代码:
从 Koa 服务端 → 协议设计 → 客户端 SocketTask 封装 → 心跳 + 队列 + 重连,一步一步来。
一、服务端:让 Koa 同时支持 HTTP + WS
不想单独起一个 WS 服务,直接用 koa-websocket 挂在现有 Koa 上。
package.json
{
"dependencies": {
"@koa/router": "^13.1.0",
"koa": "^3.0.0",
"koa-websocket": "^7.0.0",
"ws": "^8.18.2"
},
"scripts": {
"start": "node app.js",
"nodemon": "nodemon app.js"
}
}
app.js(核心)
const Koa = require('koa');
const websockify = require('koa-websocket');
const Router = require('@koa/router');
// 模拟数据
const obj = {
User: require('./modules/User.json')
};
const app = websockify(new Koa());
const router = new Router();
const port = process.env.PORT || 5566;
/**
* WebSocket 服务
*/
app.ws.use(async (ctx, next) => {
if (ctx.path) {
ctx.websocket.send("websocket连接成功!", ctx);
ctx.websocket.on('message', (message) => {
// 客户端发来的格式:User|add|{"name":"luckly","age":18}
const getData = message.toString('utf8');
console.log('接收信息:', getData);
const info = getData.split('|');
const [module, action, data] = info;
// 模拟业务返回
const result = `${module}|${action}|${JSON.stringify(
obj[module][action]
)}`;
ctx.websocket.send(result);
});
} else {
await next();
}
});
/**
* 保留 HTTP,用于健康检查 / 普通接口
*/
router.get('/u', async (ctx) => {
ctx.body = "uuuuuuuu";
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(port, () => {
console.log('5566端口服务已开启!');
});
modules/User.json
{
"add": {
"code": 200,
"message": "新增成功"
},
"update": {
"code": 200,
"message": "修改成功"
}
}
👉 到这一步,你已经有了:
ws://localhost:5566http://localhost:5566/u
二、协议设计:不用 JSON 包 JSON
很多 WS 教程喜欢这样发:
{ type: 'User', action: 'add', payload: {...} }
问题:
- 调试不直观
- 多端解析心智负担大
我们用的协议非常简单:
模块|动作|数据
客户端 → 服务端
User|add|{"name":"luckly","age":18}
服务端 → 客户端
User|add|{"code":200,"message":"新增成功"}
三、工具函数:协议编解码
发送前:对象 → 字符串
// @/utils/util.js
export function socketFormat(incident, action, data) {
if (typeof data !== 'object') {
console.error('data 不是一个 json 对象');
return;
}
const jsonData = JSON.stringify(data);
return `${incident}|${action}|${jsonData}`;
}
接收后:字符串 → 对象
export function socketStrToJson(message) {
if (typeof message !== 'string') {
throw Error('当前返回的数据不是一个字符串');
}
const resArr = message.split('|');
let data = null;
try {
data = JSON.parse(resArr[2]);
} catch (e) {
console.error(`方法返回的数据不是合法 JSON`);
}
return {
event: resArr[0], // 模块名
action: resArr[1], // 方法名
data // 业务数据
};
}
四、uni-app 里:SocketTask 不是 WebSocket
⚠️ 重点坑
浏览器里:
new WebSocket('ws://localhost:5566');
但在 uni-app 里,这样只能跑 H5。
App / 小程序要用:
uni.connectSocket()
五、socket.js:一个完整的 WS 单例管理器
下面这段代码是核心,包含了:
- 单例连接
- 消息队列
- 串行发送
- 心跳
- 重连
初始化 & 连接
export default {
socketTask: null,
isConnected: false,
heartbeatInterval: null,
retryCount: 0,
maxRetryCount: 3,
messageTask: [],
isSend: false,
init() {
if (this.socketTask) {
console.log('WebSocket 已经连接');
return;
}
this.socketTask = uni.connectSocket({
url: config.baseUrl,
success() {
console.log('connectSocket success');
},
fail: (err) => {
console.error('连接失败', err);
this.retryConnection();
}
});
this.socketTask.onOpen(() => {
this.isConnected = true;
this.retryCount = 0;
this.startHeartbeat();
});
this.socketTask.onClose(() => {
this.isConnected = false;
this.socketTask = null;
});
this._setUpListeners();
},
接收消息 & 全局分发
_setUpListeners() {
this.socketTask.onMessage((message) => {
const res = socketStrToJson(message.data);
// 全局事件分发
uni.$emit(res.event, res);
// 心跳响应
if (res.data === 'pong') {
this.responseTime = Date.now();
}
});
},
👉 页面里直接:
uni.$on('User', res => {
console.log(res.data.message);
});
消息队列:解决“还没 open 就 send”
sendMessage(msg) {
this.messageTask.push(msg);
if (!this.isSend) {
this.processMessage();
}
},
processMessage() {
if (this.messageTask.length === 0) {
this.isSend = false;
return;
}
this.isSend = true;
const msg = this.messageTask.shift();
this.socketTask.send({
data: msg,
success: () => {
setTimeout(() => this.processMessage(), 100);
},
fail: () => {
setTimeout(() => this.processMessage(), 100);
}
});
},
✅ 保证:
- 不并发 send
- 连接未 ready 时不会丢消息
心跳检测(双时间戳)
requestTime: 0,
responseTime: 0,
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.socketTask && this.socketTask.readyState === 1) {
this.requestTime = Date.now();
if (this.requestTime - this.responseTime > 5000) {
this.retryConnection();
}
const msg = socketFormat('Ping', 'pong', {});
this.sendMessage(msg);
}
}, 3000);
},
重连 + 用户感知
retryConnection() {
if (this.retryCount < this.maxRetryCount) {
this.retryCount++;
setTimeout(() => this.init(), 3000);
} else {
// #ifdef WEB
MessageBox.alert('连接失败,请点击确认重连', '系统提示', {
callback: ({ action }) => {
if (action === 'confirm') this.init();
}
});
// #endif
// #ifndef WEB
modal.confirm('连接中断,是否重连').then(ok => {
if (ok) this.init();
});
// #endif
}
}
};
六、页面里怎么用?
按钮触发
this.sendMessage({
event: 'User',
action: 'add',
data: { name: 'luckly', age: 18 }
});
监听响应
uni.$on('User', (res) => {
if (res.data.code === 200) {
this.$message.success('新增成功');
}
});
七、浏览器原生 WS 写法
如果你想在纯 H5 页面里直连:
const socket = new WebSocket('ws://localhost:5566');
socket.onopen = () => {
console.log('连接成功');
socket.send('User|add|{"name":"luckly"}');
};
socket.onmessage = (e) => {
console.log('收到:', e.data);
};
socket.onclose = () => {
console.log('连接关闭');
};
浙公网安备 33010602011771号