看起來挺簡單,細節仍是不少的,好,接上一篇,咱們已經成功鏈接singalR服務器了,那麼剩下的內容呢,就是一步一步實現聊天功能。html
咱們先看看缺什麼東西前端
在作上述工做以前,仍是要作許多準備工做的。咱們分析一下界面元素node
好的,能夠看到,一個消息裏面有消息發送時間(addtime),用戶名(username),用戶頭像(userphoto),用戶消息體(msgcontent),除此以外還須要用戶id,聊天id,以及組名(groupname).以此我先在後臺創建模型。git
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,生成組名代碼以下:(主要保證兩個用戶的組名惟一性就可,方法隨意)github
/// <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 類中來。添加用戶加入組的方法,這個方法何時調用呢,就是當你點擊某個用戶頭像彈出聊天框的時候調用。web
/// <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地址: