前言
項(xiàng)目開(kāi)始是因?yàn)楣ぷ餍枰粋€(gè)聊天室功能,但是因?yàn)槟承┰蜃罱K選用的是基于xmpp協(xié)議的Strophe.js寫的。于是就想用node自己寫一套,本來(lái)只是想簡(jiǎn)單的寫個(gè)聊天頁(yè)面,但是寫完了又不滿意,所以不斷的重構(gòu)(似乎可以理解產(chǎn)品經(jīng)理為什么老是改需求了?乛?乛?)。很多東西,比如mongodb,我也是第一次用,以前只接觸過(guò)mysql。所以都是一邊學(xué)一邊寫,利用工作之余的時(shí)間,斷斷續(xù)續(xù)的寫了幾個(gè)月(這次講的是V0.9.0版本,項(xiàng)目還在更新中···),包含了一整套的前后端交互。uI是按照自己的感覺(jué)來(lái)的,沒(méi)有設(shè)計(jì)天分(話說(shuō)主題切換到現(xiàn)在還只有一套主題,實(shí)在是不好設(shè)計(jì)啊~),輕噴---。項(xiàng)目還有很多需要優(yōu)化完善的地方,歡迎大家提到issues(文末有q群,歡迎一起學(xué)習(xí)交流)。
閑話少說(shuō),本文主要講項(xiàng)目的設(shè)計(jì)流程,以及部分功能實(shí)現(xiàn)思路。對(duì)項(xiàng)目感興趣的同學(xué)請(qǐng)移步源碼 Vchat — 從頭到腳,擼一個(gè)在線聊天的web應(yīng)用(vue + node + mongodb)。
*這是分隔線---------------------------------------深夜碼字,最近真冷
相關(guān)地址在線預(yù)覽
github
碼云
簡(jiǎn)書(shū)
知乎
項(xiàng)目架構(gòu)技術(shù)棧
前端主要采用了vue全家桶,沒(méi)什么多說(shuō)的,腳手架構(gòu)建項(xiàng)目,vuex狀態(tài)管理,vue-router控制路由,axios進(jìn)行前后端交互。后端是基于node搭的服務(wù),用的是express。我為什么不用koa呢,純粹是圖方便,因?yàn)閗oa不熟(捂臉)。聊天最重要的當(dāng)然是通信,項(xiàng)目用socket.io來(lái)進(jìn)行前后端通信。數(shù)據(jù)庫(kù)是mongoDB,主要有用戶、好友、群聊、消息、表情、號(hào)碼池等。
功能概覽
功能設(shè)計(jì)登錄注冊(cè)
Vchat中用戶注冊(cè)時(shí),會(huì)隨機(jī)指定一個(gè)code號(hào)碼,而這個(gè)code號(hào)是從預(yù)先生成的一個(gè)號(hào)碼池(號(hào)碼池存在mongodb)中取的。初始指定10000001-10001999的號(hào)碼段為用戶code, 100001-100999的號(hào)碼段為群聊code。用戶可以憑借code號(hào)或者賬號(hào)登錄。
// 號(hào)碼池設(shè)計(jì) * code 號(hào)碼 * status 1 已使用 0 未使用 * type 1 用戶 2 群聊 * random 隨機(jī)數(shù)索引,用于隨機(jī)查找某一條 // user表主要字段 * name 賬號(hào) * pass 密碼 * avatar 頭像 * signature 個(gè)性簽名 * nickname 昵稱 * email 郵件 * phone 手機(jī) * sex 性別 * bubble 氣泡 * projectTheme 項(xiàng)目主題 * wallpaper 聊天壁紙 * signUpTime 注冊(cè)時(shí)間 * lastLoginTime 最后一次登錄時(shí)間 * chatColor 聊天文字顏色 * province 省 * city 市 * town 縣 * conversationsList 會(huì)話列表 * cover 封面列表
注冊(cè)時(shí),需要判斷賬號(hào)是否已存在,以及隨機(jī)取得的code需要在號(hào)碼池中標(biāo)記為已被使用,用戶密碼用md5加密等。
// md5 密碼加密 const md5 = pass => { // 避免多次調(diào)用MD5報(bào)錯(cuò) let md5 = crypto.createHash("md5"); return md5.update(pass).digest("hex"); };
登錄同樣需要判斷用戶是否已注冊(cè),以及支持賬號(hào)和code兩種方式登錄。
const login = (params, callback) => { // 登錄 baseList.users .find({ // mongodb中可以直接用$or表示或關(guān)系 $or: [{"name": params.name}, {"code": params.name}] }) .then(r => { if (r.length) { let pass = md5(params.pass); if (r[0]["pass"] === pass) { //更新最后一次登錄時(shí)間 此處直接寫Date.now 會(huì)報(bào)錯(cuò) 需要Date.now()!!!; baseList.users.update({name: params.name}, {lastLoginTime: Date.now()}).then(raw => { console.log(raw); }); callback({code: 0, data: {name: r[0].name, photo: r[0].photo}}); } else { callback({code: -1}); } } else { callback({code: -1}); } }) };
登錄權(quán)限管理
后端設(shè)置全局中間件,將沒(méi)有登錄的api請(qǐng)求統(tǒng)一返回status: 0
app.use("/v*", (req, res, next) => { if (req.session.login) { next(); } else { if (req.originalUrl === "/v/user/login" || req.originalUrl === "/v/user/signUp") { next(); } else { res.json({ status: 0 }); } } });
前端用axios統(tǒng)一設(shè)置攔截器
// http response 服務(wù)器響應(yīng)攔截器,這里攔截未登錄和401錯(cuò)誤,并重新跳入登頁(yè)重新獲取token instance.interceptors.response.use( response => { // 攔截未登錄 if (response.data.status === 0) { router.replace("/"); } return response; }, error => { if (error.response) { switch (error.response.status) { case 401: // 這里寫清除token的代碼 router.replace("/"); } } return Promise.reject(error.response.data) });
消息
vchat中,消息種類包括好友或者加群申請(qǐng)、回復(fù)申請(qǐng)(同意or拒絕)、入群通知、聊天消息(文字、圖片、表情、文件)
在實(shí)現(xiàn)消息發(fā)送之前,需要大體的了解一些socket.io的api。詳細(xì)api文檔可以查看socket.io
// 所有的消息請(qǐng)求都是建立在已連接的基礎(chǔ)上的 io.on("connect", onConnect); // 發(fā)送給當(dāng)前客戶端 socket.emit("hello", "can you hear me?", 1, 2, "abc"); // 發(fā)送給所有客戶端,除了發(fā)送者 socket.broadcast.emit("broadcast", "hello friends!"); // 發(fā)送給同在 "game" 房間的所有客戶端,除了發(fā)送者 socket.to("game").emit("nice game", "let"s play a game"); // 發(fā)送給同在 "game" 房間的所有客戶端,包括發(fā)送者 io.in("game").emit("big-announcement", "the game will start soon");
加入房間
加入會(huì)話列表中的房間,會(huì)話列表在好友申請(qǐng)成功或者加群成功時(shí)會(huì)自動(dòng)添加。但是你也可以手動(dòng)移除或添加,移除后將不會(huì)再收到被移除會(huì)話的消息(類似于屏蔽)。
// 前端 發(fā)起加入房間的請(qǐng)求 this.conversationsList.forEach(v => { let val = { name: this.user.name, time: utils.formatTime(new Date()), avatar: this.user.photo, roomid: v.id }; this.$socket.emit("join", val); }); // 后端 接受請(qǐng)求后執(zhí)行加入操作,記錄每個(gè)房間加入的成員,以及回信告知指定房間已上線成員 socket.on("join", (val) => { socket.join(val.roomid, () => { if (OnlineUser[val.name]) { return; } OnlineUser[val.name] = socket.id; io.in(val.roomid).emit("joined", OnlineUser); // 包括發(fā)送者 }); });
多房間
同時(shí)加入多個(gè)聊天房間會(huì)出現(xiàn)一個(gè)問(wèn)題,socket可以加入多個(gè)房間并給指定房間發(fā)送消息,但是接受消息的時(shí)候并不會(huì)區(qū)分房間。換句話說(shuō),所有房間的消息,會(huì)一起發(fā)送給客戶端。所以我們需要自己區(qū)分哪條消息是哪個(gè)房間的并進(jìn)行分發(fā)。這樣就需要一個(gè)房間標(biāo)識(shí)來(lái)過(guò)濾,Vchat用的是房間id。
mes(r) { // 只有本房間的消息才展示 if (r.roomid === this.currSation.id) { this.chatList.push(Object.assign({}, r, {type: "other"})); } }
發(fā)消息
// 前端 send(params, type = "mess") { // 發(fā)送消息 if (!this.message && !params) { return; } let val = { name: this.user.name, mes: this.message, time: utils.formatTime(new Date()), avatar: this.user.photo, nickname: this.user.nickname, read: [this.user.name], roomid: this.currSation.id, style: "mess", userM: this.user.id }; this.chatList.push(Object.assign({},val,{type: "mine"})); // 更新視圖 this.$socket.emit("mes", val); this.message = ""; } // 后端 接收消息后存儲(chǔ)到數(shù)據(jù)庫(kù),并轉(zhuǎn)發(fā)給房間內(nèi)其他成員,不包括發(fā)送者。 socket.on("mes", (val) => { // 聊天消息 apiList.saveMessage(val); socket.to(val.roomid).emit("mes", val); });
消息記錄
所有的消息都會(huì)存到mongodb中,當(dāng)切換房間的時(shí)候,會(huì)獲取歷史消息。而處在當(dāng)前房間時(shí),只會(huì)把最新消息追加到dom中,不會(huì)從數(shù)據(jù)庫(kù)獲取。聊天窗口默認(rèn)只展示最新100條消息,更多消息可在聊天記錄中查看。
// 前端 獲取指定房間的歷史消息 this.$socket.emit("getHistoryMessages", {roomid: v.id, offset: 1, limit: 100}); // 后端 關(guān)聯(lián)表、分頁(yè)、排序 messages.find({roomid: params.roomid}) .populate({path: "userM", select: "signature photo nickname"}) // 關(guān)聯(lián)用戶基本信息 .sort({"time": -1}) .skip((params.offset - 1) * params.limit) .limit(params.limit) .then(r => { r.forEach(v => { // 防止用戶修改資料后,信息未更新 if (v.userM) { v.nickname = v.userM.nickname; v.photo = v.userM.photo; v.signature = v.userM.signature; } }); r.reverse(); callback({code: 0, data: r, count: count}); }).catch(err => { console.log(err); callback({code: -1}); });項(xiàng)目展示
主頁(yè)
聊天窗口,可拖拽或縮放,聊天壁紙及文字顏色設(shè)置。
個(gè)人設(shè)置
應(yīng)用空間相關(guān)閱讀
Mongoose基礎(chǔ)入門
socket.io文檔
Vchat主題切換實(shí)現(xiàn)方案來(lái)自于 d2-admin
交流群群內(nèi)有豐富學(xué)習(xí)資料^_^
寫在后面本文主要講了Vchat的整體設(shè)計(jì)以及一些主要功能的實(shí)現(xiàn),其實(shí)寫項(xiàng)目過(guò)程中坑還是挺多的,比如mongoose聯(lián)表查詢、文件上傳等等,這里就不在細(xì)說(shuō),以后有時(shí)間再更新。如果Vchat對(duì)你有幫助,記得star一下喲^_^。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/19463.html
摘要:在實(shí)際開(kāi)發(fā)項(xiàng)目中,有時(shí)我們會(huì)用到自定義按鈕因?yàn)橐粋€(gè)項(xiàng)目中,眾多的頁(yè)面,為了統(tǒng)一風(fēng)格,我們會(huì)重復(fù)用到很多相同或相似的按鈕,這時(shí)候,自定義按鈕組件就派上了大用場(chǎng),我們把定義好的按鈕組件導(dǎo)出,在全局引用,就可以在其他組件隨意使用啦,這樣可以大幅度 在實(shí)際開(kāi)發(fā)項(xiàng)目中,有時(shí)我們會(huì)用到自定義按鈕;因?yàn)橐粋€(gè)項(xiàng)目中,眾多的頁(yè)面,為了統(tǒng)一風(fēng)格,我們會(huì)重復(fù)用到很多相同或相似的按鈕,這時(shí)候,自定義按鈕組件就...
摘要:代碼整潔之道整潔的代碼不僅僅是讓人看起來(lái)舒服,更重要的是遵循一些規(guī)范能夠讓你的代碼更容易維護(hù),同時(shí)降低幾率。另外這不是強(qiáng)制的代碼規(guī)范,就像原文中說(shuō)的,。里式替換原則父類和子類應(yīng)該可以被交換使用而不會(huì)出錯(cuò)。注釋好的代碼是自解釋的。 JavaScript代碼整潔之道 整潔的代碼不僅僅是讓人看起來(lái)舒服,更重要的是遵循一些規(guī)范能夠讓你的代碼更容易維護(hù),同時(shí)降低bug幾率。 原文clean-c...
摘要:接著我之前寫的一篇有關(guān)前端面試題的總結(jié),分享幾道比較經(jīng)典的題目第一題考點(diǎn)作用域,運(yùn)算符栗子都會(huì)進(jìn)行運(yùn)算,但是最后之后輸出最后一個(gè)也就是那么其實(shí)就是而且是個(gè)匿名函數(shù),也就是屬于,就輸出第二和第三個(gè)都是類似的,而且作用域是都是輸出最后一個(gè)其實(shí)就 接著我之前寫的一篇有關(guān)前端面試題的總結(jié),分享幾道比較經(jīng)典的題目: 第一題: showImg(https://segmentfault.com/im...
對(duì)比內(nèi)容UCloudStackZStackVMwareQingCloud騰訊TStack華為云Stack優(yōu)勢(shì)總結(jié)?基于公有云自主可控?公有云架構(gòu)私有化部署?輕量化/輕運(yùn)維/易用性好?政府行業(yè)可復(fù)制案例輕量化 IaaS 虛擬化平臺(tái)?輕量化、產(chǎn)品成熟度高?業(yè)內(nèi)好評(píng)度高?功能豐富、交付部署快?中小企業(yè)案例多全套虛擬產(chǎn)品及云平臺(tái)產(chǎn)品?完整生態(tài)鏈、技術(shù)成熟?比較全面且健全的渠道?產(chǎn)品成熟度被市場(chǎng)認(rèn)可,市場(chǎng)占...
摘要:能跨平臺(tái)地設(shè)置及使用環(huán)境變量讓這一切變得簡(jiǎn)單,不同平臺(tái)使用唯一指令,無(wú)需擔(dān)心跨平臺(tái)問(wèn)題安裝方式改寫使用了環(huán)境變量的常見(jiàn)如在腳本多是里這么配置運(yùn)行,這樣便設(shè)置成功,無(wú)需擔(dān)心跨平臺(tái)問(wèn)題關(guān)于跨平臺(tái)兼容,有幾點(diǎn)注意 cross-env能跨平臺(tái)地設(shè)置及使用環(huán)境變量, cross-env讓這一切變得簡(jiǎn)單,不同平臺(tái)使用唯一指令,無(wú)需擔(dān)心跨平臺(tái)問(wèn)題 1、npm安裝方式 npm i --save-de...
摘要:引入的模塊引入的使用將打包打包的拆分將一部分抽離出來(lái)物理地址拼接優(yōu)化打包速度壓縮代碼,這里使用的是,同樣在的里面添加 const path = require(path); //引入node的path模塊const webpack = require(webpack); //引入的webpack,使用lodashconst HtmlWebpackPlugin = require(ht...
閱讀 2438·2019-08-30 15:56
閱讀 1096·2019-08-30 15:55
閱讀 3269·2019-08-30 15:44
閱讀 1004·2019-08-30 10:53
閱讀 1946·2019-08-29 16:33
閱讀 2640·2019-08-29 16:13
閱讀 771·2019-08-29 12:41
閱讀 940·2019-08-26 13:56