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官方介绍
Socket.io 是一个业界常用库,可用于在浏览器和服务器之间进行实时,双向和根据事情的通讯。
装置依赖
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
完成了两个办法 chatList
和 getUserList
- chatList 获取和当时谈天目标的谈天记载,例如我想和张三谈天,那么这个办法会返回我和张三的一切谈天记载
- getUserList 获取用户列表(相似通讯录列表),这儿获取的是user表里除自己以外一切用户并查询各自最新的一条谈天记载
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
}
}
主要完成了三个办法
-
chatList
和getUserList
办法返回的是上面事务层的谈天记载列表和通讯录列表 -
gotMessage
办法的功用是接纳/发送音讯- 接纳 发送者id,发送目标id,发送内容
- 将数据存到数据库中
- 用两者的id生成发送事情名称(也能够运用其他规矩),这样只要谈天两边能接纳到对应的音讯
前端相关完成
出于篇幅的原因,布局代码和样式就不贴出来了,我们能够到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()
})
几个留意点
-
在
onMounted
中与服务的树立衔接 transports 需求和客户端保持共同,假如服务端的@WSController()
装修器加了namespace,客户端也需求加上 -
在
getChatList
办法中,因为每次切换谈天目标都需求重新界说接纳音讯的topic,需求把前一次的监听移除
最终
以上仅仅谈天功用的简略模板,有许多细节的当地并没有触及,例如已读未读状况,发送图片和文件等。能够根据详细的需求扩展