1. 痛点
ws框架基于websocket进行处理时,在onmessage上进行过度代码编写不易维护且拓展性低
2. 解决思路
通过前后端统一规定达成共识,前端针对WebSocket类,后端针对ws框架进行二次封装。
前端部分采取订阅发布的设计模式,对每次ws请求都能立刻监听到该次请求访问的数据。方便我们在各个Vue文件中去使用这个websocket,因此这个websocket最好放进pinia状态管理里面去存储
后端部分采取路由的形式,将业务处理分散到每个函数中,并通过“${路由}/${函数名}”的形式调用对应的controller,各个controller之间又能相互调用。如果需要扩充,可以自行在LafWebsocket文件里去修改,代码也不多。
后端部分同时也提供了类似拦截器、各种钩子函数、统一错误处理等等工具,可以自行研究一下
3. 后端部分(Laf)
请直接新建函数,并命名为LafWebsocket
然后将函数替换成如下代码即可,无需任何改造。
import cloud from "@lafjs/cloud";
import { WebSocket } from "ws";
export class Client {
group: string = 'default';
ws: WebSocket;
constructor(ws: WebSocket) {
this.ws = ws;
}
}
export class ConstructorData {
router: Router[];
ctx: FunctionContext;
onConnection?: Function;
onClose?: Function;
requestInterceptor?: Function;
}
export class Context<T> {
path: string;
client: Client;
clientList: Client[];
data: T;
token?: string;
constructor(constructorData: Context<any>) {
for (const key in constructorData) {
this[key] = constructorData[key];
}
}
reply?(resp: {
status: number,
data?: any;
message?: string;
}) {
this.client.ws.send(JSON.stringify({
...resp,
path: this.path
}));
}
replyAll?(resp: {
status: number,
data?: any;
message?: string;
}, group?: string) {
let targetGroup = this.clientList;
if (group) {
targetGroup = targetGroup.filter(item => item.group === group);
}
targetGroup.forEach(item => {
item.ws.send(JSON.stringify({ ...resp, path: this.path }));
});
}
}
export class Router {
path: string;
controller: any;
}
export default class LafWebsocket {
constructor(constructorData: ConstructorData) {
this.init(constructorData);
}
private async init(constructorData: ConstructorData) {
this.checkClientList();
if (constructorData.ctx.method === "WebSocket:connection") {
let clientList: Client[] = cloud.shared.get('websocketLaf_clientList') || [];
clientList.push(new Client(constructorData.ctx.socket));
cloud.shared.set('websocketLaf_clientList', clientList);
if (constructorData.onConnection && constructorData.onConnection instanceof Function) constructorData.onConnection();
}
else if (constructorData.ctx.method === "WebSocket:close") {
this.checkClientList();
if (constructorData.onClose && constructorData.onClose instanceof Function) constructorData.onClose();
}
else if (constructorData.ctx.method === "WebSocket:message") {
let clientList: Client[] = cloud.shared.get('websocketLaf_clientList') || [];
const { data } = constructorData.ctx.params;
const errorContext = new Context({
path: 'error',
client: clientList.find(item => item.ws === constructorData.ctx.socket),
clientList,
data: null
});
const msg = JSON.parse(data.toString());
if (constructorData.requestInterceptor && constructorData.requestInterceptor instanceof Function) {
const response = constructorData.requestInterceptor(new Context({
path: msg.path,
client: clientList.find(item => item.ws === constructorData.ctx.socket),
clientList: clientList,
data: msg.data,
}));
if (response === false) return;
}
if (!/^.+\/.+$/.test(msg.path)) {
return errorContext.reply({
status: 400,
data: null,
message: 'system error: 路径格式错误'
});
}
const pathArr = msg.path.split('/');
const pathRouter = pathArr[0];
const pathController = pathArr[1];
const targetRouter = constructorData.router.find(item => item.path === pathRouter);
if (!targetRouter) {
return errorContext.reply({
status: 400,
data: null,
message: `system error: ${pathRouter}路由不存在`
});
}
if (!targetRouter.controller[pathController]) {
return errorContext.reply({
status: 400,
data: null,
message: `system error: ${pathRouter}路由下不存在该方法`
});
}
try {
await targetRouter.controller[pathController](new Context({
path: msg.path,
client: clientList.find(item => item.ws === constructorData.ctx.socket),
clientList: clientList,
data: msg.data,
}));
} catch (error) {
console.log(String(error));
return errorContext.reply({
status: 500,
data: null,
message: `system error: ${String(error)}`
});
}
}
}
private checkClientList() {
let clientList: Client[] = cloud.shared.get('websocketLaf_clientList') || [];
clientList = clientList.filter(item => item.ws.readyState !== 3);
cloud.shared.set('websocketLaf_clientList', clientList);
}
}
再继续直接新建函数,并命名为TestController
然后将函数替换成如下代码即可,无需任何改造。
import cloud from '@lafjs/cloud';
import { Context } from "@/LafWebsocket";
import { CollectionReference } from "@lafjs/cloud/node_modules/database-ql";
class TestController {
async test(ctx: Context<string>) {
ctx.reply({
status: 200,
data: ctx.data,
message: '测试成功'
});
}
}
export default new TestController();
最后请继续直接新建函数,并命名为__websocket__
然后将函数替换成如下代码即可,无需任何改造。
import TestController from '@/TestController';
import { LafWebsocket } from "@/LafWebsocket";
import { Context } from "@/LafWebsocket";
export default async function (ctx: FunctionContext) {
new LafWebsocket({
ctx,
router: [
{
path: 'test',
controller: testController
}
],
requestInterceptor: (ctx: Context) => {
},
onClose: () => {
},
onConnection: () => {
}
});
}
4. 前端部分
4.1 页面简单示例
填入laf应用实例的appId,然后打开你那该死的浏览器并把下面代码粘贴到控制台直接回车
const appId = ''
const wss = new WebSocket(`wss://${appId}.laf.run/__websocket__`);
wss.onopen = ()=>wss.send(JSON.stringify({
path: 'test/test',
data: {
username: '王老二',
password: 'wanglaoer'
}
}))
wss.onmessage = ({data})=>{
console.log(JSON.parse(data))
}
测试结果:
4.2 Vue应用示例
把你的Vue框架跑起来,我这边用vite创建了个vue3应用
装个依赖
npm i @weicup/websocket
App.vue下面搞上这些代码,记得把appId填上
<script>
import Websocket from "@weicup/websocket";
const appId = '';
const ws = new Websocket(`wss://${appId}.laf.run/__websocket__`);
ws.send('test/test', {
username: '王老二',
password: 'wanglaoer'
}).then((rs) => {
console.log(rs);
});
</script>
测试结果: