github仓库地址

运用技能

midwayjs + typeorm + redis

现有功用

  • 登录注册、验证码
  • 用户办理
  • 人物办理
  • 权限办理
  • 文件模块、excel导入导出
  • swagger
  • 谈天功用

详细运用

克隆代码到本地

git clone https://github.com/vangleer/es-midway-admin.git

发动后端

需提前装置mysql和redis,导入默许数据 src/entity/init.sql(默许路由、装备等)

留意:没装置 redis 需求把任务行列相关功用注释

cd es-midway-admin
yarn
yarn dev

发动前端

cd web
yarn
yarn dev

访问 http://localhost:3001/

midwayjs基础后台管理系统(七)-即时通讯、使用SocketIO开发聊天功能

midwayjs 项目引进 SocketIO

midwayjs官方介绍

Socket.io 是一个业界常用库,可用于在浏览器和服务器之间进行实时,双向和根据事情的通讯。

midwayjs基础后台管理系统(七)-即时通讯、使用SocketIO开发聊天功能

装置依赖

npm i @midwayjs/socketio@3 --save
## 客户端可选
npm i @types/socket.io-client socket.io-client --save-dev

开启组件

import { Configuration } from '@midwayjs/core'
import * as socketio from '@midwayjs/socketio'
@Configuration({
  imports: [socketio],
  // ...
})
export class MainConfiguration {
  async onReady() {
    // ...
  }
}

Socket.io 完成了两种 Transports(传输办法)

第一种是 HTTP 长轮询。HTTP Get 请求用于 long-running(长衔接),Post 请求用于 short-running(短衔接)。

第二种是 WebSocket 协议,直接根据 WebSocket Connection 完成。它在服务器和客户端之间供给了双向且低延迟的通讯通道。

咱们这儿运用的是第二种办法 WebSocket,需求增加一下装备

// src/config/config.default.ts
import { MidwayConfig, MidwayAppInfo } from '@midwayjs/core'
export default (appInfo: MidwayAppInfo) => {
  return {
    // ...
    socketIO: {
      transports: ['websocket'],
      // cors: { // 长轮询开启跨域
      //   origin: '*'
      // }
    }
  } as MidwayConfig
}

目录结构

下面是 Socket.io 项目的基础目录结构,和传统应用相似,咱们创建了 socket 目录,用户存放 Soscket.io 事务的服务代码。

.
├── package.json
├── src
│   ├── configuration.ts          ## 进口装备文件
│   ├── interface.ts
│   └── socket                      ## socket.io 服务的文件
│       └── hello.ts
├── test
├── bootstrap.js                  ## 服务发动进口
└── tsconfig.json

基本运用

  • 服务端
// src/socket/hello.ts
import { WSController, OnWSConnection, Inject, OnWSMessage, WSEmit } from '@midwayjs/core'
import { Context } from '@midwayjs/socketio'
@WSController('/hello')
export class HelloSocketController {
  @Inject()
  ctx: Context
  /**
   * 监听衔接
   */
  @OnWSConnection()
  async onConnectionMethod() {
    console.log('on client connect', this.ctx.id)
  }
  /**
   * 接纳音讯
   */
  @OnWSMessage('words')
  @WSEmit('words')
  async gotMessage(data) {
    console.log('client data: ', data)
    const message = 'hello world from server'
    this.ctx.broadcast.emit('words', message)
    return message
  }
}

Socket.io 是经过事情的监听办法来获取数据。Midway 供给了 @OnWSMessage() 装修器来格式化接纳到的事情,每次客户端发送事情,被修饰的办法都将被执行。

当获取到数据之后,经过事务逻辑处理数据,然后将成果返回给客户端,返回的时候,咱们也是经过另一个事情发送给客户端。

经过 @WSEmit 装修器来将办法的返回值返回给客户端。

  • 客户端写法
import { onMounted, ref } from 'vue'
import { io, Socket } from 'socket.io-client'
const socket = ref<Socket | null>(null)
onMounted(() => {
  // 树立websocket衔接
  socket.value = io('ws://127.0.0.1:7001/hello', { transports: ['websocket'] })
  // 衔接成功回调
  socket.value.on('connect', () => {
    console.log('client connected')
    socket.value?.emit('words', 'hello world from client')
  })
  // 监听音讯
  socket.value.on('words', (data) => {
    console.log(data)
  })
})

上面是 Socket.io 在midwayjs中的基本运用,更详细用法请参阅官方教程,接下来咱们运用 Socket.io 来完成一个简略的谈天系统

谈天服务端

chat 表设计

chat 表包含字段有 发送人id、接纳人id、发送内容、音讯类型 (0:文字 1:图片 2:文件)、状况 (0:未读 1:已读)

新建 src/entity/chat.ts

import { Entity, Column } from 'typeorm'
import { BaseEntity } from './base'
@Entity('chat')
export class Chat extends BaseEntity {
  @Column({ comment: '发送人id', type: 'int' })
  fromUserId: number
  @Column({ comment: '接纳人id', type: 'int' })
  toUserId: number
  @Column({ comment: '发送内容', length: 100 })
  content: string
  @Column({ comment: '音讯类型 0:文字 1:图片 2:文件', default: 0, type: 'tinyint'  })
  type: string
  @Column({ comment: '状况 0:未读 1:已读', default: 0, type: 'tinyint'  })
  status: string
}

chat 事务完成如下,新建 src/service/chat.ts

import { Provide, Inject } from '@midwayjs/core'
import { InjectEntityModel } from '@midwayjs/typeorm'
import { Not, Repository } from 'typeorm'
import { BaseService } from './base'
import { Chat } from '../entity/chat'
import { User } from '../entity/user'
import { Context } from '@midwayjs/web'
@Provide()
export class ChatService extends BaseService<Chat> {
  @InjectEntityModel(Chat)
  entity: Repository<Chat>
  @InjectEntityModel(User)
  userEntity: Repository<User>
  @Inject()
  ctx: Context
  /**
   * 获取和当时谈天目标的谈天记载
   * @param data 用户id和谈天目标id
   * @returns
   */
  async chatList(data) {
    const { fromUserId, toUserId } = data
    // 查询的是 我和你或者你和我的一切谈天记载
    const list = await this.entity
      .createQueryBuilder()
      .where('(fromUserId = :fromUserId AND toUserId = :toUserId) OR (fromUserId = :toUserId AND toUserId = :fromUserId)', { fromUserId, toUserId })
      .getMany()
    return list
  }
  /**
   * 获取用户列表(通讯录)
   * @param userId
   * @returns
   */
  async getUserList(userId) {
    const userList = await this.userEntity.find({ where: { id: Not(userId) } })
    const list = []
    // 遍历查询每个用户最新一条记载
    for (let i = 0; i < userList.length; i++) {
      const user = userList[i]
      const message = await this.entity
        .createQueryBuilder()
        .where('fromUserId = :userId OR toUserId = :userId', { userId: user.id })
        .orderBy('createTime', 'DESC')
        .getOne() || {}
      list.push({ ...user, message })
    }
    return list
  }
}

ChatService 完成了两个办法 chatListgetUserList

  • chatList 获取和当时谈天目标的谈天记载,例如我想和张三谈天,那么这个办法会返回我和张三的一切谈天记载

midwayjs基础后台管理系统(七)-即时通讯、使用SocketIO开发聊天功能

  • getUserList 获取用户列表(相似通讯录列表),这儿获取的是user表里除自己以外一切用户并查询各自最新的一条谈天记载

midwayjs基础后台管理系统(七)-即时通讯、使用SocketIO开发聊天功能

chat 控制层,新建 src/socket/chat.ts

import { WSController, OnWSConnection, Inject, OnWSMessage, WSEmit } from '@midwayjs/core'
import { Context } from '@midwayjs/socketio'
import { ChatService } from '../service/chat'
@WSController()
export class HelloSocketController {
  @Inject()
  ctx: Context;
  @Inject()
  service: ChatService
  @OnWSConnection()
  async onConnectionMethod() {
    console.log('on client connect')
  }
  /**
   * 谈天发送记载
   */
  @OnWSMessage('chat')
  @WSEmit('chat')
  async gotMessage(data) {
    const { fromUserId, toUserId, content } = data
    if (!fromUserId || !toUserId) return []
    const chatInfo = { fromUserId, toUserId, content }
    await this.service.add(chatInfo)
    // 运用谈天两边的id树立事情名称
    const ids = [fromUserId, toUserId]
    ids.sort((a, b) => a - b)
    const topic = `${ids[0]}-chat-${ids[1]}`
    // 发送给除了发送者谈天用户
    this.ctx.broadcast.emit(topic, chatInfo)
    // 这儿返回的是给发送者
    return chatInfo
  }
  /**
   * 谈天者和谈天目标的记载
   */
  @OnWSMessage('chatList')
  @WSEmit('chatList')
  async chatList(data) {
    const list = await this.service.chatList(data)
    return list
  }
  /**
   * 谈天目标列表(通讯录)
   */
  @OnWSMessage('userList')
  @WSEmit('userList')
  async getUserList(data) {
    const list = await this.service.getUserList(data.userId)
    return list
  }
}

主要完成了三个办法

  • chatListgetUserList 办法返回的是上面事务层的谈天记载列表和通讯录列表

  • gotMessage 办法的功用是接纳/发送音讯

    1. 接纳 发送者id,发送目标id,发送内容
    2. 将数据存到数据库中
    3. 用两者的id生成发送事情名称(也能够运用其他规矩),这样只要谈天两边能接纳到对应的音讯

midwayjs基础后台管理系统(七)-即时通讯、使用SocketIO开发聊天功能

前端相关完成

出于篇幅的原因,布局代码和样式就不贴出来了,我们能够到github仓库获取,这儿只触及逻辑完成

  • 装置 socket.io 客户端
npm i socket.io-client
  • web/src/components/im/IM.vue 的逻辑部分
import { ref, onMounted, shallowRef, computed, onBeforeUnmount, nextTick } from 'vue'
import { useUserStore } from '@/store'
import { io, Socket } from 'socket.io-client'
import { dayjs, ScrollbarInstance } from 'element-plus'
const userStore = useUserStore()
const message = ref<string>('')
const list = ref<any[]>([])
const userList = ref<any>([])
const chatUser = ref<any>({})
const socket = shallowRef<Socket | null>(null)
const chatTopic = ref('')
const infoRef = ref<ScrollbarInstance | null>(null)
const innerRef = ref<HTMLElement | null>(null)
const messageList = computed(() => {
  return (list.value || []).map(item => {
    return {
      ...item,
      fromUserName: userStore.username,
      toUserName: chatUser.value.username,
      self: +item.fromUserId === userStore.userid
    }
  })
})
/**
 * 点击发送
 */
function handleSendMessage() {
  if (!message.value && !message.value.trim()) return
  socket.value?.emit('chat', {
    fromUserId: userStore.userid,
    toUserId: chatUser.value.id,
    content: message.value
  })
  message.value = ''
}
/**
 * 点击谈天列表
 * @param item
 */
function handleUserClick(item) {
  chatUser.value = item
  getChatList()
}
/**
 * 获取左边列表
 */
async function getUserList() {
  socket.value?.emit('userList', { userId: userStore.userid }, (data) => {
    userList.value = data
    // 默许与列表的第一位朋友谈天
    chatUser.value = userList.value[0]
    getChatList()
  })
}
// 接纳发送的音讯
function onChatMessage (data) {
  list.value.push(data)
  setScroll()
}
/**
 * 获取和当时朋友的谈天记载
 */
function getChatList() {
  if (socket.value) {
    socket.value.emit('chatList', {
      fromUserId: userStore.userid,
      toUserId: chatUser.value.id
    }, (data) => {
      list.value = data || []
      setScroll()
    })
    // 运用谈天两边的id树立事情名称,需求和后端共同
    const ids = [userStore.userid, chatUser.value.id]
    ids.sort((a, b) => a - b)
    const topic = `${ids[0]}-chat-${ids[1]}`
    // 取消前一次的监听
    socket.value.off(chatTopic.value, onChatMessage)
    // 重新监听
    socket.value.on(topic, onChatMessage)
    chatTopic.value = topic
  } else {
    list.value = []
  }
}
function setScroll() {
  nextTick(() => {
    infoRef.value?.setScrollTop(innerRef.value?.clientHeight || 9999)
  })
}
onMounted(() => {
  // 树立websocket衔接
  socket.value = io('ws://127.0.0.1:7001', { transports: ['websocket'] })
  // 衔接后获取左边列表
  socket.value.on('connect', () => getUserList())
  // 监听自己的发送
  socket.value.on('chat', onChatMessage)
})
onBeforeUnmount(() => {
  socket.value?.close()
})

几个留意点

  1. onMounted 中与服务的树立衔接 transports 需求和客户端保持共同,假如服务端的 @WSController() 装修器加了namespace,客户端也需求加上

  2. getChatList 办法中,因为每次切换谈天目标都需求重新界说接纳音讯的topic,需求把前一次的监听移除

最终

以上仅仅谈天功用的简略模板,有许多细节的当地并没有触及,例如已读未读状况,发送图片和文件等。能够根据详细的需求扩展