使用TS装饰器从0封装一个socket.io服务器

这篇具有很好参考价值的文章主要介绍了使用TS装饰器从0封装一个socket.io服务器。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

背景

        最近沉迷WebRTC技术,想用WebRTC试做一个快启动的文件传输PWA应用。涉及到使用websocket服务器传输WebRTC的信令,全栈的话自然是选择Nodejs做服务器了。

选型

        因为项目体量肯定不是很大,所以所有选型都以高性能轻量为主要目标。前端选择的是solidjs+tailwindcss,后端的话思虑了很久,最开始想用的是fastify + ws,但是后来想了想,这个项目对HTTP的依赖程度不高,所有的资源传输都可以用websocket和WebRTC实现,所以就完全不用HTTP服务器了,又考虑到socket.io的封装程度更高,提供了Namespace、room等功能,所以根据官网介绍选择了uWebSockets.js + socket.io做服务器。

内存占用 | Socket.IO

        但是uWebSockets.js + socket.io的封装程度不好,现成框架对这俩组合的支持程度较低,所以我自己仿照nestjs的websocket服务,封装了一部分装饰器和模块的逻辑实现。

思路

        就像nestjs一样,main.ts中编写初始化框架、全局配置等的逻辑;而其他业务方面的代码,则使用模块的方式组织,模块有各自的namespace,维护namespace的事件监听、生命周期、自己内部的逻辑等,各个模块将在框架启动时实例化;

依赖及TS配置

- Nodejs v16以上,因为uWebSockets.js需要v16以上的环境

- pnpm 可选

- uWebSockets.js + socket.io + typescript + reflect-metadata

reflect-metadata是干什么用的?

        reflect-metadata库是一个对ES6 Reflect在元数据扩展方面能力的缺失而实现的一个polyfill,简单来说,就是可以通过Reflect对类、类成员添加一些元数据而不破坏这个类及类成员的构成。

TS配置

        务必开启以下两个配置,因为项目中会用到大量装饰器。

"experimentalDecorators": true,
"emitDecoratorMetadata": true

        虽然TS里面Decorator的实现已经与当前TC39 Decorator Stage3 的实现大相径庭了,但是个人项目使用一时半会儿还不要紧,不过实际项目最好做好准备,尽量不使用TS实现的Stage 2装饰器,等待原生装饰器语法成为正式标准。

代码实现

Socket.IO配合uWebSockets.js的官方示例代码

首先引入uWebSockets.js与socket.io,并将他们实例化

然后将socket.io与uWebSocket.js关联,使socket.io底层由uWebSockets.js实现

注册socket.io事件监听,执行逻辑

启动uWebSockets.js

const { App } = require("uWebSockets.js");
const { Server } = require("socket.io");

const app = new App();
const io = new Server();

io.attachApp(app);

io.on("connection", (socket) => {
  // ...
});

app.listen(3000, (token) => {
  if (!token) {
    console.warn("port already in use");
  }
});

改造

        在入口文件处,引入reflect-metadata,以启用Reflect polyfill

        初始化uWebSockets.js,开始监听端口

        关键函数constructModule,在启动时将所有主模块实例化。

import "reflect-metadata";
import { App } from "uWebSockets.js";
import { constructModule } from "./factory/constructModules";
import { AppModule } from "./src/app.module";

async function boostrap () {
  /** uWebsocket entry */
  const app = App();
  constructModule(app, [
    AppModule
  ], {
    transports: ['websocket']
  });

  const port = 3000;
  app.listen(port, token => {
    if(!token) console.warn(`port ${port} already in use`);
  });
}

boostrap();

constructModule函数

遍历传入的主模块数组,执行以下操作:

1.取出记录了需要注入的子模块的metadata;取出记录了server options的metadata

2.创建一个socket.io的Server实例,并绑定uWebSockets.js。

3.实例化所有子模块,getMetadata取得子模块的namespace配置,开始监听该namespace,如果有onConnection或onDisconnecting成员,则添加connection或disconnecting监听。

4.之后注入子模块中的server字段(注入Server实例)与namespace字段(注入Namespace实例)。

5.最后监听namespace的connect事件,在一个socket进入时为其添加模块中注明的事件监听器。

        namespace配置、server字段、socket事件监听函数应该怎么获得呢?使用装饰器,为类、类成员添加修饰,在实例化时读取到它们,然后处理。

import { Namespace, Server, ServerOptions, Socket } from "socket.io";
import { ChildModuleClass, DecoratorMetadataName, ListenersMetadata, NamespaceOptions } from "./SocketDecorators";
import { TemplatedApp } from "uWebSockets.js";

function createGlobalServer (app: TemplatedApp, serverOpt?: Partial<ServerOptions>) {
  const io = new Server(serverOpt);
  io.attachApp(app);
  return io;
}

function bindDecoratorListeners (self: ChildModuleClass, socket: Socket) {
  /** mount listener */
  const listeners: ListenersMetadata = Reflect.getMetadata(DecoratorMetadataName.EventListener, self);
  listeners?.forEach(listener => {
    socket.on(listener.name, (...args: any) => listener.listener.call(self, socket, ...args))
  });
}

function analyseDecoratorValues (self: ChildModuleClass) {
  const map = new Map<DecoratorMetadataName, string>();
  const ownKeys = Object.keys(self);
  for(let key of ownKeys) {
    /** get websocket client socket propertyKey */
    if(Reflect.getMetadata(DecoratorMetadataName.SocketProperty, self, key)) {
      map.set(DecoratorMetadataName.SocketProperty, key);
      continue;
    }
    /** get websocket namespace propertyKey */
    if(Reflect.getMetadata(DecoratorMetadataName.NamespaceProperty, self, key)) {
      map.set(DecoratorMetadataName.NamespaceProperty, key);
      continue;
    }
    /** get websocket server propertyKey */
    if(Reflect.getMetadata(DecoratorMetadataName.ServerProperty, self, key)) {
      map.set(DecoratorMetadataName.ServerProperty, key);
      continue;
    }
  }
  return map;
}

/** bind namespace and server */
function bindDecoratorValuesBeforeConnect (
  map: Map<DecoratorMetadataName, string>,
  self: ChildModuleClass,
  nsp: Namespace,
  ioInstance: Server
) {
  const nspPropertyKey = map.get(DecoratorMetadataName.NamespaceProperty);
  const serverPropertyKey = map.get(DecoratorMetadataName.ServerProperty);
  if(nspPropertyKey) {
    Reflect.set(self, nspPropertyKey, nsp);
  }
  if(serverPropertyKey) {
    Reflect.set(self, serverPropertyKey, ioInstance);
  }
}

/** bind socket */
function bindDecoratorValuesAfterConnect (
  map: Map<DecoratorMetadataName, string>,
  self: ChildModuleClass,
  socket: Socket
) {
  const socketPropertyKey = map.get(DecoratorMetadataName.SocketProperty);
  if(socketPropertyKey) {
    Reflect.set(self, socketPropertyKey, socket);
  }
}

/**
 * construct child module
 * inject provider
 */
function constructChildModule (
  modules: (new () => ChildModuleClass)[],
  io: Server
) {
  modules.forEach(ModuleItem => {
    const instance = new ModuleItem();
    const opt: NamespaceOptions = Reflect.getMetadata(DecoratorMetadataName.NamespaceModule, ModuleItem);
    const { namespace } = opt ?? {};
    const nsp = io?.of(namespace ?? '/');
    const decoratorMap = analyseDecoratorValues(instance);
    bindDecoratorValuesBeforeConnect(decoratorMap, instance, nsp, io);
    nsp.on('connection', (socket) => {
      bindDecoratorValuesAfterConnect(decoratorMap, instance, socket);
      bindDecoratorListeners(instance, socket);
      instance.onConnection?.(socket);
      socket.on('disconnect', (...args) => {
        if(instance?.onDisconnect) instance.onDisconnect.call(instance, ...args);
      });
      socket.on('disconnecting', (...args) => {
        if(instance?.onDisconnecting) instance.onDisconnecting.call(instance, ...args);
      });
    });
  });
}

/**
 * construct global instance of socket.io
 * construct module
 */
export function constructModule (
  app: TemplatedApp,
  modules: (new () => any)[],
  globalServerOpt?: Partial<ServerOptions>
) {
  modules.forEach(ModuleItem => {
    const childModules = Reflect.getMetadata(DecoratorMetadataName.ChildModules, ModuleItem);
    const serverOpts = Reflect.getMetadata(DecoratorMetadataName.ServerOptions, ModuleItem);
    const io = createGlobalServer(app, {
      ...globalServerOpt,
      ...serverOpts
    });
    constructChildModule(childModules, io);
  });
}

装饰器

        共有ServerModule、NamespaceModule、Subscribe、WebsocketServer、WebsocketNamespace四个装饰器,为类、类成员注入metadata,metadata中保存相应信息,在实例化时取出并应用。

        Subscribe装饰器会将所有监听事件放入一个数组中,而非直接为该成员添加eventName metadata,是因为class创建的类中,类方法都未挂载在this上,而是挂载在原型链上,无法通过遍历类成员获得。

import { Socket, Namespace, Server, ServerOptions } from "socket.io";

/** 注入Metadata的name */
export enum DecoratorMetadataName {
  ChildModules = '[[childmodules]]',
  ServerOptions = '[[serveroptions]]',
  NamespaceModule = '[[wsopt]]',
  EventListener = '[[listener]]',
  ServerProperty = '[[serverprop]]',
  NamespaceProperty = '[[namespaceprop]]',
  SocketProperty = '[[socketprop]]'
}

export interface ChildModuleClass {
  socket?: Socket,
  namespace?: Namespace,
  server?: Server,
  [key: string | number | symbol]: any
}
/** 定义server模块,该模块为主模块 */
export function ServerModule(childModules?: ChildModuleClass[], serverOpt?: ServerOptions): ClassDecorator {
  return function (target: Function) {
    Reflect.defineMetadata(DecoratorMetadataName.ChildModules, childModules, target);
    Reflect.defineMetadata(DecoratorMetadataName.ServerOptions, serverOpt, target);
  }
}

/** 定义namespace模块,该模块为子模块 */
export interface NamespaceOptions {
  namespace?: string;
}
export function NamespaceModule(namespaceOpt?: NamespaceOptions): ClassDecorator {
  return function (target: Function) {
    Reflect.defineMetadata(DecoratorMetadataName.NamespaceModule, namespaceOpt,target);
  }
}

/** 订阅消息装饰器 */
export type ListenersMetadata = Array<{
  name: string,
  listener: (...args: any[]) => void
}>
export function Subscribe(eventName?: string): MethodDecorator {
  return function (target: any, _, descriptor) {
    const listeners = Reflect.getMetadata(DecoratorMetadataName.EventListener, target) ?? [];
    Reflect.defineMetadata(
      DecoratorMetadataName.EventListener,
      listeners.concat([{
        name: eventName,
        listener: descriptor.value,
      }]),
      target
    );
  }
}

export function WebsocketServer(target: object, propertyKey: string | symbol) {
  Reflect.defineMetadata(DecoratorMetadataName.ServerProperty, true, target, propertyKey)
}

export function WebsocketNamespace(target: object, propertyKey: string | symbol) {
  Reflect.defineMetadata(DecoratorMetadataName.NamespaceProperty, true, target, propertyKey)
}

/**
 * 已废弃装饰器,用于注入Socket实例
 * 因Socket实例在connect事件后才会实例化,所以在模块constructor中无法获取到该实例
 */
export function SocketInstance(target: object, propertyKey: string | symbol) {
  Reflect.defineMetadata(DecoratorMetadataName.SocketProperty, true, target, propertyKey)
}

装饰器使用方法

主模块

import { ServerModule } from "../factory/SocketDecorators";
import { UserModule } from "./user/user.module";
import { ExchangeModule } from "./exchange/exchange.module";

@ServerModule(
  [UserModule, ExchangeModule],
  { pingInterval: 3000 }
)
export class AppModule {}

子模块

import { WebsocketServer, Subscribe, NamespaceModule, SocketInstance } from "../../factory/SocketDecorators";
import { Server, Socket } from "socket.io";

@NamespaceModule({ namespace: '/user' })
export class UserModule {
  @WebsocketServer server?: Server;
  @SocketInstance socket?: Socket;

  constructor() {}

  onConnection() {
    console.log('user connection', this.socket?.id);
  }

  @Subscribe('token')
  public handleToken(socket: Socket, data: any) {
    console.log('token', data);
  }
}

全部代码

        目前是未完成版本,之后可能会对其进行迭代优化

TransferS/backend at master · YThinker/TransferS · GitHub文章来源地址https://www.toymoban.com/news/detail-475812.html

到了这里,关于使用TS装饰器从0封装一个socket.io服务器的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • ts 装饰器

    使用装饰器前,需要把 tsconfig.json 中 experimentalDecorators 设置为 true 学习了小满的B站课程:https://www.bilibili.com/video/BV1wR4y1377K?p=24 ts中有几种装饰器类型: 类装饰器 ClassDecorator 方法装饰器 MethodDecorator 参数装饰器 ParameterDecorator 属性装饰器 PropertyDecorator 功能 使用场景: 不想破坏

    2024年02月08日
    浏览(22)
  • 四、axios在vite+ts使用class类二次封装

    引入需要的文件创建class类 index.ts 配置解决import.meta.env报错问题 status.ts types.ts api接口 创建element-puls.d.ts文件 完成后在script里用的弹框在ts不会报错

    2024年02月09日
    浏览(49)
  • 【TypeScript】TS进阶-装饰器(九)

    🐱个人主页: 不叫猫先生 🙋‍♂️作者简介:前端领域新星创作者、阿里云专家博主,专注于前端各领域技术,共同学习共同进步,一起加油呀! 💫系列专栏:vue3从入门到精通、TypeScript从入门到实践 📢资料领取:前端进阶资料以及文中源码可以找我免费领取 🔥社群招

    2024年02月21日
    浏览(39)
  • vue3+ts+vite中封装axios,使用方法从0到1

    http.ts文件内容: methods.ts文件内容: api/user.ts 用户相关接口 types/user.d.ts定义接口ts类型 请求成功测试

    2024年02月02日
    浏览(52)
  • 微信小程序使用TS+腾讯位置API,输入地址,解析经纬度(全局封装及调用)

    我们有时候做小程序时需要用到获取地址的经纬度,腾讯位置提供了相关API,我们可以使用其API进行调用实现功能。 1. 登录腾讯位置服务网址 腾讯位置服务 - 立足生态,连接未来 腾讯位置服务为各类应用厂商和开发者提供领先的LBS服务和解决方案;有针对Web应用的JavaScrip

    2024年02月16日
    浏览(44)
  • BMapGL -- 使用Class在 Vue3 ts 中封装创建标记和多边形标签

    useSymbol.ts 文件 MarkerLabel 使用 PolygonLabel使用 这段代码定义了两个类:MarkerLabel 和 PolygonLabel,分别用于创建标记和多边形标签,并将它们添加到地图上。 MarkerLabel 类接收一个 markConfig 对象作为参数,该对象包含创建标记所需的各种配置信息,例如标记的位置、标记的样式、标

    2024年02月15日
    浏览(30)
  • Vue3 + Vite + Ts自己封装的基础组件库发布npm ,npm安装使用(Volar )支持TS 类型提示功能(vite-plugin-dts 使用)

    在开发Vue3 + Ts项目时:使用自己二次封装的基础组件,没有 Ts类型提示 ,不能像 Element-plus 鼠标停在标签或者属性上就能提示当前组件有哪些属性(即props)及其属性的类型,如下图是 Element-plus 组件的 使用Vs Code Volar的提示: 此插件的作用:为打包的库里加入声明文件(即生

    2024年02月09日
    浏览(66)
  • 设计模式之装饰者模式-TS中装饰器介绍

    装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。 装饰器使用@expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入 装饰器分类 装饰器大体上分为: 方法装饰器 类装饰器 属性装饰器

    2024年02月12日
    浏览(30)
  • 如何建设一个用于编译 iOS App 的 macOS 云服务器集群?

    作者:京东零售 叶萌 现代软件开发一般会借助 CI/CD 来提升代码质量、加快发版速度、自动化重复的事情,iOS App 只能在 mac 机器上编译,CI/CD 工具因此需要有一个 macOS 云服务器集群来执行 iOS App 的编译。 今天就来谈谈如何建设 macOS 云服务器集群 最简单的方式就是购买一批

    2023年04月25日
    浏览(44)
  • Vue3.2+TS在v-for的时候,循环处理时间,将其变成xx-xx-xx xx:xx:xx格式,最后教给大家自己封装一个时间hooks,直接复用

    Vue3.2+TS在v-for的时候,循环处理时间,将其变成xx-xx-xx xx:xx:xx格式 最后教给大家自己封装一个时间hooks,直接复用 1.没有封装,直接使用 假如我们现在其他地方还需要用到这个,那么我们可以把这个方法封装成一个hooks,用以复用 1.在src目录下,新建一个utils文件夹,定义一个

    2024年02月13日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包