前言: 最近在写一个聊天室的项目,前端写了挺多的 JS(function),导致有点懵比,出了 BUG,也迟迟找不到。所以昨天把写过的代码总结了一下,写成博客。
项目背景
参考博客: http://www.cnblogs.com/alex3714/articles/5337630.html
最开始的界面布局:
加点 bootstrap 样式:
实时的聊天效果:
1.1、给点击好友添加 active 属性,使其高亮。
Alex Li 是一个 li 标签,属性有联系类型,与 Alex Li 的用户 id.
- <li contact-type="single" id="1" class="list-group-item active" onclick="OpenChatWindow(this)">
- </li>
1.2、上面的 single 与 id 是怎么来的呢? 见如下 html 代码。
- <ul class="list-group">
- {% for friend in request.user.userprofile.friends.select_related %}
- <li contact-type="single" id="{{friend.id}}" class="list-group-item" onclick="OpenChatWindow(this)">
- <span class="badge hide">
- 14
- </span>
- <!-- 新消息提醒数量 -->
- <span class="contact-name">
- {{friend.name}}
- </span>
- </li>
- {%endfor%}
- </ul>
第二步:使界面标题框出现 "正在和 Alex Li 聊天" 字样。
并给其 div 添加 contact-id, 与 contact-type 属性。
- <div class="chat-box-title" contact-id="1" contact-type="single">
- <span>
- 正在和 Alex Li 聊天
- </span>
- </div>
第三步:通过事件委托 (可看博客《聊一聊 JQ 中 delegate 事件委托的好处》) 绑定事件,一按回车键就调用 SendMsg(msg_text); 发送消息
- // 事件委托
- $("body").delegate("textarea", "keydown",
- function(e) { // e==event
- if (e.which == 13) { // 按下键的数字(e.which);13是enter键的ASCII码
- var msg_text = $("textarea").val();
- if ($.trim(msg_text).length > 0) {
- console.log(msg_text);
- // 发送消息给对方
- SendMsg(msg_text);
- // 将发送的信息打印到自己的window界面
- AddSendMsgIntoWindow(msg_text);
- $("textarea").val(""); // 将输入框清空
- } else {
- alert("请输入要发送的消息")
- }
- }
- });
4.1、消息的格式:
- varmsg_item={
- "from":"{{request.user.userprofile.id}}",
- "to":contact_id,
- "type":contact_type,
- "msg":msg_text//要发送的消息};
type 为发送的格式:
前面都没什么难度的,到这里通过 ajax 将消息字典 (json 格式) 发到后台,后台怎么处理么
简单阿,将消息发给用户阿,那要是用户没登陆呢?? 那只好先将数据存起来,存在哪呢? 要满足先进先出,可以用队列 queue,当然,生产环境最好用 rabbitmq(《python 之 rabbitMQ》);
4.2、后台每个用户都有一条队列 queue. 后台的全局队列如下: 以用户的 id 作为 key, 队列作为 value
- GLOBAL_MSG_QUEUES={
- "id": queue.Queue(),
- }#队列:全局变量
4.3、将消息字典存到待接收用户的队列中去,如果用户此时队列不存在,则先给待接收消息的用户生成对应的一条队列,再将消息字典存到待接收用户的队列中去。
- 1 #如果用户队列不存在,注意,如果id(key)对应的队列不存在,则输出None,不会曝错
- 2 if not GLOBAL_MSG_QUEUES.get(queue_id):
- 3GLOBAL_MSG_QUEUES[queue_id]=queue.Queue()#创建队列
- 4
- 5GLOBAL_MSG_QUEUES[queue_id].put(msg_dic)#将消息字典(带时间戳)放进队列
4.4、将消息字典 (含时间戳) 成功存到待接收用户的队列后,ajax 请求完毕,返回 "---receive msg---",表示后台已成功接收到要发送的消息,表示消息字典成功存到待接收用户的队列中。
- return HttpResponse("---receive msg---")
- function AddSendMsgIntoWindow(msg_text){
- varnew_msg_ele=""+
- "" + "{{request.user.userprofile.name}}" + "" +
- "" + newDate().toLocaleDateString() + "" +
- "" + msg_text + "" +
- "";
- $(".chat-box-window").append(new_msg_ele);
- $(".chat-box-window").animate({
- scrollTop:$(".chat-box-window")[0].scrollHeight//每隔0.5s自动向下滚动},500);
- //console.log($(".chat-box-window")[0]);}
将消息打印到自己的界面一开始会出现问题,比如你发了很多数据,界面整个 div 已经装不下了,就会 "溢出"div。简单阿,给聊天界面加个 "overflow" 样式就行了
- overflow: auto;
- /* 给div 内容多了自动加滚动条 */
太棒了,现在聊天内容一多,就自动出现滚动条,牛!! 但问题又来了,虽然有滚动条,但每次你一发消息,都得自己去拉滚动条到最底部才能看到刚刚发的消息。这……
于是我上网找到了这篇博客:scrollTop 和 scrollHeight 的意思
- 今天要用到实时显示最近更新内容,也就是要让对话框随时都在最底部。
- 查了一下,
- 用div.scrollTop=div.scrollHeight;就可以了。
- 又查了查这两个参数什么意思。stackoverflow上面有人是这样解答的。
- If I scroll down 5pxinthis window, the window's scrollTop value is 5. If I scroll right 10px in a scrollable div, the div's scrollLeft valueis10.
- When I scroll to the top left corner of this window, both its scrollTop and scrollLeft values are 0.
- 还有一个人作了补充: scrollTop andscrollHeight. In summary, scrollTopishow much it's currently scrolled, and scrollHeight is the total height, including content scrolled out of view.总的来说,scrollTop就是卷起来的部分,也就是我们随着下拉,看不见的部分。scrollHeight就是整个窗口可以滑动的高度。
so, 我用下面的方法就可以解决了,每隔 0.5s 自动向下滚动至底部, animate() 方法
- $(".chat-box-window").animate({
- scrollTop: $(".chat-box-window")[0].scrollHeight
- },
- 5000);
如何取消息?? 即是说: A 向 B 发数据,后台收到 A 的数据后,如何返回给 B
- {#用定时器浏览器会崩 (卡)#
- } {#setInterval(function() {#
- } {#GetNewMsgs();#
- } {#
- },
- 3000)#
- }
3、用定时器的话,会出现消息不实时的情况。比如,A 用户啪啪啪很快发了很多数据,但 B 用户每隔 3 秒才去后台取数据,中间有最多 3 秒的时延。这就出现消息不实时的问题。
4、用户查询自己的队列中是否有无数据,无数据的话则后台挂起。
- # 超时挂起
- # 若队列为空,则60秒后会曝queue.Empty异常(相当在这60秒内卡住挂起)
- try:
- msg_list.append(q_obj.get(timeout=60))
- exceptqueue.Empty:# 若队列为空,则60秒后会曝queue.Empty异常(相当在这60秒内卡住挂起)
- 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 请求。若队列中有消息的话,则返回给用户,返回形式是列表形式,列表每个元素都是数据字典形式。
- eg: {
- "from": "3",
- "to": "2",
- "type": "single",
- "msg": "1111"
- } < class 'str' >
- return HttpResponse(json.dumps(msg_list))#序列化,转化为json格式
问题: A 用户与 B 女神聊的正嗨,点击左侧好友切换到 C 女神, 与 C 聊天,但聊天窗口,依旧是与 B 女神聊天的内容。
接下来要完成视图切换功能,在切换之前将原本的视图的 html 元素存到全局字典
- // 全局字典,用于存切换视图前的html元素GLOBAL_CHAT_RECORD_DIC = {
- "single":{},
- "group":{}
- };
全局字典的作用可将 C 女神发来的消息存入,格式应在 "single" 的 value(字典),再设置一个用户 id 与用户聊天数据的字典 (C 用户的 id 为 3,xxx 为将经过处理的 html 元素)。
如:"single":{"3": "xxx"},
切换视图前将与 B 女神聊天窗口的 html 元素 (数据) 添加到用户的全局字典中去:
- // 视图切换,在切换之前将原本的视图的html元素存到全局字典
- // 原本的框题框contact-id属性不为空,即切换视图前左侧已有点击对象
- if($(".chat-box-title").attr("contact-id")){
- // 从标题框取出切换前用户的id,与联系方式contact-type
- varsession_id = $(".chat-box-title").attr("contact-id");
- varsession_type = $(".chat-box-title").attr("contact-type");
- // 将切换产前的视图存入全局字典GLOBAL_CHAT_RECORD_DIC[session_type][session_id] = $(".chat-box-window").html();
- }
框题框显示 "正在与 C 女神聊天" 后,从全局字典取出与 C 的聊天数据,并显示在聊天窗口。
- // 把chat-window的html元素从全局字典中存出来
- // 第一次点击该联系人时,chat_record为undefined,因为字典的single下还没有生成id(key)对应的value
- varchat_record = GLOBAL_CHAT_RECORD_DIC[contact_type][contact_id];
- console.log(chat_record,typeof chat_record);
- if(typeofchat_record == "undefined"){
- $(".chat-box-window").html("");
- }else {
- // 如果chat_record为undefined,则下面代码无法将对话界面清空(重要)$(".chat-box-window").html(chat_record);
- console.log("haha>>", chat_record)
- }
视图切换功能基本完成。
这里就要分情况了。
如果用户收到的是与当前正在聊天用户的消息,直接将 html 元素添加到聊天窗口 $(".chat-box-window").append(new_msg_ele); 否则,如:用户 A 正在与 B 聊天,此时收到来自 C 发来的消息。C 发来的消息要发在哪里呢? 当然是前面设置的全局变量啊!!
- // 用户收到的是与当前正在聊天用户的消息
- if(current_session_id==callback[msg_item]["from"] && current_session_type==callback[msg_item]["type"]){
- // 将消息的html元素添加到聊天窗口$(".chat-box-window").append(new_msg_ele);
- }else {
- // 用户没打开消息发送者的对话框,消息暂存到内存中(全局变量中)
- if(typeofGLOBAL_CHAT_RECORD_DIC[callback[msg_item]["type"]][callback[msg_item]["from"]]=="undefined"){
- GLOBAL_CHAT_RECORD_DIC[callback[msg_item].type][callback[msg_item].from]=new_msg_ele;
- }else{// 如果GLOBAL_CHAT_RECORD_DIC[current_session_type][current_session_id]不为undefinedGLOBAL_CHAT_RECORD_DIC[callback[msg_item].type][callback[msg_item].from]+=new_msg_ele;
- }
- }
先写这么多吧。转发注明出版: http://www.cnblogs.com/0zcl/p/6903017.html
前几天学了 git,有想一起做这个小项目的么?? 一个人写代码总感觉太慢了,有想一起完成的可以看看我的 git 项目 https://github.com/0zcl/bbs_project,可以 pull 给我。欢迎交流。
来源: http://www.cnblogs.com/0zcl/p/6903017.html