从 HTTP 到 WebSocket:我用 Koa + uni-app 搭了一套轻量实时通信服务(附完整源码)

后台管理、工控终端、设备交互,最怕两件事:

  1. 频繁轮询,接口被刷爆
  2. 后端状态变了,前端不知道

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:5566
  • http://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('连接关闭');
};
posted on 2026-08-17 16:28  羽丫头不乖  阅读(4)  评论(0)    收藏  举报