簡(jiǎn)單的寫(xiě)寫(xiě)程序邏輯盅惜。
緣由
因網(wǎng)站需求窃爷,要一個(gè)Web版的聊天程序,前端方面選擇了LayIM,只購(gòu)買(mǎi)了前端程序嘹裂,后臺(tái)得自己實(shí)現(xiàn)豁延。
于是利用Express+MongoDB+Socket.io搭建了一個(gè)即時(shí)聊天的后臺(tái)丐重。但因?yàn)槟菚r(shí)候LayIM剛出mobile版硼婿,只有一個(gè)對(duì)話(huà)窗口。自己寫(xiě)了一個(gè)簡(jiǎn)陋的消息接受列表俗慈,將就的用了一段時(shí)間姑宽。
但畢竟是第一次接觸Node.JS,各方面代碼總是不太滿(mǎn)意闺阱。第一次接觸炮车,因?yàn)楦鞣N教程版本不同,導(dǎo)致許多地方留下嘗試性設(shè)置酣溃。加上不支持異步瘦穆,換著方式實(shí)現(xiàn)功能,但最后還是添加上了es6赊豌,可此前的一些實(shí)現(xiàn)沒(méi)隨之更改扛或,整個(gè)項(xiàng)目還是比較雜亂的。剛好LayIM也出了新版碘饼,更新了完整的移動(dòng)端界面熙兔,于是開(kāi)始動(dòng)手重寫(xiě)后臺(tái)悲伶。
前期準(zhǔn)備
- 重寫(xiě)打算不再使用Express,一是很關(guān)鍵的對(duì)異步不支持住涉,二是Express太過(guò)基礎(chǔ)麸锉,雖說(shuō)自定義性很高,但只是實(shí)現(xiàn)一個(gè)小后臺(tái)舆声,找一些集成度高的框架淮椰,避免自己寫(xiě)基礎(chǔ)性代碼,也讓代碼更加有條理纳寂。
最終選中了ThinkJS
,一款國(guó)產(chǎn)的框架泻拦,集成度比較高毙芜,功能夠用。其中也有一部分原因争拐,因其名字與ThinkPHP相似腋粥,框架結(jié)構(gòu)似乎有借鑒,之后也打算看看ThinkPHP架曹,所以先用這個(gè)嘗試一下隘冲。 - 數(shù)據(jù)庫(kù)方面,MongoDB支持JSON格式绑雄,存取數(shù)據(jù)很方便展辞。只是在多表聯(lián)合查詢(xún)上并不方便,于是新版的查詢(xún)中選擇了常見(jiàn)的MySQL(查看資料說(shuō)5.7版以后支持了JSON万牺,但服務(wù)器上的數(shù)據(jù)庫(kù)似乎不支持)罗珍。
- WebSocket還是使用
Socket.io
,對(duì)瀏覽器兼容性有處理脚粟。 - 前端使用
LayIM
覆旱,支持PC和Mobile,兩者參數(shù)字段都統(tǒng)一核无,這次處理起來(lái)比較方便扣唱。
ThinkJS配置
ThinkJS是MVC框架,默認(rèn)使用ES6/7特性团南,可以使用async/await解決異步嵌套問(wèn)題噪沙。具體可查看文檔
我們只有一個(gè)IM功能,所以就不必再添加新的模塊已慢,使用默認(rèn)生成的home
模塊就行曲聂。
首先需要配置,src/common/config/config.js
文件中可以設(shè)置公用的配置信息佑惠,如果線(xiàn)上環(huán)境與開(kāi)發(fā)環(huán)境配置不一樣朋腋,可以在src/common/config/env
目錄下找到development.js
齐疙,在其中寫(xiě)入開(kāi)發(fā)環(huán)境所需的配置,production.js
對(duì)應(yīng)為線(xiàn)上環(huán)境所需的配置旭咽。
數(shù)據(jù)庫(kù)配置信息在src/common/config/db.js
文件中配置贞奋。
模板引擎使用默認(rèn)的ejs,路由配置也是使用默認(rèn)設(shè)置穷绵。
還需要在項(xiàng)目中打開(kāi)WebSocket轿塔,找到src/common/config/
目錄下websocket.js
文件,其中on
設(shè)置為true
仲墨,并且messages
下寫(xiě)入WebSocket事件對(duì)應(yīng)的地址勾缭。例如:
"use strict";
export default {
on: true, //是否開(kāi)啟 WebSocket
type: 'socket.io', //使用的 WebSocket 庫(kù)類(lèi)型,默認(rèn)為 socket.io
allow_origin: '', //允許的 origin
adp: undefined, // socket 存儲(chǔ)的 adapter目养,socket.io 下使用
path: '', //url path for websocket
messages: {
open: 'home/socketio/open',
close: 'home/socketio/close',
message: 'home/socketio/message' // message事件俩由,在home目錄下socketio.js文件中的message函數(shù)
}
};
文件
所有的代碼獨(dú)立存放在各種的文件中。
WebSocket相關(guān)代碼放在home/controller/socketio.js
文件癌蚁。
后臺(tái)管理相關(guān)代碼存放在home/controller/manage.js
文件幻梯。
LayIM所需的用戶(hù)登陸信息獲取、存入努释,查看聊天記錄等代碼存放在home/controller/api.js
文件碘梢。
在home/logic
目錄下,與controller
目錄下同名的js文件伐蒂,是提交數(shù)據(jù)效驗(yàn)代碼煞躬,提交數(shù)據(jù)可以在這一層里先經(jīng)過(guò)校驗(yàn)判斷或是過(guò)濾處理。
其他
在src/common/bootstarp/global.js
文件中可以寫(xiě)入全局函數(shù)逸邦。
跨域ThinkJS也提供了說(shuō)明汰翠,查看文檔。
數(shù)據(jù)庫(kù)設(shè)計(jì)
按LayIM所需字段昭雌,額外增加一個(gè)用于記錄登陸時(shí)間的字段复唤。
user
表:id
、username
烛卧、avatar
佛纫、sign
、status
总放、logintime
id
唯一呈宇,判斷用戶(hù)的依據(jù)。username
雖然隨著更新局雄,但實(shí)際上主站并不讓更改甥啄。
logintime
用于給后臺(tái)管理時(shí)查看
id
、username
炬搭、avatar
三個(gè)字段是LayIM必需的字段
好友功能本身沒(méi)實(shí)現(xiàn)的必要蜈漓,因?yàn)榱奶於际峭ㄟ^(guò)網(wǎng)站點(diǎn)擊一下在線(xiàn)按鈕穆桂,已經(jīng)有了歷史會(huì)話(huà)列表,好友列表并不是很重要(淘寶移動(dòng)端也是顯示歷史會(huì)話(huà)列表)融虽。
但考慮到以后會(huì)不會(huì)有其他需求享完,而且單獨(dú)一個(gè)歷史會(huì)話(huà)列表界面也并不是很好看,于是添加上了好友功能有额。
group
表:gid
般又、groupname
、id
巍佑、frienduid
茴迁。
gid
是表的id
groupname
是好友表的名字
id
是使用者自己的id
frienduid
是好友的id
一個(gè)好友就是一條記錄,通過(guò)多表聯(lián)合查詢(xún)萤衰,可以直接將好友列表的結(jié)果一次性讀取出來(lái)笋熬。增刪都很方便。
之后便是存儲(chǔ)聊天信息腻菇。大致按LayIM本身的信息結(jié)構(gòu)存入就行,只是額外添加上一個(gè)自增字段昔馋,一個(gè)用于判斷是否推送離線(xiàn)消息的字段筹吐。
msg
表:mid
、id
秘遏、username
丘薛、avatar
、content
邦危、toid
洋侨、tousername
、type
倦蚪、timestamp
希坚、push
當(dāng)push
為1時(shí),表式推送消息陵且。
后臺(tái)需要一個(gè)管理界面裁僧,所以需要一個(gè)表用于記錄后臺(tái)管理員賬號(hào)信息。
manager
表:id
慕购、username
聊疲、password
、logintime
password
為加鹽的二次MD5值沪悲。
后臺(tái)消息收發(fā)邏輯
登陸
- 因?yàn)橹髡臼鞘褂肞HP获洲,即時(shí)聊天使用Node.JS提供API,所以不用設(shè)計(jì)注冊(cè)登陸功能殿如,只需要接收用戶(hù)相關(guān)字段并更新就行贡珊。為了避免用戶(hù)手動(dòng)修改前端的配置信息最爬,所以IM接收的是一串hash字符,IM后端接收到后利于這字符向主站拿取用戶(hù)信息飞崖。
需要嚴(yán)格按LayIM要求的格式組裝好JSON烂叔。LayIM文檔
layim.config({
init: {
url: '' // API的地址,以get形式提交hash
,data: {}
}
……
用戶(hù)登陸除了向主站取得用戶(hù)信息固歪,還需要查詢(xún)數(shù)據(jù)庫(kù)中的好友數(shù)據(jù)蒜鸡,需要user
、group
兩個(gè)表聯(lián)合查詢(xún)牢裳,ThinkJS中提供model.join(join)
來(lái)聯(lián)合查詢(xún)逢防,但始終避免不了要自己寫(xiě)上表前綴,如果修改表蒲讯,此處容易被忽略忘朝。
let group = await modelGroup.where({ id: mine.id}).distinct('gid').field('gid,groupname').select();
distinct
是去重的字段,field
是篩選字段判帮。
WebSocket
幾個(gè)主要的地方
- 主要是用戶(hù)信息與
SocketID
的對(duì)應(yīng)局嘁,利用一個(gè)變量來(lái)記錄所有的SocketID
與用戶(hù)信息的對(duì)應(yīng),方便查詢(xún)晦墙。 - 同時(shí)需要PC端悦昵、Mobile端兩端都能正常接收信息,并且PC端重復(fù)打開(kāi)頁(yè)面晌畅,只發(fā)送信息給最后一個(gè)打開(kāi)的頁(yè)面但指。
- 前端連接上socket的時(shí)候,發(fā)送用戶(hù)信息中包括登陸端類(lèi)型(
PC
抗楔、Mobile
)棋凳,然后利用兩個(gè)變量分別記錄PC端和Mobile端的用戶(hù)。 - 由于Mboile正常情況需要點(diǎn)擊進(jìn)入聊天界面才知道是否有新消息连躏,所以額外增加一個(gè)事件剩岳,用來(lái)推送未讀提醒。
其他功能同樣以此方式調(diào)取用戶(hù)信息執(zhí)行相關(guān)操作入热。添加好友卢肃、刪除好友,由于不是主要功能才顿,所以以最簡(jiǎn)單的方式莫湘,附加在工具欄上實(shí)現(xiàn)。
Socket登記流程
當(dāng)用戶(hù)連接上socket時(shí)郑气,會(huì)默認(rèn)執(zhí)行connect
事件幅垮,其中寫(xiě)入一連接成功即發(fā)送用戶(hù)數(shù)據(jù)至后臺(tái)。
socket.emit('init', { jointype: 'PC', key: 'hash'});
由于LayIM的ready
事件需要使用url獲取用戶(hù)信息才會(huì)觸發(fā)尾组,同時(shí)Mobile中也沒(méi)有提供這樣事件忙芒,所以全部由socket.io的connect
事件替代示弓。
后臺(tái)接收到數(shù)據(jù),并驗(yàn)證數(shù)據(jù)可用呵萨。然后以SocketID
為下標(biāo)奏属,存入變量中,這樣每個(gè)通過(guò)驗(yàn)證的連接都可以知曉是屬于哪個(gè)用戶(hù)潮峦。
但是這樣不利于查詢(xún)一個(gè)用戶(hù)下的所有連接囱皿,同時(shí)LayIM發(fā)送信息中只提供了用戶(hù)ID
。所以需要再以用戶(hù)ID
為下標(biāo)忱嘹,分別將PC與Mobile存入不同的變量中嘱腥,以便管理。
這樣只要檢查變量中是否存在用戶(hù)ID
拘悦,便可以知道這個(gè)用戶(hù)是否在線(xiàn)齿兔,因?yàn)槭?code>push存入信息,所以最后一個(gè)必然是最新的SocketID
础米。
前端發(fā)送用戶(hù)信息至后臺(tái) ---> 后臺(tái)驗(yàn)證數(shù)據(jù)是否可用 ---> 可用則以SocketID為下標(biāo)存入變量 ---> 再以用戶(hù)ID為下標(biāo)存入變量 ---> 讀取離線(xiàn)消息等其他操作
Socket退出流程
退出會(huì)響應(yīng)close
事件分苇,在close
函數(shù)中可以獲得用戶(hù)的SocketID
,通過(guò)這個(gè)可以獲取此次退出的是哪個(gè)用戶(hù)屁桑。先刪除SocketID
為下標(biāo)的變量中的數(shù)據(jù)医寿,再刪除以用戶(hù)ID
為下標(biāo)的PC、Mobile兩個(gè)變量中的數(shù)據(jù)掏颊。
為了避免各種意外狀況,每次退出時(shí)最后再將在線(xiàn)用戶(hù)列表中的SocketID
在所有連接對(duì)象thinkCache(thinkCache.WEBSOCKET)
中進(jìn)行檢測(cè)艾帐,不存在則刪除乌叶。
發(fā)送消息流程
LayIM發(fā)送的消息會(huì)自動(dòng)打包成指定格式,其中包含發(fā)送用戶(hù)信息柒爸、接收用戶(hù)信息准浴、發(fā)送消息類(lèi)型等。
后端接收到數(shù)據(jù)捎稚,先判斷消息類(lèi)型乐横,以分別執(zhí)行對(duì)應(yīng)操作。如果是friend
類(lèi)型今野,則提取消息發(fā)送出去葡公。
先會(huì)將消息組織成數(shù)據(jù)庫(kù)可存入的結(jié)構(gòu)、消息發(fā)送的結(jié)構(gòu)条霜,然后通過(guò)用戶(hù)ID
判斷用戶(hù)是否在線(xiàn)催什,在線(xiàn)則通過(guò)用戶(hù)ID
提取出最新的SocketID
。ThinkJS把當(dāng)前所有的連接對(duì)象都存在了thinkCache(thinkCache.WEBSOCKET)
只需要以SocketID
為下標(biāo)宰睡,調(diào)出對(duì)象蒲凶,就可以調(diào)用emit
向指定連接發(fā)送事件气筋。
如果有執(zhí)行發(fā)送,則push
字段值修改為0
旋圆,否則按默認(rèn)為1
宠默。下次用戶(hù)登陸后會(huì)先讀取push
為1
的數(shù)據(jù)推送至前端。
最后將數(shù)據(jù)存入數(shù)據(jù)庫(kù)灵巧。
接收到消息 ---> 重新拆分按指定格式重組數(shù)據(jù) ---> 根據(jù)ID判斷用戶(hù)是否在線(xiàn) ---> 將數(shù)據(jù)存入數(shù)據(jù)庫(kù)
后臺(tái)管理頁(yè)面
LayIM中本身就需要引入LayUI搀矫,所以就直接使用LayUI,找了一些默認(rèn)的頁(yè)面元素實(shí)現(xiàn)了前端頁(yè)面孩等。
登陸
直接訪(fǎng)問(wèn)IM服務(wù)器地址會(huì)出現(xiàn)簡(jiǎn)單的服務(wù)器信息艾君,點(diǎn)擊圖片會(huì)跳轉(zhuǎn)入登陸頁(yè)面(本來(lái)沒(méi)打算做管理頁(yè)面,常用的服務(wù)器信息都在此頁(yè)面)肄方。
密碼考慮到雖然前端經(jīng)過(guò)MD5無(wú)意義冰垄,但總是可以避免明文密碼在傳輸過(guò)程中被泄露,到后臺(tái)再經(jīng)過(guò)加鹽二次MD5权她,存入數(shù)據(jù)庫(kù)虹茶。
前端的登陸信息,傳到后臺(tái)與數(shù)據(jù)庫(kù)比對(duì)隅要,正確即在session中標(biāo)記登陸蝴罪。若錯(cuò)誤,則以json返回錯(cuò)誤信息步清。退出登陸則清除session中存儲(chǔ)的信息要门。
ThinkJS提供了this.success()
、this.fail()
廓啊、this.json()
函數(shù)用來(lái)輸出JSON欢搜。頁(yè)面跳轉(zhuǎn)可以使用http.redirect()
,其中輸入需要跳轉(zhuǎn)的頁(yè)面地址即可谴轮。
用戶(hù)管理
用戶(hù)管理主要實(shí)現(xiàn)可以查看所有用戶(hù)炒瘟,查看每個(gè)用戶(hù)的聊天對(duì)象,以及一對(duì)一聊天的聊天記錄第步。
沒(méi)有添加疮装、刪除、修改功能粘都,因?yàn)樗杏脩?hù)數(shù)據(jù)都是在主站上廓推,IM僅僅只是接收,并且每次登陸即更新翩隧,添加受啥、刪除、修改都沒(méi)意義。
ThinkJS中有提供分頁(yè)查詢(xún)函數(shù)model.page(page, listRows)
滚局,page
為頁(yè)數(shù)居暖、listRoes
為每頁(yè)條數(shù),會(huì)自動(dòng)轉(zhuǎn)化為limit
語(yǔ)句藤肢。
在此頁(yè)面有個(gè)功能并未做到網(wǎng)頁(yè)中太闺。打開(kāi)開(kāi)發(fā)者工具,會(huì)輸出登陸的用戶(hù)嘁圈、PC端省骂、Mobile端、移動(dòng)端未讀信息連接最住,四個(gè)變量中的數(shù)據(jù)钞澳,方便調(diào)試時(shí)查看網(wǎng)站情況。
后臺(tái)賬號(hào)管理
沒(méi)有需要設(shè)置的地方涨缚,做一個(gè)演示功能轧粟。可添加新賬號(hào)脓魏、修改自己賬號(hào)密碼兰吟、刪除賬號(hào)
上線(xiàn)
上線(xiàn)需要啟動(dòng)www/production.js
文件,不再是www/development.js
茂翔,并且修改文件后不會(huì)再自動(dòng)編譯運(yùn)行載入混蔼,要手動(dòng)編程、重啟珊燎。
其中ThinkJS默認(rèn)會(huì)關(guān)閉靜態(tài)資源訪(fǎng)問(wèn)惭嚣,需要在src/common/config/env/production.js
文件中的resource_on
改為true
。
有使用HTTPS可以使用Nginx作反向代理悔政。
后記
第一次接觸ThinkJS框架晚吞,理解上花了一些時(shí)間。LayIM因一些不知緣由的小錯(cuò)誤卓箫,也處理了好些時(shí)間(大部分是數(shù)據(jù)裝后不規(guī)范)载矿。
socket部分因?yàn)槭亲约嚎刂频顷懶畔⒙⒊薄z測(cè)用戶(hù)權(quán)限烹卒,所以邏輯上比較多。雖然總感覺(jué)可以寫(xiě)的更加簡(jiǎn)練弯洗,但目前沒(méi)找到更適合的方法旅急。就程序本身來(lái)說(shuō)并沒(méi)太多難的地方。
起初重寫(xiě)IM后臺(tái)時(shí)牡整,打算寫(xiě)一篇詳細(xì)的入門(mén)教程藐吮。但寫(xiě)寫(xiě)文章時(shí)發(fā)現(xiàn),有很多細(xì)節(jié)零散的東西,寫(xiě)入教程變動(dòng)啰嗦影響閱讀谣辞,不寫(xiě)入又不算是入門(mén)教程迫摔,于是最后改成僅僅記錄一些主要的邏輯。大部分地方需要配合源碼與文檔一起看泥从。
源碼
暫未開(kāi)放