ThinkJS搭建网页版即时聊天后台

简单的写写程序逻辑。

缘由

因网站需求,要一个Web版的聊天程序,前端方面选择了LayIM,只购买了前端程序,后台得自己实现。
于是利用Express+MongoDB+Socket.io搭建了一个即时聊天的后台。但因为那时候LayIM刚出mobile版,只有一个对话窗口。自己写了一个简陋的消息接受列表,将就的用了一段时间。
但毕竟是第一次接触Node.JS,各方面代码总是不太满意。第一次接触,因为各种教程版本不同,导致许多地方留下尝试性设置。加上不支持异步,换着方式实现功能,但最后还是添加上了es6,可此前的一些实现没随之更改,整个项目还是比较杂乱的。刚好LayIM也出了新版,更新了完整的移动端界面,于是开始动手重写后台。

前期准备

  • 重写打算不再使用Express,一是很关键的对异步不支持,二是Express太过基础,虽说自定义性很高,但只是实现一个小后台,找一些集成度高的框架,避免自己写基础性代码,也让代码更加有条理。
    最终选中了ThinkJS,一款国产的框架,集成度比较高,功能够用。其中也有一部分原因,因其名字与ThinkPHP相似,框架结构似乎有借鉴,之后也打算看看ThinkPHP,所以先用这个尝试一下。
  • 数据库方面,MongoDB支持JSON格式,存取数据很方便。只是在多表联合查询上并不方便,于是新版的查询中选择了常见的MySQL(查看资料说5.7版以后支持了JSON,但服务器上的数据库似乎不支持)。
  • WebSocket还是使用Socket.io,对浏览器兼容性有处理。
  • 前端使用LayIM,支持PC和Mobile,两者参数字段都统一,这次处理起来比较方便。

ThinkJS配置

ThinkJS是MVC框架,默认使用ES6/7特性,可以使用async/await解决异步嵌套问题。具体可查看文档
我们只有一个IM功能,所以就不必再添加新的模块,使用默认生成的home模块就行。

首先需要配置,src/common/config/config.js文件中可以设置公用的配置信息,如果线上环境与开发环境配置不一样,可以在src/common/config/env目录下找到development.js,在其中写入开发环境所需的配置,production.js对应为线上环境所需的配置。
数据库配置信息在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函数
  }
};

文件

所有的代码独立存放在各种的文件中。
WebSocket相关代码放在home/controller/socketio.js文件。
后台管理相关代码存放在home/controller/manage.js文件。
LayIM所需的用户登陆信息获取、存入,查看聊天记录等代码存放在home/controller/api.js文件。
home/logic目录下,与controller目录下同名的js文件,是提交数据效验代码,提交数据可以在这一层里先经过校验判断或是过滤处理。

其他

src/common/bootstarp/global.js文件中可以写入全局函数。
跨域ThinkJS也提供了说明,查看文档

数据库设计

按LayIM所需字段,额外增加一个用于记录登陆时间的字段。

user表:idusernameavatarsignstatuslogintime
id唯一,判断用户的依据。username虽然随着更新,但实际上主站并不让更改。
logintime用于给后台管理时查看
idusernameavatar三个字段是LayIM必需的字段

好友功能本身没实现的必要,因为聊天都是通过网站点击一下在线按钮,已经有了历史会话列表,好友列表并不是很重要(淘宝移动端也是显示历史会话列表)。
但考虑到以后会不会有其他需求,而且单独一个历史会话列表界面也并不是很好看,于是添加上了好友功能。

group表:gidgroupnameidfrienduid
gid是表的id
groupname是好友表的名字
id是使用者自己的id
frienduid是好友的id
一个好友就是一条记录,通过多表联合查询,可以直接将好友列表的结果一次性读取出来。增删都很方便。

之后便是存储聊天信息。大致按LayIM本身的信息结构存入就行,只是额外添加上一个自增字段,一个用于判断是否推送离线消息的字段。

msg表:mididusernameavatarcontenttoidtousernametypetimestamppush
push为1时,表式推送消息。

后台需要一个管理界面,所以需要一个表用于记录后台管理员账号信息。

manager表:idusernamepasswordlogintime
password为加盐的二次MD5值。

后台消息收发逻辑

登陆
  • 因为主站是使用PHP,即时聊天使用Node.JS提供API,所以不用设计注册登陆功能,只需要接收用户相关字段并更新就行。为了避免用户手动修改前端的配置信息,所以IM接收的是一串hash字符,IM后端接收到后利于这字符向主站拿取用户信息。
    需要严格按LayIM要求的格式组装好JSON。LayIM文档
layim.config({
    init: {
    url: '' // API的地址,以get形式提交hash
    ,data: {}
    }
……

用户登陆除了向主站取得用户信息,还需要查询数据库中的好友数据,需要usergroup两个表联合查询,ThinkJS中提供model.join(join)来联合查询,但始终避免不了要自己写上表前缀,如果修改表,此处容易被忽略。

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

distinct是去重的字段,field是筛选字段。

WebSocket

几个主要的地方

  • 主要是用户信息与SocketID的对应,利用一个变量来记录所有的SocketID与用户信息的对应,方便查询。
  • 同时需要PC端、Mobile端两端都能正常接收信息,并且PC端重复打开页面,只发送信息给最后一个打开的页面。
  • 前端连接上socket的时候,发送用户信息中包括登陆端类型(PCMobile),然后利用两个变量分别记录PC端和Mobile端的用户。
  • 由于Mboile正常情况需要点击进入聊天界面才知道是否有新消息,所以额外增加一个事件,用来推送未读提醒。

其他功能同样以此方式调取用户信息执行相关操作。添加好友、删除好友,由于不是主要功能,所以以最简单的方式,附加在工具栏上实现。

Socket登记流程

当用户连接上socket时,会默认执行connect事件,其中写入一连接成功即发送用户数据至后台。

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

由于LayIM的ready事件需要使用url获取用户信息才会触发,同时Mobile中也没有提供这样事件,所以全部由socket.io的connect事件替代。
后台接收到数据,并验证数据可用。然后以SocketID为下标,存入变量中,这样每个通过验证的连接都可以知晓是属于哪个用户。
但是这样不利于查询一个用户下的所有连接,同时LayIM发送信息中只提供了用户ID。所以需要再以用户ID为下标,分别将PC与Mobile存入不同的变量中,以便管理。
这样只要检查变量中是否存在用户ID,便可以知道这个用户是否在线,因为是push存入信息,所以最后一个必然是最新的SocketID

前端发送用户信息至后台 ---> 后台验证数据是否可用 ---> 可用则以SocketID为下标存入变量  ---> 再以用户ID为下标存入变量 ---> 读取离线消息等其他操作
Socket退出流程

退出会响应close事件,在close函数中可以获得用户的SocketID,通过这个可以获取此次退出的是哪个用户。先删除SocketID为下标的变量中的数据,再删除以用户ID为下标的PC、Mobile两个变量中的数据。
为了避免各种意外状况,每次退出时最后再将在线用户列表中的SocketID在所有连接对象thinkCache(thinkCache.WEBSOCKET)中进行检测,不存在则删除。

发送消息流程

LayIM发送的消息会自动打包成指定格式,其中包含发送用户信息、接收用户信息、发送消息类型等。
后端接收到数据,先判断消息类型,以分别执行对应操作。如果是friend类型,则提取消息发送出去。
先会将消息组织成数据库可存入的结构、消息发送的结构,然后通过用户ID判断用户是否在线,在线则通过用户ID提取出最新的SocketID。ThinkJS把当前所有的连接对象都存在了thinkCache(thinkCache.WEBSOCKET)只需要以SocketID为下标,调出对象,就可以调用emit向指定连接发送事件。
如果有执行发送,则push字段值修改为0,否则按默认为1。下次用户登陆后会先读取push1的数据推送至前端。
最后将数据存入数据库。

接收到消息  ---> 重新拆分按指定格式重组数据  ---> 根据ID判断用户是否在线  ---> 将数据存入数据库

后台管理页面

LayIM中本身就需要引入LayUI,所以就直接使用LayUI,找了一些默认的页面元素实现了前端页面。

登陆

直接访问IM服务器地址会出现简单的服务器信息,点击图片会跳转入登陆页面(本来没打算做管理页面,常用的服务器信息都在此页面)。


密码考虑到虽然前端经过MD5无意义,但总是可以避免明文密码在传输过程中被泄露,到后台再经过加盐二次MD5,存入数据库。


用户登陆页

前端的登陆信息,传到后台与数据库比对,正确即在session中标记登陆。若错误,则以json返回错误信息。退出登陆则清除session中存储的信息。
ThinkJS提供了this.success()this.fail()this.json()函数用来输出JSON。页面跳转可以使用http.redirect(),其中输入需要跳转的页面地址即可。

用户管理

用户管理主要实现可以查看所有用户,查看每个用户的聊天对象,以及一对一聊天的聊天记录。
没有添加、删除、修改功能,因为所有用户数据都是在主站上,IM仅仅只是接收,并且每次登陆即更新,添加、删除、修改都没意义。
ThinkJS中有提供分页查询函数model.page(page, listRows)page为页数、listRoes为每页条数,会自动转化为limit语句。
在此页面有个功能并未做到网页中。打开开发者工具,会输出登陆的用户、PC端、Mobile端、移动端未读信息连接,四个变量中的数据,方便调试时查看网站情况。

用户管理页
聊天记录
后台账号管理

没有需要设置的地方,做一个演示功能。可添加新账号、修改自己账号密码、删除账号


后台设置-修改密码中

上线

上线需要启动www/production.js文件,不再是www/development.js,并且修改文件后不会再自动编译运行载入,要手动编程、重启。
其中ThinkJS默认会关闭静态资源访问,需要在src/common/config/env/production.js文件中的resource_on改为true
有使用HTTPS可以使用Nginx作反向代理。

后记

第一次接触ThinkJS框架,理解上花了一些时间。LayIM因一些不知缘由的小错误,也处理了好些时间(大部分是数据装后不规范)。
socket部分因为是自己控制登陆信息、检测用户权限,所以逻辑上比较多。虽然总感觉可以写的更加简练,但目前没找到更适合的方法。就程序本身来说并没太多难的地方。

起初重写IM后台时,打算写一篇详细的入门教程。但写写文章时发现,有很多细节零散的东西,写入教程变动啰嗦影响阅读,不写入又不算是入门教程,于是最后改成仅仅记录一些主要的逻辑。大部分地方需要配合源码与文档一起看。

源码

暂未开放

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,339评论 0 15
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,565评论 25 707
  • 苍南一个私人皮肤门诊内,看着丁医生被围得水泄不通,暗暗庆幸自己一大早就打电话取了号,还有两个就轮到了。等啊等...
    晓卉669阅读 1,100评论 1 0
  • 安装了vmware-tools和open-vm-tools后,发现能找到hgfs但是还是找不到分享文件夹 解决方法 原文
    silentsvv阅读 4,117评论 0 1
  • 今天是阴历九月三十。每年的这个日子,我都会去看您,和您说说心里话。今天,我又如约而至。 现在,我已经接受...
    玫兰妮阅读 3,989评论 0 1