看起來挺簡單,細節還是很多的,好,接上一篇,我們已經成功連線singalR伺服器了,那麼剩下的內容呢,就是一步一步實現聊天功能。
我們先看看缺什麼東西
- 點選好友彈框之後,要給伺服器發訊息,進入組Group.Group原理在上一篇已經介紹了,這裡不再贅述。
- 點選傳送訊息到後臺,後臺在傳送回來
- 將htmlappend到相應元素上,demo已經實現了,我們把程式碼拿過來用就可以了
- 模擬使用者登入,點選傳送聊天
在做上述工作之前,還是要做許多準備工作的。我們分析一下介面元素
好的,可以看到,一個訊息裡面有訊息傳送時間(addtime),使用者名稱(username),使用者頭像(userphoto),使用者訊息體(msgcontent),除此之外還需要使用者id,聊天id,以及組名(groupname).以此我先在後臺建立模型。
namespace LayIM.Model { public enum CSMessageType { System = 1,//系統訊息,出錯,引數錯誤等訊息 Custom = 2 //普通訊息,對話,或者群組訊息 } }
namespace LayIM.Model { public class CSChatMessage { public CSChatMessage() { addtime = DateTime.Now.ToString("HH:mm:ss"); } /// <summary> /// 訊息來源 /// </summary> public CSUser fromuser { get; set; } public CSUser touser { get; set; } /// <summary> /// 訊息內容 /// </summary> public string msg { get; set; } /// <summary> /// 訊息傳送時間 /// </summary> public string addtime { get; set; } /// <summary> /// 訊息型別 /// </summary> public CSMessageType msgtype { get; set; } public object other { get; set; } } }
namespace LayIM.Model { public class SingalRUser { protected string _groupName { get; set; } private string _connectionId { get; set; } /// <summary> /// 使用者當前所在組 /// </summary> public string groupname { get { return this._groupName; } } /// <summary> /// 使用者當前所在connectionid /// </summary> public string connectionid { get { return this._connectionId; } } public SingalRUser(string groupName, string connectionId) { _groupName = groupName; _connectionId = connectionId; } public SingalRUser() { } } /// <summary> /// 使用者Model /// </summary> public class CSUser : SingalRUser { public CSUser(string groupName, string connectionId) : base(groupName, connectionId) { } /// <summary> /// 使用者id /// </summary> public int userid { get; set; } /// <summary> /// 使用者暱稱 /// </summary> public string username { get; set; } /// <summary> /// 使用者頭像 /// </summary> public string photo { get; set; } } }
ok,很簡單的幾個model,CSUser為使用者,CSChatMessage為訊息體。那麼,如果想讓兩個使用者聯通,我們需要得到他們所在的組,即經常說到的 userID1+userID2,生成組名程式碼如下:(主要保證兩個使用者的組名唯一性就可,方法隨意)
/// <summary> /// 根據兩個使用者ID得到對應的組織名稱 /// </summary> /// <param name="sendid">傳送人(主動聯絡人)</param> /// <param name="receiveid">接收人(被動聯絡人)</param> /// <returns></returns> public static string GetGroupName(string sendid, string receiveid) { /* 排序的目的就是為了保證,無論誰連線伺服器,都能得到正確的組織ID */ int compareResult = string.Compare(sendid, receiveid); if (compareResult > 0) { //重新排序 如果sendid>receiveid return string.Format("G{0}{1}", receiveid, sendid); } return string.Format("G{0}{1}", sendid, receiveid); }
現在groupName也有了,我們回到 CustomServiceHub 類中來。新增使用者加入組的方法,這個方法什麼時候呼叫呢,就是當你點選某個使用者頭像彈出聊天框的時候呼叫。
/// <summary> /// 人對人聊天 連線伺服器 /// </summary> /// <param name="sendid">傳送人</param> /// <param name="receiveid">接收人</param> /// <returns></returns> public Task ClientToClient(string sendid, string receiveid) { if (sendid == null || receiveid == null) { throw new ArgumentNullException("sendid or receiveid can't be null"); } //獲取組名 string groupName = MessageUtils.GetGroupName(sendid, receiveid); //將當前使用者新增到此組織內 Groups.Add(CurrentUserConnectionId, groupName); //構建系統連線成功訊息 var msg = MessageUtils.GetSystemMessage(groupName, MessageConfig.ClientToClientConnectedSucceed, new { currentid = sendid, receiveid = receiveid }); //將訊息推送到當前組 (A和B聊天的組) 同樣呼叫receiveMessage方法 return Clients.Caller.receiveMessage(msg); }
裡面有些程式碼是我封裝的,大體看清思路就可以了。下面去讀一下layim.js裡的原始碼,找到彈出使用者視窗那一段。
//彈出聊天窗 config.chatings = 0; node.list.on('click', '.xxim_childnode', function () { var othis = $(this); //當前登入使用者id var currentid = config.user.id; //取得被點選的使用者id var receiveid = othis.data('id'); //呼叫signalR封裝的方法,連線伺服器,將傳送人id,接收人id傳給後臺,當前使用者加入組 csClient.server.ctoc(currentid, receiveid); xxim.popchatbox(othis); });
在看一下csClient到底做了什麼
ctoc: function (sid, rid) { //呼叫hub的clientToClient方法 if (!chat.isConnected(rid)) { //如果沒有連線過,進行連線 console.log("使用者 " + rid + "沒有連線過..."); _this.proxy.proxyCS.server.clientToClient(sid, rid); } else { console.log("使用者 " + rid + "已經連線過了,不需要連線了..."); } },
這裡呢,我另外加了個js物件快取,防止每次點選都要重複連線資料庫,當然,頁面重新整理之後快取消失,需要重新 連。到這裡我們點選一下,看看效果。
好,從上圖可以看到,伺服器返回了成功的訊息,並且,groupname也是按照順序生成的。這個訊息有什麼用呢,其實對於客戶端是沒有什麼效果的,如果想要提示使用者連線成功或者提示對方是否線上可以用到,這裡我不在擴充套件,只是為了列印看是否連線成功,當連線成功之後呢,使用者就會存在組 G1000010003中了,這時候你發訊息如果對面沒有連線的話,他是看不見的。連線成功之後,就要做發訊息功能了。繼續回到 CustomServiceHub 類,新增傳送訊息方法:
/// <summary> /// 傳送訊息 ,伺服器接收的是CSChatMessage實體,他包含傳送人,接收人,訊息內容等資訊 /// </summary> /// <param name="msg"></param> /// <returns></returns> public Task ClientSendMsgToClient(CSChatMessage msg) { var groupName = MessageUtils.GetGroupName(msg.fromuser.userid.ToString(), msg.touser.userid.ToString()); /* 中間處理一下訊息直接轉發給(A,B所在組織,即聊天視窗) */ msg.msgtype = CSMessageType.Custom;//訊息型別為普通訊息 return Clients.Group(groupName).receiveMessage(msg); }
可以看到,同樣是用到了receiveMessage方法,不過這裡呢,呼叫的Clients.Group(groupName)也就是說,傳送的這條訊息職能在這個組內的人才能看到,那麼組裡就兩個人,是不是就實現了1對1 聊天呢,離線留言也支援哦。訊息傳送成功之後,其實不管對方在不線上,我們都可以做一下本地處理,為了演示訊息傳送效果,我們不用本地js在傳送的時候直接拼接到頁面上,而是client端接收到訊息體之後再處理,這樣會看出訊息延時效果。(擴充套件:假如傳送的訊息很慢的話,就可以在訊息體旁邊加一個等待的小菊花,提示傳送成功,失敗等。)好,我直接將layim裡模擬訊息處理的程式碼拿出來了,我們看詳細程式碼。
handleCustomMsg: function (result) { var log = {}; //接收人 var keys = 'one' + result.touser.userid; //傳送人 var keys1 = 'one' + result.fromuser.userid; //這裡一定要注意,這個keys是會變的,也就是說,如果只取一個的話,會造成 log.imarea[0]為undefined的情況,至於為什麼會變,看看程式碼好好思考一下吧 log.imarea = $('#layim_area' + keys);//layim_areaone0 if (!log.imarea.length) { log.imarea = $('#layim_area' + keys1);//layim_areaone0 } //拼接html模板 log.html = function (param, type) { return '<li class="' + (type === 'me' ? 'layim_chateme' : '') + '">' + '<div class="layim_chatuser">' + function () { if (type === 'me') { return '<span class="layim_chattime">' + param.time + '</span>' + '<span class="layim_chatname">' + param.name + '</span>' + '<img src="' + param.face + '" >'; } else { return '<img src="' + param.face + '" >' + '<span class="layim_chatname">' + param.name + '</span>' + '<span class="layim_chattime">' + param.time + '</span>'; } }() + '</div>' + '<div class="layim_chatsay">' + param.content + '<em class="layim_zero"></em></div>' + '</li>'; }; //上述程式碼還是layim裡的程式碼,只不過拼接html的時候,引數採用signalR返回的引數 var type = result.fromuser.userid == currentUser.id ? "me" : "";//如果傳送人的id==當前使用者的id,那麼這條訊息型別為me //拼接html 直接呼叫layim裡的程式碼 log.imarea.append(log.html({ time: result.addtime, name: result.fromuser.username, face: result.fromuser.photo, content: result.msg }, type)); //滾動條處理 log.imarea.scrollTop(log.imarea[0].scrollHeight); },
好了, 程式碼也都處理完了,這裡呢有個小插曲,我們怎麼確定當前使用者是誰呢?由於我寫的是死資料,所以我就採用隨機生成的方法,然後將使用者儲存到 localStorage裡面了,這樣當使用者再次開啟頁面,還是會取到第一次的使用者,這裡呢不多做介紹了。
/* 獲取隨機一個使用者 當使用者第一次登陸就獲取,然後存到本地localStorage中模擬使用者,之後再登入就直接從快取裡面取 */ function getRandomUser() { var userKey = "SIGNALR_USER"; var user = local.get(userKey); if (user) { return JSON.parse(user);} var userids = []; var usernames = ["痴玉", "書筠", "詩冬", "飛楓", "盼玉", "靖菡", "宛雁", "之卉", "凡晴", "書楓", "沛夢"]; var userphotos = []; //新增id,使用者頭像陣列 for (var i = 0; i < 9; i++) { userids.push(10000 + i); userphotos.push("/photos/00" + i.toString() + ".jpg"); } //取一個random值,自動生成當前使用者 var random = Math.random().toString().substr(3, 1); if (random > 8) { random = 8; } var user = { name: usernames[random], photo: userphotos[random], id:userids[random] }; local.set(userKey, JSON.stringify(user)); return user; } /*本地儲存*/ var local = { get: function (key) { return localStorage.getItem(key); }, set: function (key, value) { localStorage.setItem(key, value); } }
當然裡面有好多需要注意的細節沒有給大家講,具體的可以看詳細程式碼,思路基本已經出來了。我在重複一遍吧:第一,點選使用者,連線伺服器,當前使用者加入對應的組。第二,傳送訊息,呼叫server端的方法,將訊息傳送出去後,在推送到組裡面去,第三,客戶端接收到訊息之後,加到html頁面上,就這麼簡單。還有一個細節,注意訊息在左邊還是右邊。
演示一下吧:模擬第一個使用者登入。(谷歌瀏覽器)
好,很好聽的名字:飛楓,id為10003,下面第二個使用者登入,(QQ瀏覽器)
id為10001,名字為 書筠。那麼我們先用第一個使用者點選 書筠 頭像 開啟聊天視窗,然後在用第二個使用者點選 飛楓頭像開啟聊天視窗(由於沒有做歷史記錄,所以離線留言功能暫時不支援,只支援線上)
開啟之後,唉,單身的我只能模擬兩個人聊天玩了。。
到此為止呢,1v1聊天就到一段落,僅支援。。。文字,還不知道輸入script有沒有處理。。另外還有一個bug,就是視窗多開的話,應該 資訊可能會亂,不是因為 傳送亂了,而是,裡面有可能有重複的ID導致資訊賦html錯誤,我沒測,但是我猜測是這樣的。非常感謝“賢心”大神的web前端通訊框架。本篇到此結束,喜歡的同學點個贊吧。多多轉發哦。下篇預告:最終章-修改1v1聊天bug,新增圖片表情,附件傳送功能。群聊功能實現。
GitHub地址:
https://github.com/fanpan26/LayIM/tree/master