这篇文章主要是用WebSocket技术实现一个即时通讯聊天室,首先先要了解为什么使用WebSocket而不是普通的HTTP协议,如果使用HTTP协议它是下面这种情况:
这个就可以通过WebSocket去解决,本篇文章包括的内容如下:
项目的源代码在Github中,项目采用pnpm+Monorepo的方式搭建,如何搭建一个Monorepo项目可以参考从0开始使用pnpm构建一个Monorepo方式管理的demo 。
文本所用到的技术如下:
服务端:
socket.io@4.15.1* nodemon@2客户端
vue@3.2* vite@3.0* tailwindcss@3.1.6* daisyui@2.19(关于这个UI组件库可以参考daisyUI快速上手,解决TailwindCSS疯狂堆砌class的问题)🍏 什么是WebSocket
WebSocket是另一种网络协议,但没有完全脱离HTTP,握手阶段采用的就是HTTP协议,这么做的好处就是不易被屏蔽,能通过各种HTTP代理服务器;
WebSocket最大的特点就是服务器可以主动向客户端推送消息,当然,客户端也可以主动的向服务器发送消息。而普通的HTTP协议只能由客户端向服务器发送,服务器根据内容进行返回。
通信过程如下图:
首先我们使用Vue+daisyUI搭建一下静态页面。
这里静态页面用啥都能写,我图省事选择了daisyUI,想要了解可以通过我上一篇文章,简单的介绍。
这里我将聊天部分主要拆分3个组件,如下图所示:
这里对这几个组件的思路进行讲解,源代码可以去GitHub中获取。
这个组件比较简单,没有什么复杂的,将群聊名称和群聊人数通过父级传递过去就好,Props定义如下:
interface Props {groupName: stringpersonNumber: number
}
组件的代码比较简单,这里就不列出占篇幅了,如果需要可以从Github中获取。
这里为了省事,把聊天的消息和进群退群的通知封装到一个组件,通过不同的type进行划分,类型定义如下:
export interface ChatDataItem {type: 'your' | 'me' | 'tips'id: string // 这条消息唯一的idname?: string // 用户名称content: string // 聊天内容 || 提示内容avatar?: string // 头像userId?: string // 用户的id
}
interface Props {chatData: ChatDataItem[]
}
type属性值如果为me表示自己发送的消息,如果为your则表示对面发送的消息,要是为tips则表示进群退群的提示。
这个组件就更简单了,就是一个输入框,一个发送按钮,有个细节就是在输入框中按下回车可以触发与按下按钮相同的事件,这个在Vue中特别简单,就是通过keyup事件的enter修饰符即可,写法如下:
<inputtype="text"v-model="value"@keyup.enter="handleSend"
/>
这个组件是加入弹框组件,我使用的是Vue,如果你用的是小程序或者H5的话这个组件做成一个页面会更好一些。
这里我就做了最简单的一版,头像是随机的,然后关闭弹框后将头像以及名称通过事件的方式进行返回,其中<script>代码如下:
import { ref } from 'vue'
import avatarList from './../assets/avatar'
export interface JoinEvent {name: stringavatar: string
}
const aList = [...avatarList]
const emits = defineEmits({// 校验 join 事件join: (e: JoinEvent) => {const { name, avatar } = eif (name && avatar) {return true} else {console.warn('未输入名字~')return false}},
})
const name = ref('')
const isOpen = ref(true)
const handleJoin = () => {// 随机头像const randomIndex = Math.floor(Math.random() * aList.length)const avatar = aList[randomIndex]emits('join', { name: name.value, avatar })isOpen.value = false
}
前面我们编写了很多组件,这里我们将组件组合起来进行展示,示例代码如下:
<script setup lang="ts">
import { reactive, ref } from 'vue'
import MainContainer from './components/MainContainer.vue'
import NavHeader from './components/NavHeader.vue'
import ChatItem, { ChatDataItem } from './components/ChatItem.vue'
import InputBox from './components/InputBox.vue'
import JoinModal, { JoinEvent } from './components/JoinModal.vue'
// 聊天数据
const chatData = ref<ChatDataItem[]>([])
// 当前用户
const curUser = reactive({ name: '', avatar: '', id: '', })
// 用户列表
const userList = ref(new Map())
const message = ref('')
const handleSend = (v: string) => {console.log(v) // v 即要发送的数据message.value = ''
}
const handleJoin = (e: JoinEvent) => {console.log(e) // 要加入的用户
}
</script>
<template><!-- 外层容器 --><MainContainer><!-- 顶部栏 --><NavHeader :group-name="'甜粥铺'" :person-number="userList.size" /><!-- 内容区域 --><div class="px-4"><ChatItem :chat-data="chatData" /></div><InputBox v-model="message" @send="handleSend" /></MainContainer><JoinModal @join="handleJoin" />
</template>
<style scoped></style>
运行效果如下图:
这里我使用的是socket.io和socket.io-client,这个操作的话会更简便一些,代码更简单一些,首先看一个小demo:
服务端代码如下:
import { Server } from 'socket.io'
// 开启cors跨域 https://socket.io/docs/v4/handling-cors/
const io = new Server(5432, { cors: true })
io.on('connection', socket => {console.log('连接成功')// receive a message from the clientsocket.on('send', e => {console.log(e)socket.emit('back', '服务器返回的消息')})socket.on('disconnecting', () => {console.log('用户离开,连接断开')})
})
首先需要保证已经安装了
socket.io这个依赖
这里我们监听connection这个事件,如果连接成功会触发这个回调函数,回调函数中有个socket实例,其中包含很多属性和方法,其中有一个id,用于表示这个连接唯一的标识。
服务端监听send,当客户端有消息进来则发出一个back事件,在客户端那边进行监听;
如果关闭这个连接,在客户端会发出一个disconnecting事件,服务器监听并作出响应。
客户端代码如下:
<script setup lang="ts">
import { io } from 'socket.io-client'
// 创建 socket 实例
const socket = io('ws://localhost:5432')
const send = () => {socket.emit('send', '来自客户端的消息')
}
socket.on('back', e => {console.log(e)
})
</script>
<template><button class="btn btn-success" @click="send">发送</button>
</template>
<style scoped></style>
创建io示例的过程中进行与服务端的socket进行连接,整个过程如下所示:
send后,发出back事件,客户端输出***** 最后关闭标签页断开连接,服务器输出****。### 🥝 实现用户登入,保存状态首先我们先实现服务端,服务端相对来说比较简单,实现代码如下:
import { Server } from 'socket.io'
const io = new Server(5432, { cors: true })
let userList = new Map()
io.on('connection', socket => {// 监听加入用户加入socket.on('join', e => {userList.set(socket.id, e)// 加入成功后返回加入成功的事件socket.emit('joined', Object.assign({}, e, { id: socket.id }))})
})
这里监听join事件,加入后将数据存储前面定义的map中,然后发出一个joined事件表示用户已经成功加入。
客户端的话需要在弹框关闭后发送join事件给服务端,然后监听joined事件并进行存储加入的用户的数据,实现代码如下:
import { io } from 'socket.io-client'
// 创建 socket 实例
const socket = io('ws://192.168.0.103:5432')
const curUser = reactive({name: '',avatar: '',id: '',
})
// 发送加入事件
const handleJoin = (e: JoinEvent) => {socket.emit('join', Object.assign({}, e))
}
// 监听加入成功的事件
socket.on('joined', (e: typeof curUser) => {curUser.avatar = e.avatarcurUser.id = e.idcurUser.name = e.name
})
编写完成进行测试,我们加入以后可以发现在加入成功后curUser的数据发送了变化。
用户加入后欢迎实现非常简单,在加入成功后去发出事件,然后在客户端监听这个事件就好,实现代码如下:
服务端
import { Server } from 'socket.io'
const io = new Server(5432, { cors: true })
let userList = new Map()
io.on('connection', socket => {// 监听加入用户加入socket.on('join', e => {userList.set(socket.id, e)// 加入成功后返回加入成功的事件socket.emit('joined', Object.assign({}, e, { id: socket.id }))const uList = [...userList.entries()]// 触发广播socket.broadcast.emit('welcome', {...e,uList,})// 自己展示加入的信息socket.emit('welcome', {...e,uList,})})
})
这里发出了两次welcome事件,这是因为第一次是广播,发送给除自己外的所有人,第二次是仅仅发送给自己。
客户端实现只需要往chatData中push数据即可,代码如下:
// 监听 welcome
socket.on('welcome', ({ name, uList }) => {// 将当前群聊中的成员保存到uList中uList.forEach((item: any[]) => {const [id, value] = itemuserList.value.set(id, value)})// 在消息卡片中展示欢迎信息chatData.value.push({type: 'tips',id: Math.random().toString().split('.')[1].slice(0, 10),content: '欢迎' + name + '加入群聊~',})
})
此时加入一个成员即可展示对应的信息。
这里我们实现一下消息的发送以及接受展示,服务端只需将收到的消息广播出去即可,服务端代码如下:
// 监听消息发送
socket.on('send', e => {// 接受到消息给他广播出去socket.broadcast.emit('message', e)
})
客户端代码如下:
// 点击发送按钮或者在输入框中键入回车
const handleSend = (v: string) => {const obj = {id: Math.random().toString().split('.')[1].slice(0, 10),name: curUser.name,avatar: curUser.avatar,content: v,userId: curUser.id,}// 在 chatData 中新增一条数据,表示自己发送的const type: 'me' = 'me'chatData.value.push(Object.assign({}, { type }, obj))// 清空 input box 中的内容message.value = ''// 发出send事件,将消息发送出去socket.emit('send', obj)
}
// 监听消息的广播
socket.on('message', (e: any) => {const msg = Object.assign({}, e, { type: 'your' }) as ChatDataItemchatData.value.push(msg)
})
这里的发送消息其实就是如何往数组中push数据。
最后我们来实现一下用户退出的播报功能,首先我们在服务端监听disconnecting事件的触发,如果触发则将用户在用户列表中删除并发出一个quit事件,在客户端进行展示。
服务端代码如下:
// 用户离开
socket.on('disconnecting', () => {const bool = userList.delete(socket.id)// 如果有用户离开,在进行广播(因为只打开页面不进入关闭页面也会触发这个事件)bool && socket.broadcast.emit('quit', socket.id)
})
客户端代码如下:
// 监听退出
socket.on('quit', (id: string) => {const user = userList.value.get(id)userList.value.delete(id)chatData.value.push({type: 'tips',id: Math.random().toString().split('.')[1].slice(0, 10),content: user.name + '退出群聊~',})
})
到这为止我们就把所有的代码全部写完了,现在就与开头的动图中实现的效果是一致的。
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:




文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取
我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何
我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>
如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象
关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?
我正在编写一个方法,它将在一个类中定义一个实例方法;类似于attr_accessor:classFoocustom_method(:foo)end我通过将custom_method函数添加到Module模块并使用define_method定义方法来实现它,效果很好。但我无法弄清楚如何考虑类(class)的可见性属性。例如,在下面的类中classFoocustom_method(:foo)privatecustom_method(:bar)end第一个生成的方法(foo)必须是公共(public)的,第二个(bar)必须是私有(private)的。我怎么做?或者,如何找到调用我的cust
我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案
我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b
我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的rubyyaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir