日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > vue >内容正文

vue

撸一个聊天室(vue+koa2+websokect+mongodb)

發布時間:2024/1/8 vue 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 撸一个聊天室(vue+koa2+websokect+mongodb) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

擼一個聊天室(vue+koa2+websokect+mongodb)

本篇博客主要介紹聊天室項目,作者學習vue和node時間較短,若有什么錯誤或建議,歡迎指出,謝謝~


貼上源碼鏈接 -> 源碼

vue的布局在這就不說了,大家可以查看源碼。

效果圖如下:


首先來看一個聊天室需要哪些功能:

  • 發消息(單聊)
  • 添加好友
  • 發表情和圖片
  • 發文件
  • 這三個算是聊天室需要的最基本的功能,其他功能還可以自己拓展,在我的源碼中還實現了離線消息、禁止多端登錄以及富文本功能。首先需要了解的是如何在vue中使用webSocket。

    // 在vue中使用webSocket,需要使用vue-socket.io // 下載 npm i vue-socket.io -D// main.js import VueSocketio from 'vue-socket.io' Vue.use(VueSocketio, 'http://localhost:8888/') // 第二個參數為服務端地址 // 在vue中定義socket屬性 sockets: { // 不能改// 定義服務端需要調用的方法getOnlineNum: function (num) {this.onlineNum = num} }// 調用方法 this.$socket.emit('signIn', 'username')

    服務端使用的是koa2 + socket.io

    const app = new Koa()const server = require('http').createServer(app.callback()) const io = require('socket.io')(server) server.listen(process.env.PORT) // 監聽端口號// socket // 每個用戶登錄時,將他們的用戶名和socketID通過鍵值對存起來,方便單聊時向指定用戶發送 // 通過用戶獲取socketID,通過socketID獲取用戶名 let user = {} // 鍵值對:username: socketID let socketID = {} // 鍵值對: socketID: username// socket連接 io.on('connection', function (socket) {// 注冊signIn方法,在vue登錄時調用socket.on('signIn', function (username) {user[username] = socket.idsocketID[socket.id] = username// 獲取當前在線人數io.sockets.emit('getOnlineNum', Object.keys(socketID).length)})// 斷開連接socket.on('disconnect', function () {const username = socketID[socket.id] // 獲取下線的用戶delete socketID[socket.id]delete user[username]io.sockets.emit('getOnlineNum', Object.keys(socketID).length)console.log(username + '下線')}) })

    其實webSocket的用法就是在雙向調用,客戶端注冊服務端要調用的方法,服務端注冊客戶端要調用的方法。

    實現單聊

    編寫單聊功能時需要注意,需要給輸入框綁定當前聊天好友的消息變量,沒有好友時單獨綁定一個變量。

    <textarea v-if="friends.length>0" @keyup.enter="send()" v-model="friends[nowChat].textmsg"></textarea> <textarea v-else @keyup.enter="send(textmsg)" v-model="textmsg"></textarea> // 點擊發送按鈕 send () {// 判斷是否有好友if (this.friends.length > 0) {// 參數:1.方法名,2.消息,3.來自哪個用戶,4.發送給哪個用戶this.$socket.emit('receive', this.friends[this.nowChat].textmsg, this.curUsername, this.friends[this.nowChat].username)} else {alert('你還沒有好友,先去加好友吧')} }// 服務端接收 socket.on('receive', function (msg, from, to) {let date = new Date().toTimeString().substr(0, 8) // 記錄時間let socketId = user[to] // 查找接收方的socketIDlet meSocketId = user[from] // 查找發送方的socketID// 給自己發送一條io.sockets.sockets[meSocketId].emit('newMsg', {from: from, to: to, msg: msg, date: date})// 判斷接收方是否在線if (socketId) {// 在線就直接發送io.sockets.sockets[socketId].emit('newMsg', {from: from, to: to, msg: msg, date: date})}// 將消息存儲到數據庫DBModule.NewsList.addNews({from: from, to: to, msg: msg, date: date}) })// 客戶端在頁面掛載時請求獲取當前用戶的所有消息 getNewsList () {this.$axios.post('/getNews', {username: this.curUsername}).then(res => {this.newMsg = res.data.data}).catch(err => console.log(err)) } mounted () {this.getNewsList() }

    添加好友

    用戶一端添加好友時,就將雙方好友信息互存起來,再更新雙方的頁面的好友信息。

    // vue addFriend () {this.$axios.post('/addFriend', {username: this.curUsername,friend: this.friend}).then(res => {if (res.data.status === 200) {// 將存在store里的好友信息更新this.$store.commit('updateFriends', res.data.data)this.friends = res.data.data// 通過webSocket更新對方的好友信息this.$socket.emit('updateFriends', this.curUsername, this.friend)}}).catch(err => {console.log(err)}) }// 服務端 socket.on('updateFriends', function (username, friend) {let socketId = user[friend]if (socketId) {io.sockets.sockets[socketId].emit('updateFriends', username)} })// vue中更新好友方法,寫在socket中,服務端調用 updateFriends: function (data) {this.friends.push({username: data,textmsg: ''}) }

    以上屬于強行加好友系列。

    還可實現添加好友通知功能,只有對方用戶同意添加后才能添加。

    做一個簡單的富文本編輯框

    一開始輸入框只想到了textarea,但在textarea中只能顯示文本,那如何做到像qq一樣可以將圖片顯示到輸入框呢?經學長提示就決定使用“contenteditable”屬性,將標簽的“contenteditable”屬性設置為true就可以使不可輸入元素變為可輸入元素。

    <pre contenteditable="true" ref="editDiv" v-html="innerText" @keyup.enter="sendMsg"@input="changeTxt" @focus="lock=true" @blur="lock=false"></pre> export default {props: {child: {type: String,default: ''}},data () {return {innerText: this.child,lock: false}},watch: {child: {handler (newValue, oldValue) {if (!this.lock) {this.innerText = this.child}},deep: true}},methods: {changeTxt () {this.$emit('updateMsg', this.$refs.editDiv.innerHTML)},blur () {this.lock = false},setInnerText () {this.$refs.editDiv.innerHTML = ''this.innerText = this.child},sendMsg () {this.$emit('send')}} }// 父組件的方法,更新消息 updateMsg (msg) {if (this.friends.length > 0) {this.friends[this.nowChat].textmsg = msg} else {this.textmsg = msg} }

    當然,這里也可以用除pre以外的標簽。還記得上面將輸入框的消息與對應的好友消息綁定吧?這里我們依然需要實現雙向綁定,將富文本編輯框作為一個子組件,接收父組件傳過來的消息,展示在編輯框內。這里需要注意一個問題:此時若是去掉lock,就會發生輸入字符不匹配以及光標置于最前的問題。參考博客才解決這個問題。實際上這樣寫,“this.innerText = this.child”只執行了一次,而setInnerText方法將innerText重新賦值。

    發表情和圖片

  • 發表情
    表情包是我們自己提供的,所以一開始就將圖片的放在服務端,這里就需要配置靜態服務器,讓客戶端可以通過鏈接獲取到服務端的資源。
  • // 服務端 const convert = require('koa-convert') const koaStatic = require('koa-static') // 由于koa-static目前不支持koa2,所以只能用koa-convert封裝一下,配置資源所在路徑。 app.use(convert(koaStatic(path.join(__dirname, `../static`) )))

    此時就可以通過鏈接訪問服務端端資源,例如:localhost:8888/emoji/1.gif,static目錄不用寫。

    // 傳入表情的名字,服務端返回文件的url addEmoji (index) {this.$axios.post('/searchEmojiUrl', {emojiId: index}).then(res => {if (this.friends.length > 0) {this.friends[this.nowChat].textmsg += '<img src="' + res.data.data.emojiUrl + '" alt="emoji">'} else {this.textmsg += '<img src="' + res.data.data.emojiUrl + '" alt="emoji">'}this.emojiShow = false}).catch(err => console.log(err)) }
  • 發圖片
    發圖片與發表情一個很重要的區別是表情一開始就在服務器上,而圖片不是,所以在獲取圖片的url之前,需要先將圖片上傳服務器。

    然而在富文本編輯框中會發現用戶可以通過退格鍵刪除圖片,那么如何只將用戶點擊發送那一刻編輯框內所有的圖片上傳服務器,防止服務器接收過多無用的圖片。

    經過很長的思考及詢問,我決定采用暴力法。


    思路:先把點擊上傳以后的全部圖片的名字和文件對象全部存在一個數組內,插入到富文本時將文件名字放在data-name屬性內,而在富文本中使用fileReader.readAsDataURL(file)將圖片解析成字符串顯示,點擊發送按鈕時操作DOM查找富文本中的img標簽,通過data-name屬性拿到文件名,再去數組里找到文件對象,以鍵值對(文件名,文件)的方式存在FormData對象中。在發送消息之前將FormData對象發送給服務器,服務器存儲后返回鍵值對(文件名,文件URL),再對img標簽的src進行賦值,賦值為服務器端的地址。最后發送消息字符串。
  • // 在data中添加imgArr: [],保存每次輸入添加的所有圖片信息 send () {// 暴力操作DOM獲取富文本下的img標簽let imgList = document.querySelector('.msgText').querySelectorAll('img')let formData = new FormData() // 存儲要上傳到服務器的圖片let isNull = truefor (let i = 0; i < imgList.length; i++) { // 獲取消息中剩余的圖片this.imgArr.map((ele, idx) => {if (ele.name === imgList[i].getAttribute('data-name')) {isNull = falseformData.append(ele.name, ele.fileObj)}})}this.imgArr = [] // 將數組清空,等待下次存儲if (this.friends.length > 0) {if (!isNull) { // 若好友不為空,且發送的圖片不為空,則發送上傳圖片的請求this.$axios.post('/uploadImg', formData, {headers: { // 傳送文件設置請求頭'Content-Type': 'multipart/form-data'}}).then(res => {if (res.data.status === 200) {for (let i = 0; i < imgList.length; i++) {for (let key in res.data.data) {if (imgList[i].getAttribute('data-name') === key) {imgList[i].src = res.data.data[key]}}}// 使用socket發送消息,這里的消息內容需要通過innerHTML獲取,因為儲存在父組件的消息字符串中img標簽的src標簽沒有替換。this.$socket.emit('receive', document.querySelector('.msgText pre').innerHTML, this.curUsername, this.friends[this.nowChat].username)this.friends[this.nowChat].textmsg = '' // 將發出的消息清空this.$refs.editPre.setInnerText() // 將當前富文本清空}})} else {this.$socket.emit('receive', document.querySelector('.msgText pre').innerHTML, this.curUsername, this.friends[this.nowChat].username)this.friends[this.nowChat].textmsg = ''this.$refs.editPre.setInnerText()}} else {alert('你還沒有好友,先去加好友吧')} }

    服務端上傳文件代碼,使用busboy上傳文件

    const path = require('path') const fs = require('fs') const Busboy = require('busboy') const inspect = require('util').inspect const baseController = require('./baseController')/*** 同步創建文件目錄* @param {string} dirname 目錄絕對地址* @return {boolean} 創建目錄結果*/ function mkdirSync (dirname) {if (fs.existsSync(dirname)) {return true} else {// path.dirname()用于獲取一個路徑中的目錄名,當參數值為目錄路徑時,// 該方法返回該目錄的上層;當參數為文件路徑時,該方法返回該文件所在的目錄if (mkdirSync(path.dirname(dirname))) {fs.mkdirSync(dirname)return true}} }/*** 獲取上傳文件的后綴名* @param {string} fileName 獲取上傳文件的后綴名* @param {string} 文件后綴名*/function getSuffixName (fileName) {let nameList = fileName.split('.')return nameList[nameList.length - 1] }/*** 上傳文件* @param {object} ctx koa上下文* @param {object} options 文件上傳參數 fileType文件類型,path文件存放路徑* @return {promise}*/ function uploadImg (ctx, options) {const req = ctx.reqvar busboy = new Busboy({ headers: req.headers })let fileType = options.fileType || 'common' // 默認文件夾為commonlet filePath = path.join(options.path, fileType)let mkdirResult = mkdirSync(filePath)if (mkdirResult) {return new Promise((resolve, reject) => {console.log('文件上傳中')let result = { // 默認返回success: false,data: {}}busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {let fileName = new Date().getTime() + '.' + getSuffixName(filename)console.log('File [' + fieldname + ']: filename: ' + filename)let _uploadFilePath = path.join(filePath, fileName)let saveTo = path.join(_uploadFilePath)// 文件保存到指定路徑file.pipe(fs.createWriteStream(saveTo)) // 先讀入再讀出來寫// 文件保存到指定路徑file.on('end', function () {console.log('File [' + fieldname + '] Finished')result.data[filename] = `http://${ctx.host}/img/${fileType}/${fileName}`})})busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated) {console.log('Field [' + fieldname + ']: value: ' + inspect(val))})busboy.on('finish', function () {result.success = trueresult.message = '文件上傳成功'console.log('文件上傳成功')resolve(result)})busboy.on('error', function (err) {console.log(err)console.log('文件上傳出錯')})req.pipe(busboy)})} }// 調用方法 async (ctx, next) => {let serverFilePath = path.join(__dirname, '../static/img')const result = await uploadImg(ctx, {fileType: 'album',path: serverFilePath})if (result.success === true) {ctx.body = { status: 200, msg: '上傳成功', data: result.data }} else {ctx.body = { status: 401, msg: '上傳失敗' }} }

    文件上傳

    文件上傳同圖片上傳類似,沒有圖片那么復雜,點擊文件上傳按鈕直接上傳即可。在消息中顯示為[file:文件地址],在消息顯示時解析成你想要的結果。而文件下載功能,可以通過a標簽,每一個a標簽都會發出一個請求。

    // 解析每一條消息 changeMsg (msg) {let fileName = ''let result = msglet fileReg = /\[file:.+?\]/glet fileMatch = fileReg.exec(msg)let fileType = ''while (fileMatch) {fileName = fileMatch[0].slice(6, -1)console.log(fileName)var index = fileName.lastIndexOf('/')var str = fileName.substring(index + 1, fileName.length)var strFileName = str.replace(/^.+?\\([^\\]+?)(\.[^.\\]*?)?$/gi, '$1')var fileExt = str.replace(/.+\./, '').toLowerCase()if (fileExt === 'doc') {fileType = 'word'} else if (fileExt === 'xlsx') {fileType = 'excel'} else {fileType = 'file'}result = result.replace(fileMatch[0], `<p class="fileMsg"><img src="../../static/img/${fileType}.png" ><span>${strFileName}</span><a href="${fileName}" class="downloadBtn">下載</a></p>`)fileMatch = fileReg.exec(msg)}return result }

    小結

    在寫代碼過程中,遇到過許多問題,也不斷優化改進,以后也會實現新的功能。如果有什么建議,歡迎提出。再次貼出源碼地址,如果對你有幫助,請給我的倉庫一個star ?,非常感謝。

    總結

    以上是生活随笔為你收集整理的撸一个聊天室(vue+koa2+websokect+mongodb)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。