前言: 最近在写一个聊天室的项目,前端写了挺多的JS(function),导致有点懵比,出了BUG,也迟迟找不到。所以昨天把写过的代码总结了一下,写成博客。

项目背景

参考博客: http://www.cnblogs.com/alex3714/articles/5337630.html

先直观来几张图感受下

最开始的界面布局:

加点bootstrap样式:

实时的聊天效果:

第一步:点击左侧界面的好友,触发事件,打开聊天界面

1.1、给点击好友添加active属性,使其高亮。

Alex Li是一个li标签,属性有联系类型,与Alex Li的用户id.

  1. <li contact-type="single" id="1" class="list-group-item active" onclick="OpenChatWindow(this)"> </li>

1.2、上面的single与id是怎么来的呢? 见如下html代码。

  1. <ul class="list-group">
  2. {% for friend in request.user.userprofile.friends.select_related %}
  3. <li contact-type="single" id="{{friend.id}}" class="list-group-item"
  4. onclick="OpenChatWindow(this)">
  5. <span class="badge hide">14</span> <!-- 新消息提醒数量 -->
  6. <span class="contact-name">{{friend.name}}</span>
  7. </li>
  8. {%endfor%}
  9. </ul>

第二步:使界面标题框出现"正在和Alex Li聊天"字样。

并给其div 添加contact-id,与contact-type属性。

  1. <div class="chat-box-title" contact-id="1" contact-type="single"><span>正在和 Alex Li 聊天</span></div>

第三步:通过事件委托(可看博客《聊一聊JQ中delegate事件委托的好处》)绑定事件,一按回车键就调用SendMsg(msg_text);发送消息

  1. // 事件委托
  2. $("body").delegate("textarea", "keydown", function (e) { // e==event
  3. if(e.which == 13){ // 按下键的数字(e.which);13是enter键的ASCII码
  4. var msg_text = $("textarea").val();
  5. if ($.trim(msg_text).length > 0){
  6. console.log(msg_text);
  7. // 发送消息给对方
  8. SendMsg(msg_text);
  9. // 将发送的信息打印到自己的window界面
  10. AddSendMsgIntoWindow(msg_text);
  11. $("textarea").val(""); // 将输入框清空
  12. }else {
  13. alert("请输入要发送的消息")
  14. }
  15.  
  16. }
  17. });

第四步:通过ajax将消息发送到后台

4.1、消息的格式:

  1. var msg_item={
  2. "from":"{{request.user.userprofile.id}}",
  3. "to":contact_id,
  4. "type":contact_type,
  5. "msg":msg_text //要发送的消息
  6. };

type为发送的格式:

  • 为single表示一对一发送;
  • 为group表示群发,此时的"to",后面的3表示群组id

前面都没什么难度的,到这里通过ajax将消息字典(json格式)发到后台,后台怎么处理么?

简单阿,将消息发给用户阿,那要是用户没登陆呢??那只好先将数据存起来,存在哪呢?要满足先进先出,可以用队列queue,当然,生产环境最好用rabbitmq(《python之rabbitMQ》);

4.2、后台每个用户都有一条队列queue. 后台的全局队列如下: 以用户的id作为key, 队列作为value

  1. GLOBAL_MSG_QUEUES={
  2. "id": queue.Queue(),
  3. }#队列:全局变量

4.3、将消息字典存到待接收用户的队列中去,如果用户此时队列不存在,则先给待接收消息的用户生成对应的一条队列,再将消息字典存到待接收用户的队列中去。

  1. #如果用户队列不存在,注意,如果id(key)对应的队列不存在,则输出None,不会曝错
  2. if not GLOBAL_MSG_QUEUES.get(queue_id):
  3. GLOBAL_MSG_QUEUES[queue_id]=queue.Queue() #创建队列
  4.  
  5. GLOBAL_MSG_QUEUES[queue_id].put(msg_dic) #将消息字典(带时间戳)放进队列

4.4、将消息字典(含时间戳)成功存到待接收用户的队列后,ajax请求完毕,返回"---receive msg---",表示后台已成功接收到要发送的消息,表示消息字典成功存到待接收用户的队列中。

  1. return HttpResponse("---receive msg---")

  

第五步:将发送的信息打印到自己的window界面,调用AddSendMsgIntoWindow(msg_text);

  1. function AddSendMsgIntoWindow(msg_text){
  2. varnew_msg_ele="<divclass='msg-item'>"+
  3. "<span>" + "{{request.user.userprofile.name}}" + "</span>" +
  4. "<span>" + newDate().toLocaleDateString() + "</span>" +
  5. "<divclass='msg-text'>" + msg_text + "</div>" +
  6. "</div>";
  7. $(".chat-box-window").append(new_msg_ele);
  8.  
  9. $(".chat-box-window").animate({
  10. scrollTop:$(".chat-box-window")[0].scrollHeight//每隔0.5s自动向下滚动
  11. },500);
  12. //console.log($(".chat-box-window")[0]);
  13. }

将消息打印到自己的界面一开始会出现问题,比如你发了很多数据,界面整个div已经装不下了,就会“溢出”div。简单阿,给聊天界面加个"overflow"样式就行了

  1. overflow: auto; /* 给div 内容多了自动加滚动条 */

太棒了,现在聊天内容一多,就自动出现滚动条,牛!! 但问题又来了,虽然有滚动条,但每次你一发消息,都得自己去拉滚动条到最底部才能看到刚刚发的消息。这……

于是我上网找到了这篇博客:scrollTop 和 scrollHeight的意思

  1. 今天要用到实时显示最近更新内容,也就是要让对话框随时都在最底部。
  2. 查了一下,
  3. div.scrollTop=div.scrollHeight;就可以了。
  4. 又查了查这两个参数什么意思。stackoverflow上面有人是这样解答的。
  5.  
  6. If I scroll down 5px in this window, the window's scrollTop value is 5. If I scroll right 10px in a scrollable div, the div's scrollLeft value is 10.
  7. When I scroll to the top left corner of this window, both its scrollTop and scrollLeft values are 0.
  8. 还有一个人作了补充: scrollTop and scrollHeight. In summary, scrollTop is how much it's currently scrolled, and scrollHeight is the total height, including content scrolled out of view.
  9. 总的来说,scrollTop就是卷起来的部分,也就是我们随着下拉,看不见的部分。scrollHeight就是整个窗口可以滑动的高度。

so, 我用下面的方法就可以解决了,每隔0.5s 自动向下滚动至底部, animate() 方法

  1. $(".chat-box-window").animate({
  2. scrollTop:$(".chat-box-window")[0].scrollHeight
  3. },5000);

第六步:界面一加载完毕就开始调用GetNewMsgs();开始取消息,向后台发起ajax起求

如何取消息?? 即是说:A向B发数据,后台收到A的数据后,如何返回给B

  1. 后台设置一个队列,将A发送的消息存到专属于B的队列中。
  2. 前端写一个定时器,每隔3秒就通过ajax去后台查询用户的队列是否为空,不为空的话,则取出数据。
  1. {# 用定时器浏览器会崩(卡)#}
  2. {# setInterval(function () {#}
  3. {# GetNewMsgs();#}
  4. {# }, 3000)#}

  3、用定时器的话,会出现消息不实时的情况。比如,A用户啪啪啪很快发了很多数据,但B用户每隔3秒才去后台取数据,中间有最多3秒的时延。这就出现消息不实时的问题。

  4、用户查询自己的队列中是否有无数据,无数据的话则后台挂起。

  1.      # 超时挂起
  2. # 若队列为空,则60秒后会曝queue.Empty异常(相当在这60秒内卡住挂起)
  3. try:
  4. msg_list.append(q_obj.get(timeout=60))
  5. except queue.Empty: # 若队列为空,则60秒后会曝queue.Empty异常(相当在这60秒内卡住挂起)
  6. print("\033[41;1mno msg for [%s][%s],timeout\033[0m" % (request.user.userprofile.id,request.user))

虽然队列无数据时,后台可以挂起,但是前端用定时器每3秒发起一次ajax起求,问 "我的队列中有没有来新数据阿??", 每次起求相当于浏览器起一个线程,时间一长,浏览器会卡,撑不住。这怎么办呢?? 难道不能用定时器?? 那还能用什么牛逼屌炸天的方法?

按我最好情况的理解是,前端每发起一次请问,若队列有数据,则立马取数据,若无数据,则挂起60秒(时间可在后台设置),这60秒内若队列中有数据,同样从队列取数据,若60秒内队列都无数据,则出queue.Empty异常,此时前端再发起ajax请求。

在解决上面的浏览器线程过多,太卡前,我们先来看看后台是如何处理队列为空时挂起 这一功能的。看下面这张图加上上面第4点的代码,你应该懂的。

好,回到浏览器太卡这个BUG上来,我用了递归这个方法,代码如下:

  1. // 用户获取消息
  2. function GetNewMsgs() {
  3. console.log("----getting new msg----");
  4. $.getJSON("{% url 'get_new_msgs' %}",
  5. function (callback) {
  6. // callback是列表对象,object, 列表每个元素都是一个消息字典
  7. console.log(callback, typeof callback); // Array [Object] object
  8. // 解析消息,用户可能收到与当前正在聊天用户的消息,也可能是其他用户发的消息
  9. ParseNewMsgs(callback);
  10.  
  11. return GetNewMsgs(); // 递归
  12. })
  13. }

不用定时器。前端发起一个ajax请求,后端队列无数据则挂起。当后台向前端返回数据时,有两种情况,一种是超时,如60秒内,队列都无新消息,出queue.Empty异常后返回数据;第二种是用户接收到别人发给他的数据,队列一有数据,则返加给前端。前端收到数据后,回调函数再发起一个ajax请求。

若前端没收到数据(在后台挂起的时间内),则不会发起ajax请求,此时相当于实现前端挂起。不会起太多的线程。

(注: python专门设置的一种机制用来防止无限递归造成Python溢出崩溃。在python的递归是有层数据限制的,999层。超过就抛出 “RuntimeError: maximum recursion depth exceeded” )

第七步

后台查询用户队列中是否有消息(数据字典),无的话就挂起,一分钟无消息,则前端再发起ajax请求。若队列中有消息的话,则返回给用户,返回形式是列表形式,列表每个元素都是数据字典形式。

  1. eg:{"from":"3","to":"2","type":"single","msg":"1111"} <class 'str'>
  2.  
  3. return HttpResponse(json.dumps(msg_list))#序列化,转化为json格式

第八步:

问题: A用户与B女神聊的正嗨,点击左侧好友切换到C女神, 与C聊天,但聊天窗口,依旧是与B女神聊天的内容。

接下来要完成视图切换功能,在切换之前将原本的视图的html元素存到全局字典

  1. // 全局字典,用于存切换视图前的html元素
  2. GLOBAL_CHAT_RECORD_DIC = {
  3. "single":{},
  4. "group":{}
  5. };

全局字典的作用可将C女神发来的消息存入,格式应在"single"的value(字典),再设置一个用户id与用户聊天数据的字典(C用户的id为3,xxx为将经过处理的html元素)。

如:"single":{"3": "xxx"},

切换视图前将与B女神聊天窗口的html元素(数据)添加到用户的全局字典中去:

  1. // 视图切换,在切换之前将原本的视图的html元素存到全局字典
  2. // 原本的框题框contact-id属性不为空,即切换视图前左侧已有点击对象
  3. if ($(".chat-box-title").attr("contact-id")){
  4. // 从标题框取出切换前用户的id,与联系方式contact-type
  5. var session_id = $(".chat-box-title").attr("contact-id");
  6. var session_type = $(".chat-box-title").attr("contact-type");
  7. // 将切换产前的视图存入全局字典
  8. GLOBAL_CHAT_RECORD_DIC[session_type][session_id] = $(".chat-box-window").html();
  9. }

框题框显示"正在与C女神聊天"后,从全局字典取出与C的聊天数据,并显示在聊天窗口。

  1. // 把chat-window的html元素从全局字典中存出来
  2. // 第一次点击该联系人时,chat_record为undefined,因为字典的single下还没有生成id(key)对应的value
  3. var chat_record = GLOBAL_CHAT_RECORD_DIC[contact_type][contact_id];
  4. console.log(chat_record,typeof chat_record);
  5. if (typeof chat_record == "undefined"){
  6. $(".chat-box-window").html("");
  7. }else {
  8. // 如果chat_record为undefined,则下面代码无法将对话界面清空(重要)
  9. $(".chat-box-window").html(chat_record);
  10. console.log("haha>>", chat_record)
  11. }

视图切换功能基本完成。

第九步:前端通过ajax接收到后台返回的数据后,将数据渲染成html样式后显示在聊天窗口。

这里就要分情况了。

如果用户收到的是与当前正在聊天用户的消息,直接将html元素添加到聊天窗口$(".chat-box-window").append(new_msg_ele);  否则,如:用户A正在与B聊天,此时收到来自C发来的消息。C发来的消息要发在哪里呢? 当然是前面设置的全局变量啊!!

  1. // 用户收到的是与当前正在聊天用户的消息
  2. if (current_session_id==callback[msg_item]["from"] && current_session_type==callback[msg_item]["type"]){
  3. // 将消息的html元素添加到聊天窗口
  4. $(".chat-box-window").append(new_msg_ele);
  5. }else {
  6. // 用户没打开消息发送者的对话框,消息暂存到内存中(全局变量中)
  7. if (typeof GLOBAL_CHAT_RECORD_DIC[callback[msg_item]["type"]][callback[msg_item]["from"]]=="undefined"){
  8. GLOBAL_CHAT_RECORD_DIC[callback[msg_item].type][callback[msg_item].from]=new_msg_ele;
  9. }else { // 如果GLOBAL_CHAT_RECORD_DIC[current_session_type][current_session_id]不为undefined
  10. GLOBAL_CHAT_RECORD_DIC[callback[msg_item].type][callback[msg_item].from]+=new_msg_ele;
  11. }
  12. }

先写这么多吧。转发注明出版: http://www.cnblogs.com/0zcl/p/6903017.html

前几天学了git,有想一起做这个小项目的么??一个人写代码总感觉太慢了,有想一起完成的可以看看我的git项目https://github.com/0zcl/bbs_project,可以pull给我。欢迎交流。

web聊天室总结的更多相关文章

  1. 使用Servlet和JSP实现一个简单的Web聊天室系统

    1 问题描述                                                利用Java EE相关技术实现一个简单的Web聊天室系统,具体要求如下. (1)编写一个登录 ...

  2. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室 实战系列

    ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(零) 前言  http://www.cnblogs.com/panzi/p/5742089.html ASP.NET S ...

  3. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(十二) 代码重构使用反射工厂解耦(一)缓存切换

    前言 上一篇中,我们用了反射工厂来解除BLL和UI层耦合的问题.当然那是最简单的解决方法,再复杂一点的程序可能思路相同,但是在编程细节中需要考虑的就更多了,比如今天我在重构过程中遇到的问题.也是接下来 ...

  4. web聊天室

    开发一个web聊天室 功能需求: 1.用户可以与好友一对一聊天 2.群聊 所需知识 1.Django 2.bootstrap 3.CSS 4.ajax 涉及到的新的知识点 1.如果设计表结构的时候,一 ...

  5. 利用html 5 websocket做个山寨版web聊天室(手写C#服务器)

    在之前的博客中提到过看到html5 的websocket后很感兴趣,终于可以摆脱长轮询(websocket之前的实现方式可以看看Developer Works上的一篇文章,有简单提到,同时也说了web ...

  6. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(零) 前言

    前端时间听一个技术朋友说 LayIM 2.0 发布了,听到这个消息抓紧去官网看了一下.(http://layim.layui.com/)哎呀呀,还要购买授权[大家支持一下哦],果断买了企业版,喜欢钻研 ...

  7. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(一) 之 基层数据搭建,让数据活起来(数据获取)

    大家好,本篇是接上一篇 ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(零) 前言  ASP.NET SignalR WebIM系列第二篇.本篇会带领大家将 LayIM ...

  8. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(二) 之 ChatServer搭建,连接服务器,以及注意事项。

    上篇:ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(一) 之 基层数据搭建,让数据活起来(数据获取) 上一篇我们已经完成了初步界面的搭建工作,本篇将介绍IM的核心内容 ...

  9. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(三) 之 实现单聊,群聊,发送图片,文件。

    上篇讲解了如何搭建聊天服务器,以及客户端js怎么和layui的语法配合.服务器已经连接上了,那么聊天还会远吗? 进入正题,正如上一篇提到的我们用 Client.Group(groupId)的方法向客户 ...

  10. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(四) 之 用户搜索(Elasticsearch),加好友流程(1)。

    前面几篇基本已经实现了大部分即时通讯功能:聊天,群聊,发送文件,图片,消息.不过这些业务都是比较粗犷的.下面我们就把业务细化,之前用的是死数据,那我们就从加好友开始吧.加好友,首先你得知道你要加谁.L ...

随机推荐

  1. jquery判断邮箱对错

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  2. 【js数据结构】可逐次添加叶子的二叉树(非最优二叉树)

    最近小菜鸟西瓜莹看到了一道面试题: 给定二叉树,按层打印.例如1的子节点是2.3, 2的子节点是3.4, 5的子节点是6,7. 需要建立如图二叉树: 但是西瓜莹找到的相关代码都是用js构建最优二叉树, ...

  3. 串口屏Modbus协议,串口屏的modbus协议资料,串口屏modbus通讯协议开发,串口屏之modbus协议使用技巧

    串口屏Modbus协议,串口屏的modbus协议资料,串口屏modbus通讯协议开发,串口屏之modbus协议使用技巧 本例程中用51单片机作为Modbus从机,从机的设备地址为2,从机有4个寄存器, ...

  4. Elasticsearch高级搜索排序( 中文+拼音+首字母+简繁转换+特殊符号过滤)

    一.先摆需求: 1.中文搜索.英文搜索.中英混搜   如:"南京东路","cafe 南京东路店" 2.全拼搜索.首字母搜索.中文+全拼.中文+首字母混搜   如 ...

  5. JavaScript分支语句if, else if, switch 案例详解

      if语句主要是在需要判断,或者在可知有多少种情形时使用的语句.A==B?"A等于B";"A不等于B"; 基本结构:           if(判断条件){ ...

  6. AngularJS创建新指令 - 基本功能

    指令(Directives)是所有AngularJS应用最重要的部分.尽管AngularJS已经提供了非常丰富的指令,但还是经常需要创建应用特定的指令. AngularJS原有的指令 ng-init  ...

  7. python——杂货铺

    三目运算: >>> 1 if 5>3 else 0 1 >>> 1 if 5<3 else 0 0 深浅拷贝: 一.数字和字符串 对于 数字 和 字符串 ...

  8. 深入tornado中的Configurable

    Configurable十分重要! 位于tornado.util文件中,它是一个工厂类. 我们暂且称这个类为 配置类 . 我们暂且约定:该类的子类称之为 直属配置子类 , 该类的孙类.重孙类……称之为 ...

  9. 关于li标签之间的间隔如何消除!

    问题:li标签用了display:inline之后虽然成功的合并在一行,但是li标签之间出现了间距. 原因:按enter键换行之后li标签之间存在着空格,正是这些空格占据了li标签之间的空间. 解决方 ...

  10. hdu2767强连通加缩点

    https://vjudge.net/contest/156688#problem/B 题目说了一大堆,前面的没有用,就是让你判断要加几条边才能强连通,用到缩点的知识 二重循环,判断邻接表下一个点是不 ...