前言: 最近在寫一個聊天室的項目,前端寫了挺多的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("請輸入要發送的消息") } } });
第四步:通過ajax將消息發送到后台
4.1、消息的格式:
var msg_item={ "from":"{{request.user.userprofile.id}}", "to":contact_id, "type":contact_type, "msg":msg_text //要發送的消息
};
type為發送的格式:
- 為single表示一對一發送;
- 為group表示群發,此時的"to",后面的3表示群組id
前面都沒什么難度的,到這里通過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): 3 GLOBAL_MSG_QUEUES[queue_id]=queue.Queue() #創建隊列
4
5 GLOBAL_MSG_QUEUES[queue_id].put(msg_dic) #將消息字典(帶時間戳)放進隊列
4.4、將消息字典(含時間戳)成功存到待接收用戶的隊列后,ajax請求完畢,返回"---receive msg---",表示后台已成功接收到要發送的消息,表示消息字典成功存到待接收用戶的隊列中。
return HttpResponse("---receive msg---")
第五步:將發送的信息打印到自己的window界面,調用AddSendMsgIntoWindow(msg_text);
function AddSendMsgIntoWindow(msg_text){ varnew_msg_ele="<divclass='msg-item'>"+
"<span>" + "{{request.user.userprofile.name}}" + "</span>" +
"<span>" + newDate().toLocaleDateString() + "</span>" +
"<divclass='msg-text'>" + msg_text + "</div>" +
"</div>"; $(".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 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. When I scroll to the top left corner of this window, both its scrollTop and scrollLeft values are 0. 還有一個人作了補充: 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.
總的來說,scrollTop就是卷起來的部分,也就是我們隨着下拉,看不見的部分。scrollHeight就是整個窗口可以滑動的高度。
so, 我用下面的方法就可以解決了,每隔0.5s 自動向下滾動至底部, animate() 方法
$(".chat-box-window").animate({ scrollTop:$(".chat-box-window")[0].scrollHeight },5000);
第六步:界面一加載完畢就開始調用GetNewMsgs();開始取消息,向后台發起ajax起求
如何取消息?? 即是說:A向B發數據,后台收到A的數據后,如何返回給B
- 后台設置一個隊列,將A發送的消息存到專屬於B的隊列中。
- 前端寫一個定時器,每隔3秒就通過ajax去后台查詢用戶的隊列是否為空,不為空的話,則取出數據。
{# 用定時器瀏覽器會崩(卡)#} {# setInterval(function () {#} {# GetNewMsgs();#} {# }, 3000)#}
3、用定時器的話,會出現消息不實時的情況。比如,A用戶啪啪啪很快發了很多數據,但B用戶每隔3秒才去后台取數據,中間有最多3秒的時延。這就出現消息不實時的問題。
4、用戶查詢自己的隊列中是否有無數據,無數據的話則后台掛起。
# 超時掛起
# 若隊列為空,則60秒后會曝queue.Empty異常(相當在這60秒內卡住掛起)
try: msg_list.append(q_obj.get(timeout=60)) except queue.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
var session_id = $(".chat-box-title").attr("contact-id"); var session_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
var chat_record = GLOBAL_CHAT_RECORD_DIC[contact_type][contact_id]; console.log(chat_record,typeof chat_record); if (typeof chat_record == "undefined"){ $(".chat-box-window").html(""); }else { // 如果chat_record為undefined,則下面代碼無法將對話界面清空(重要)
$(".chat-box-window").html(chat_record); console.log("haha>>", chat_record) }
視圖切換功能基本完成。
第九步:前端通過ajax接收到后台返回的數據后,將數據渲染成html樣式后顯示在聊天窗口。
這里就要分情況了。
如果用戶收到的是與當前正在聊天用戶的消息,直接將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 (typeof GLOBAL_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]不為undefined
GLOBAL_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給我。歡迎交流。