ThinkJS搭建網(wǎng)頁版即時聊天后臺

簡單的寫寫程序邏輯。

緣由

因網(wǎng)站需求,要一個Web版的聊天程序,前端方面選擇了LayIM,只購買了前端程序,后臺得自己實現(xiàn)。
于是利用Express+MongoDB+Socket.io搭建了一個即時聊天的后臺。但因為那時候LayIM剛出mobile版,只有一個對話窗口。自己寫了一個簡陋的消息接受列表,將就的用了一段時間。
但畢竟是第一次接觸Node.JS,各方面代碼總是不太滿意。第一次接觸,因為各種教程版本不同,導致許多地方留下嘗試性設置。加上不支持異步,換著方式實現(xiàn)功能,但最后還是添加上了es6,可此前的一些實現(xiàn)沒隨之更改,整個項目還是比較雜亂的。剛好LayIM也出了新版,更新了完整的移動端界面,于是開始動手重寫后臺。

前期準備

  • 重寫打算不再使用Express,一是很關鍵的對異步不支持,二是Express太過基礎,雖說自定義性很高,但只是實現(xiàn)一個小后臺,找一些集成度高的框架,避免自己寫基礎性代碼,也讓代碼更加有條理。
    最終選中了ThinkJS,一款國產(chǎn)的框架,集成度比較高,功能夠用。其中也有一部分原因,因其名字與ThinkPHP相似,框架結(jié)構似乎有借鑒,之后也打算看看ThinkPHP,所以先用這個嘗試一下。
  • 數(shù)據(jù)庫方面,MongoDB支持JSON格式,存取數(shù)據(jù)很方便。只是在多表聯(lián)合查詢上并不方便,于是新版的查詢中選擇了常見的MySQL(查看資料說5.7版以后支持了JSON,但服務器上的數(shù)據(jù)庫似乎不支持)。
  • WebSocket還是使用Socket.io,對瀏覽器兼容性有處理。
  • 前端使用LayIM,支持PC和Mobile,兩者參數(shù)字段都統(tǒng)一,這次處理起來比較方便。

ThinkJS配置

ThinkJS是MVC框架,默認使用ES6/7特性,可以使用async/await解決異步嵌套問題。具體可查看文檔
我們只有一個IM功能,所以就不必再添加新的模塊,使用默認生成的home模塊就行。

首先需要配置,src/common/config/config.js文件中可以設置公用的配置信息,如果線上環(huán)境與開發(fā)環(huán)境配置不一樣,可以在src/common/config/env目錄下找到development.js,在其中寫入開發(fā)環(huán)境所需的配置,production.js對應為線上環(huán)境所需的配置。
數(shù)據(jù)庫配置信息在src/common/config/db.js文件中配置。
模板引擎使用默認的ejs,路由配置也是使用默認設置。

還需要在項目中打開WebSocket,找到src/common/config/目錄下websocket.js文件,其中on設置為true,并且messages下寫入WebSocket事件對應的地址。例如:

"use strict";
export default {
  on: true, //是否開啟 WebSocket
  type: 'socket.io', //使用的 WebSocket 庫類型,默認為 socket.io
  allow_origin: '', //允許的 origin
  adp: undefined, // socket 存儲的 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ù)
  }
};

文件

所有的代碼獨立存放在各種的文件中。
WebSocket相關代碼放在home/controller/socketio.js文件。
后臺管理相關代碼存放在home/controller/manage.js文件。
LayIM所需的用戶登陸信息獲取、存入,查看聊天記錄等代碼存放在home/controller/api.js文件。
home/logic目錄下,與controller目錄下同名的js文件,是提交數(shù)據(jù)效驗代碼,提交數(shù)據(jù)可以在這一層里先經(jīng)過校驗判斷或是過濾處理。

其他

src/common/bootstarp/global.js文件中可以寫入全局函數(shù)。
跨域ThinkJS也提供了說明,查看文檔

數(shù)據(jù)庫設計

按LayIM所需字段,額外增加一個用于記錄登陸時間的字段。

user表:id、username、avatar、sign、status、logintime
id唯一,判斷用戶的依據(jù)。username雖然隨著更新,但實際上主站并不讓更改。
logintime用于給后臺管理時查看
id、username、avatar三個字段是LayIM必需的字段

好友功能本身沒實現(xiàn)的必要,因為聊天都是通過網(wǎng)站點擊一下在線按鈕,已經(jīng)有了歷史會話列表,好友列表并不是很重要(淘寶移動端也是顯示歷史會話列表)。
但考慮到以后會不會有其他需求,而且單獨一個歷史會話列表界面也并不是很好看,于是添加上了好友功能。

group表:gid、groupname、id、frienduid。
gid是表的id
groupname是好友表的名字
id是使用者自己的id
frienduid是好友的id
一個好友就是一條記錄,通過多表聯(lián)合查詢,可以直接將好友列表的結(jié)果一次性讀取出來。增刪都很方便。

之后便是存儲聊天信息。大致按LayIM本身的信息結(jié)構存入就行,只是額外添加上一個自增字段,一個用于判斷是否推送離線消息的字段。

msg表:mid、id、username、avatar、content、toid、tousername、type、timestamp、push
push為1時,表式推送消息。

后臺需要一個管理界面,所以需要一個表用于記錄后臺管理員賬號信息。

manager表:id、usernamepassword、logintime
password為加鹽的二次MD5值。

后臺消息收發(fā)邏輯

登陸
  • 因為主站是使用PHP,即時聊天使用Node.JS提供API,所以不用設計注冊登陸功能,只需要接收用戶相關字段并更新就行。為了避免用戶手動修改前端的配置信息,所以IM接收的是一串hash字符,IM后端接收到后利于這字符向主站拿取用戶信息。
    需要嚴格按LayIM要求的格式組裝好JSON。LayIM文檔
layim.config({
    init: {
    url: '' // API的地址,以get形式提交hash
    ,data: {}
    }
……

用戶登陸除了向主站取得用戶信息,還需要查詢數(shù)據(jù)庫中的好友數(shù)據(jù),需要user、group兩個表聯(lián)合查詢,ThinkJS中提供model.join(join)來聯(lián)合查詢,但始終避免不了要自己寫上表前綴,如果修改表,此處容易被忽略。

let group = await modelGroup.where({ id: mine.id}).distinct('gid').field('gid,groupname').select();

distinct是去重的字段,field是篩選字段。

WebSocket

幾個主要的地方

  • 主要是用戶信息與SocketID的對應,利用一個變量來記錄所有的SocketID與用戶信息的對應,方便查詢。
  • 同時需要PC端、Mobile端兩端都能正常接收信息,并且PC端重復打開頁面,只發(fā)送信息給最后一個打開的頁面。
  • 前端連接上socket的時候,發(fā)送用戶信息中包括登陸端類型(PCMobile),然后利用兩個變量分別記錄PC端和Mobile端的用戶。
  • 由于Mboile正常情況需要點擊進入聊天界面才知道是否有新消息,所以額外增加一個事件,用來推送未讀提醒。

其他功能同樣以此方式調(diào)取用戶信息執(zhí)行相關操作。添加好友、刪除好友,由于不是主要功能,所以以最簡單的方式,附加在工具欄上實現(xiàn)。

Socket登記流程

當用戶連接上socket時,會默認執(zhí)行connect事件,其中寫入一連接成功即發(fā)送用戶數(shù)據(jù)至后臺。

socket.emit('init', { jointype: 'PC', key: 'hash'});

由于LayIM的ready事件需要使用url獲取用戶信息才會觸發(fā),同時Mobile中也沒有提供這樣事件,所以全部由socket.io的connect事件替代。
后臺接收到數(shù)據(jù),并驗證數(shù)據(jù)可用。然后以SocketID為下標,存入變量中,這樣每個通過驗證的連接都可以知曉是屬于哪個用戶。
但是這樣不利于查詢一個用戶下的所有連接,同時LayIM發(fā)送信息中只提供了用戶ID。所以需要再以用戶ID為下標,分別將PC與Mobile存入不同的變量中,以便管理。
這樣只要檢查變量中是否存在用戶ID,便可以知道這個用戶是否在線,因為是push存入信息,所以最后一個必然是最新的SocketID。

前端發(fā)送用戶信息至后臺 ---> 后臺驗證數(shù)據(jù)是否可用 ---> 可用則以SocketID為下標存入變量  ---> 再以用戶ID為下標存入變量 ---> 讀取離線消息等其他操作
Socket退出流程

退出會響應close事件,在close函數(shù)中可以獲得用戶的SocketID,通過這個可以獲取此次退出的是哪個用戶。先刪除SocketID為下標的變量中的數(shù)據(jù),再刪除以用戶ID為下標的PC、Mobile兩個變量中的數(shù)據(jù)。
為了避免各種意外狀況,每次退出時最后再將在線用戶列表中的SocketID在所有連接對象thinkCache(thinkCache.WEBSOCKET)中進行檢測,不存在則刪除。

發(fā)送消息流程

LayIM發(fā)送的消息會自動打包成指定格式,其中包含發(fā)送用戶信息、接收用戶信息、發(fā)送消息類型等。
后端接收到數(shù)據(jù),先判斷消息類型,以分別執(zhí)行對應操作。如果是friend類型,則提取消息發(fā)送出去。
先會將消息組織成數(shù)據(jù)庫可存入的結(jié)構、消息發(fā)送的結(jié)構,然后通過用戶ID判斷用戶是否在線,在線則通過用戶ID提取出最新的SocketID。ThinkJS把當前所有的連接對象都存在了thinkCache(thinkCache.WEBSOCKET)只需要以SocketID為下標,調(diào)出對象,就可以調(diào)用emit向指定連接發(fā)送事件。
如果有執(zhí)行發(fā)送,則push字段值修改為0,否則按默認為1。下次用戶登陸后會先讀取push1的數(shù)據(jù)推送至前端。
最后將數(shù)據(jù)存入數(shù)據(jù)庫。

接收到消息  ---> 重新拆分按指定格式重組數(shù)據(jù)  ---> 根據(jù)ID判斷用戶是否在線  ---> 將數(shù)據(jù)存入數(shù)據(jù)庫

后臺管理頁面

LayIM中本身就需要引入LayUI,所以就直接使用LayUI,找了一些默認的頁面元素實現(xiàn)了前端頁面。

登陸

直接訪問IM服務器地址會出現(xiàn)簡單的服務器信息,點擊圖片會跳轉(zhuǎn)入登陸頁面(本來沒打算做管理頁面,常用的服務器信息都在此頁面)。


密碼考慮到雖然前端經(jīng)過MD5無意義,但總是可以避免明文密碼在傳輸過程中被泄露,到后臺再經(jīng)過加鹽二次MD5,存入數(shù)據(jù)庫。


用戶登陸頁

前端的登陸信息,傳到后臺與數(shù)據(jù)庫比對,正確即在session中標記登陸。若錯誤,則以json返回錯誤信息。退出登陸則清除session中存儲的信息。
ThinkJS提供了this.success()、this.fail()、this.json()函數(shù)用來輸出JSON。頁面跳轉(zhuǎn)可以使用http.redirect(),其中輸入需要跳轉(zhuǎn)的頁面地址即可。

用戶管理

用戶管理主要實現(xiàn)可以查看所有用戶,查看每個用戶的聊天對象,以及一對一聊天的聊天記錄。
沒有添加、刪除、修改功能,因為所有用戶數(shù)據(jù)都是在主站上,IM僅僅只是接收,并且每次登陸即更新,添加、刪除、修改都沒意義。
ThinkJS中有提供分頁查詢函數(shù)model.page(page, listRows),page為頁數(shù)、listRoes為每頁條數(shù),會自動轉(zhuǎn)化為limit語句。
在此頁面有個功能并未做到網(wǎng)頁中。打開開發(fā)者工具,會輸出登陸的用戶、PC端、Mobile端、移動端未讀信息連接,四個變量中的數(shù)據(jù),方便調(diào)試時查看網(wǎng)站情況。

用戶管理頁
聊天記錄
后臺賬號管理

沒有需要設置的地方,做一個演示功能。可添加新賬號、修改自己賬號密碼、刪除賬號


后臺設置-修改密碼中

上線

上線需要啟動www/production.js文件,不再是www/development.js,并且修改文件后不會再自動編譯運行載入,要手動編程、重啟。
其中ThinkJS默認會關閉靜態(tài)資源訪問,需要在src/common/config/env/production.js文件中的resource_on改為true
有使用HTTPS可以使用Nginx作反向代理。

后記

第一次接觸ThinkJS框架,理解上花了一些時間。LayIM因一些不知緣由的小錯誤,也處理了好些時間(大部分是數(shù)據(jù)裝后不規(guī)范)。
socket部分因為是自己控制登陸信息、檢測用戶權限,所以邏輯上比較多。雖然總感覺可以寫的更加簡練,但目前沒找到更適合的方法。就程序本身來說并沒太多難的地方。

起初重寫IM后臺時,打算寫一篇詳細的入門教程。但寫寫文章時發(fā)現(xiàn),有很多細節(jié)零散的東西,寫入教程變動啰嗦影響閱讀,不寫入又不算是入門教程,于是最后改成僅僅記錄一些主要的邏輯。大部分地方需要配合源碼與文檔一起看。

源碼

暫未開放

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 點擊查看原文 Web SDK 開發(fā)手冊 SDK 概述 網(wǎng)易云信 SDK 為 Web 應用提供一個完善的 IM 系統(tǒng)...
    layjoy閱讀 14,299評論 0 15
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,872評論 25 709
  • 蒼南一個私人皮膚門診內(nèi),看著丁醫(yī)生被圍得水泄不通,暗暗慶幸自己一大早就打電話取了號,還有兩個就輪到了。等啊等...
    曉卉669閱讀 1,197評論 1 0
  • 安裝了vmware-tools和open-vm-tools后,發(fā)現(xiàn)能找到hgfs但是還是找不到分享文件夾 解決方法 原文
    silentsvv閱讀 4,401評論 0 1
  • 今天是陰歷九月三十。每年的這個日子,我都會去看您,和您說說心里話。今天,我又如約而至。 現(xiàn)在,我已經(jīng)接受...
    玫蘭妮閱讀 4,118評論 0 1

友情鏈接更多精彩內(nèi)容