Hi~,我是 一碗周,一个在舒适区垂死挣扎的前端,如果写的文章有幸可以得到你的青睐,万分有幸~
🍑 写在前面
前一段时间通过WebSocket实现了一个即时通讯聊天室,使用是Vue3+Node,那篇文章点我进入,这篇文章在上一篇文章的基础上进行一个简单的扩展,实现一个一对一即时聊天应用。
注:这篇文章完全是在上一篇的基础上进行的,直接看的话可能会懵逼。
运行效果如下图所示:
🍓 组件的编写
🍇 私聊组件的编写
这里完全是在上一篇文章的基础上进行的,这里只需要扩展一个抽屉组件就可以了,上图中私聊的组件是复用的群聊组件,实现代码如下:
<script setup lang="ts">
import { computed, ref } from 'vue'
interface Props {
modelValue: boolean
}
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const aniShow = ref(false)
const show = computed({
get() {
aniShow.value = props.modelValue
return props.modelValue
},
set(v) {
emitUpdate(v)
},
})
const emitUpdate = (v: boolean, time = 300) => {
aniShow.value = v
window.setTimeout(() => {
emit('update:modelValue', v)
}, time)
}
</script>
<template>
<div class="ywz-drawer h-screen w-screen fixed z-10 top-0" v-show="show">
<div
class="drawer-overlay absolute inset-0 z-0"
@click="emitUpdate(false)"
></div>
<Transition>
<div
v-show="aniShow"
class="drawer-content absolute right-0 top-0 cursor-default h-screen bg-base-100"
>
<slot></slot>
</div>
</Transition>
</div>
</template>
<style scoped>
.drawer-overlay {
--tw-bg-opacity: 0.4;
opacity: 0.999999;
cursor: pointer;
background-color: hsl(var(--nf, var(--n)) / var(--tw-bg-opacity));
transition: all 0.3s;
}
.v-enter-active,
.v-leave-active {
transition: all 0.3s;
}
.v-enter-from,
.v-leave-to {
transform: translateX(100%);
}
</style>
这个组件我们通过v-model
的方式实现抽屉的打开与关闭,这里还为组件的进入的退出增加了一个过渡效果。
🍅 NavHeader组件的修改
这里我们在NavHeader
组件的头部展示了用户的头像,如果存在新消息(通过new
属性来判断)则展示一个绿色的圆点,点击头像触发一个事件,告诉父组件可以进行操作,实现代码如下:
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
groupName: string
personNumber: number
userList: Map<any, any>
curUserId: string
}
interface User {
id: string
avatar: string
name: string
new: boolean
}
const props = defineProps<Props>()
const emit = defineEmits(['more'])
const handleMore = (user: User) => {
emit('more', user)
}
const users = computed<User[]>(() => {
const list: User[] = []
if (props.userList.size === 0) return []
props.userList.forEach((value, key) => {
if (key !== props.curUserId) {
list.push({
avatar: value.avatar,
id: key,
name: value.name,
new: value.new,
})
}
})
return list
})
</script>
<template>
<!-- 顶部栏 -->
<div class="navbar text-primary-content rounded-box space-x-1 h-16">
<div class="flex-1">
<a class="normal-case text-xl pl-4"
>{{ props.groupName }}({{ props.personNumber }})</a
>
</div>
<div class="flex-none avatar-list pr-4">
<div
class="avatar ml-1 cursor-pointer"
:class="item.new ? 'online' : ''"
@click="handleMore(item)"
v-for="item in users"
:key="item.id"
>
<div class="w-6 rounded-full">
<img :src="item.avatar" />
</div>
</div>
</div>
</div>
</template>
<style scoped></style>
🍍 服务端的实现
服务端这里比较好实现,主要是监听消息的发送以及告诉客户端谁给谁发送了消息,实现代码如下:
socket.on('send-user', e => {
const sendUserId = e.sendUserId
socket.to(sendUserId).emit('message-user', e)
})
- 首先我们通过监听
send-user
事件,监听用户什么时候发送私聊消息; - 发送消息后我们获取到发送给某个用户的id;
- 最后我们将这个消息通过
to
的方式指定的发出一个message-user
的事件,告诉他谁给他发的消息。
🍈 客户端的实现
现在我们就根据上面打下的基础,来编写客户端。
🍉 定义变量与基础方法
首先我们先定义变量以及数据结构,代码如下:
interface User {
name: string
avatar: string
id: string
new: boolean
}
// 控制控制的显示与隐藏
const drawerShow = ref(false)
// 与所有用户私聊的内容存储单元
const userChatData = ref<Map<string, ChatDataItem[]>>(new Map())
// 正在私聊的用户id
const chatUserId = ref('')
// 私聊聊天框的文字内容
const userMessage = ref('')
// 打开抽屉
const handleOpenDrawer = (user: typeof curUser) => {
drawerShow.value = true
}
// 私聊发送消息
const handleSendUser = (v: string) => {
const obj = {}
// 发送消息
socket.emit('send-user', obj)
}
// 监听接受消息
socket.on('message-user', (e: any) => {
})
这里我们通过数据结构来存储用户的聊天记录,这里也可以使用普通对象存储,这里我为了后续方便扩展我使用了Map
。
🍒 实现私聊窗口的打开
实现私聊窗口打开非常的容易,实现代码如下:
const handleOpenDrawer = (user: typeof curUser) => {
chatUserId.value = user.id
// 清空头像右上角的绿色圆圈
const u = userList.value.get(chatUserId.value)
if (u) {
u.new = false
}
drawerShow.value = true
}
🍐 实现私聊消息的发送
私聊发送消息几乎与群聊发送消息一致,不同的是需要传递接收消息那个人的id以及保存的位置也所有不同,实现代码如下:
const handleSendUser = (v: string) => {
const obj = {
id: Math.random().toString().split('.')[1].slice(0, 10),
name: curUser.name,
avatar: curUser.avatar,
content: v,
userId: curUser.id,
sendUserId: chatUserId.value,
}
// 在 userChatData 中新增一条数据,表示自己发送的
const type: 'me' = 'me'
// 判断是否与该用户聊过天,如果没有创建一个空的聊天记录
if (!userChatData.value.has(chatUserId.value)) {
userChatData.value.set(chatUserId.value, [])
}
// 获取聊天记录,准备添加
const _chatData = userChatData.value.get(chatUserId.value) ?? []
_chatData.push(Object.assign({}, { type }, obj))
// 清空 input box 中的内容
userMessage.value = ''
// 发出send事件,将消息发送出去
socket.emit('send-user', obj)
}
🍏 实现消息的接收
接受消息也比较简单,首选确定是谁发送的消息,然后将其添加到指定用户的聊天记录中即可。
socket.on('message-user', (e: any) => {
const msg = Object.assign({}, e, { type: 'your' }) as ChatDataItem
const sendId = e.userId
if (!userChatData.value.has(sendId)) {
userChatData.value.set(sendId, [])
}
const chatData = userChatData.value.get(sendId) ?? []
chatData.push(msg)
// 设置小绿点
const u = userList.value.get(sendId)
if (u) {
u.new = true
}
})
🥭 写在最后
到这为止我们就实现了简单的私聊功能,由于该篇文章与上一篇文章【你的第一个socket应用】Vue3+Node实现一个WebSocket即时通讯聊天室,有非常强的联系,所有需要先阅读过前面的那一篇。
上面的代码还有好多的可以优化的点,比如在群聊中点击头像可以私聊等(这个功能我已经实现,可以在GitHub中找到参考)。
创作不易,如果可以三连支持一下,你的三连是我持续输出的动力😜😜