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