在《開源企業即時通信和在線客服》中已介紹了Lesktop的桌面模式和Web模式,可是沒有移動端。評論中 dotnetcms.org工做室 提到了LayIM,看了一下官網的演示和文檔,若是用這套LayIM的移動端Lestop也能夠輕鬆開發出移動端web版本。本文將說明如何接入LayIM移動端UI,同時對一些Lesktop的接口進行說明,做爲接入其餘前端UI的指引。 html
移動端功能展現:前端
源代碼下載:https://files.cnblogs.com/files/lucc/IM3.1.zip git
源代碼Git: https://github.com/luchuncheng/Lesktop.gitgithub
移動端:http://im.luchuncheng.com/mobile.aspxweb
註冊用戶&內部人員json
Web版:http://im.luchuncheng.com
數組
PC版下載:http://client.luchuncheng.com瀏覽器
客服平臺(訪客端)緩存
Web版:http://service.luchuncheng.com服務器
PC版:http://im.luchuncheng.com/client.ashx?chatwith=4&embedcode=1&createaccount=true
(embedcode=1表示顯示ID=1的客服嵌入代碼指定的客服人員,chatwith=4表示啓動和ID=4的客服人員對話窗口)
接入的第一個步驟就是登陸,登陸界面很是簡單,就是兩個文本框和一個登陸按鈕,服務單隻須要調用ServerImpl.Instance.Login便可:
int userid = AccountImpl.Instance.GetUserID(user); // 僅驗證不啓動回話,重定向到default.aspx再啓動回話 ServerImpl.Instance.Login("", Context, userid, false, null, false, 2);
最後第二個參數startSession=false,表示只是設置cookie不啓動會話,移動端的login.aspx僅僅只是驗證,登陸後重定向到default.aspx再啓動會話
最後一個參數device=2表示登陸設備爲移動端web版
LayIM初始化時須要好友,羣組和分組等信息,所以登陸完成後須要提供這些數據。在此以前先了解一下Lesktop的經常使用聯繫人功能:
如上圖所示,Lesktop容許用戶本身建立經常使用聯繫人分組,支持無限層級,用戶能夠將好友或內部人員添加到自建的任何層級的分組中。因爲LayIM不支持多層次分組,因此在移動端中將全部經常使用聯繫人不分層級顯示,以下圖所示
除了經常使用聯繫人,還須要一個「個人好友」分組,用於顯示已加本身爲好友的註冊用戶。接下來須要瞭解幾個和分組,好友和羣組相關的接口:
(1) ServerImpl.Instance.CommonStorageImpl.GetCategories
GetCategories用於獲取用戶建立的全部分組,返回值是一個DataRowCollection,每一行包括5個列:
ID | 類別ID |
UserID | 添加到該列表的聯繫人ID |
Name | 分組名稱 |
ParentID | 父級分組ID(移動端用不上) |
Type | 分組類別(1:聯繫人,2:羣組,3:部門,移動端只用到聯繫人的) |
(2) ServerImpl.Instance.CommonStorageImpl.GetCategoryItems
GetCategoryItems獲取和分組相關的全部聯繫人,羣組和部門ID(移動端只用到聯繫人),返回值爲DataRowCollection,每一行包括4個列:
UserID | 建立該分類的用戶ID |
CategoryID | 類別ID |
ItemID | 聯繫人,羣組或部門ID(移動端只用到聯繫人) |
CategoryType | 分組類別(1:聯繫人,2:羣組,3:部門,移動端只用到聯繫人的) |
(3) Category_CH.GetAccountInfos
GetCategoryItems只能獲取和分組相關的全部聯繫人的ID,還須要調用GetAccountInfos才能獲取到聯繫人的所有詳細信息,返回值爲AccountInfo數組,AccountInfo屬性以下:
ID | 用戶ID |
Name | 登陸名 |
Nickname | 暱稱 |
Type | 類型,0-聯繫人,1-羣組 |
SubType | 子類型:0-註冊用戶,1-管理員建立的內部人員 |
IsTemp | 是否爲臨時用戶,即訪客 |
IsDeleted | 是否已被刪除 |
HeadIMG | 頭像 |
(4) AccountImpl.Instance.GetVisibleUsersDetails
GetVisibleUsersDetails用於獲取全部和指定用戶相關的聯繫人和羣組,包括全部由管理員建立的內部人員,已加本身爲好友的註冊用戶,本身建立和加入的全部羣組和本身建立或被拉進去的多人會話,這部分數據主要是爲LayIM提供「個人好友」分組和羣聊,返回值爲一個Hashtable,每一個項的值爲AccountInfo。
(5)ServerImpl.Instance.GetCurrentUser
GetCurrentUser用戶獲取當前用戶詳細信息(AccountInfo)
以上5個接口已經獲取到了全部LayIM初始化須要的數據,打包成json,「賦值」給頁面的MobileInitParams全局變量便可:
namespace Core.Web { public class Mobile_Default : System.Web.UI.Page { string init_params_ = "{}"; protected void Page_Load(object sender, EventArgs e) { AccountInfo current_user = ServerImpl.Instance.GetCurrentUser(Context); if(current_user != null) { String sessionId = Guid.NewGuid().ToString().ToUpper(); ServerImpl.Instance.Login(sessionId, Context, current_user.ID, false, DateTime.Now.AddDays(7), true, 2); DataRowCollection categories = ServerImpl.Instance.CommonStorageImpl.GetCategories(current_user.ID); DataRowCollection items = ServerImpl.Instance.CommonStorageImpl.GetCategoryItems(current_user.ID); Hashtable users = Category_CH.GetAccountInfos(items); AccountInfo[] visible_users = AccountImpl.Instance.GetVisibleUsersDetails(current_user.Name); init_params_ = Utility.RenderHashJson( "Result", true, "IsLogin", true, "UserInfo", current_user.DetailsJson, "SessionID", sessionId, "CompanyInfo", ServerImpl.Instance.CommonStorageImpl.GetCompanyInfo(), "Categories", categories, "CategorieItems", items, "CategorieUsers", users, "VisibleUsers", visible_users ); } else { Response.Redirect("login.aspx"); } } public string InitParams { get { return init_params_; } } } }
頁面加載後,LayIM_Init裏面就能夠經過MobileInitParams獲取到這些數據,LayIM初始化參數請看官網文檔,如下函數用於將Lesktop的數據轉換成LayIM須要的格式:
// 獲取分組和聯繫人 function GetFriends() { var friends = []; for (var i = 0; i < window.MobileInitParams.Categories.length; i++) { var category = window.MobileInitParams.Categories[i]; if (category.Type == 1) { // Type=1爲經常使用聯繫人類別,將全部經常使用聯繫人類別(不分層次)顯示爲LayIM的分組 var groupid = category.ID + 10000; var group = { "groupname": category.Name, "id": groupid.toString(), "online": 0, "list": [] }; var user_count = 0; var online_count = 0; for (var j = 0; j < window.MobileInitParams.CategorieItems.length; j++) { // 從CategorieItems中獲取該分組全部聯繫人ID var item = window.MobileInitParams.CategorieItems[j]; if (item.CategoryID == category.ID) { // 經過聯繫人ID從CategorieUsers中獲取聯繫人詳細信息 var friend_info = window.MobileInitParams.CategorieUsers[item.ItemID.toString()]; if(friend_info != undefined) { group.list.push({ "username": friend_info.Nickname, "id": friend_info.ID.toString(), "avatar": Core.CreateHeadImgUrl(friend_info.ID, 150, false, friend_info.HeadIMG), "sign": "" }); user_count++; if (friend_info.State == "Online") { online_count++; } } } } if (user_count > 0) { friends.push(group); } } } var grou_myfriend = { "groupname": "個人好友", "id": LayIMGroup_MyFriend, "online": 0, "list": [] } var current_user = window.MobileInitParams.UserInfo; // 獲取全部好友並顯示到好友分組 for (var i = 0; i < window.MobileInitParams.VisibleUsers.length; i++) { var user = window.MobileInitParams.VisibleUsers[i]; if (user.Type == 0 && ((current_user.SubType == 1 && user.SubType == 0) || current_user.SubType == 0)) { // 內部人員(SubType=1)顯示註冊用戶並添加本身爲好友的,不包括其餘內部人員 // 註冊用戶(SubType=0)顯示添加本身爲好友的其餘註冊用戶和內部用戶 grou_myfriend.list.push({ "username": user.Nickname, "id": user.ID.toString(), "avatar": Core.CreateHeadImgUrl(user.ID, 150, false, user.HeadIMG), "sign": "" }); } } friends.push(grou_myfriend); friends.push({ "groupname": "其餘聯繫人", "id": LayIMGroup_Other, "online": 0, "list": [] }); return friends; }
// 獲取羣聊 function GetGroups() { // 獲取全部羣組和多人會話 var groups = []; for (var i = 0; i < window.MobileInitParams.VisibleUsers.length; i++) { var user = window.MobileInitParams.VisibleUsers[i]; if(user.Type == 1) { groups.push({ "groupname": user.Nickname, "id": user.ID.toString(), "avatar": Core.CreateGroupImgUrl(user.HeadIMG, user.IsTemp) }); } } return groups; }
這次爲了接入LayIM,加了一個全局委託Core.OnNewMessage,每當收到新消息是會調用該委託,若是須要監聽新消息,只須要附加一個處理函數便可
function LayIM_OnNewMessage(msg) { } // 監聽新消息 Core.OnNewMessage.Attach(LayIM_OnNewMessage);
因爲收到的消息多是web或pc端發送的,包含LayIM消息面板不支持的富文本,所以須要先處理掉全部HTML tag,此外還須要處理文件標誌([FILE:...])生成下載連接,完整代碼以下:
function LayIM_ParseMsg(text) { var newText = text; try { // 處理掉HTML開始TAG newText = text.toString().replace( /<([a-zA-Z0-9]+)([\s]+)[^<>]*>/ig, function (html, tag) { if (tag.toLowerCase() == "img") { var filename = Core.GetFileNameFromImgTag(html); if (filename != "") { // Lesktop服務器上的文件,從新加上分辨率限制參數,改成下載縮略圖,連接到原圖 var url = Core.CreateDownloadUrl(filename); return String.format("a({0})[img[{0}&MaxWidth=450&MaxHeight=800]]", url); } else { // 外源圖片,改爲超連接,防止下載圖片浪費流量 var src = Core.GetSrcFromImgTag(html); return String.format("a({0})[{1}]", src, " 圖片 "); } } return ""; } ) .replace( /\x5BFILE:([^\x5B\x5D]+)\x5D/ig, function (filetag, filepath) { // 提取文件消息,改成視頻,音頻或文件 var path = unescape(filepath) var ext = Core.Path.GetFileExtension(path).toLowerCase(); if (ext == ".mp4" || ext == ".mov") { return String.format("video[{0}]", Core.CreateDownloadUrl(path), Core.Path.GetFileName(path)); } else if (ext == "mp3") { return String.format("audio[{0}]", Core.CreateDownloadUrl(path), Core.Path.GetFileName(path)); } else { return String.format("file({0})[{1}]", Core.CreateDownloadUrl(path), Core.Path.GetFileName(path)); } } ) .replace( /<([a-zA-Z0-9]+)[\x2F]{0,1}>/ig, function (html, tag) { // 清理<br/>等 return ""; } ) .replace( /<\/([a-zA-Z0-9]+)>/ig, function (html, tag) { // 清理HTML結束TAG return ""; } ); } catch(ex) { newText += " ERROR:"; newText += ex.message; } return newText; } function LayIM_OnNewMessage(msg) { // msg.Sender, msg.Receiver只包括最基本的ID,Name,需從新獲取詳細信息 var sender_info = Core.AccountData.GetAccountInfo(msg.Sender.ID); if (sender_info == null) sender_info = msg.Sender; var receiver_info = Core.AccountData.GetAccountInfo(msg.Receiver.ID); if (receiver_info == null) receiver_info = msg.Receiver; if (msg.Receiver.Type == 0) { // 私聊消息 if (!LayIM_UserExists(sender_info.ID.toString())) { // 分組列表中不包括消息發送者,將發送者加入到其餘聯繫人分組 layim.addList({ type: 'friend', "username": sender_info.Nickname, "id": sender_info.ID.toString(), "groupid": LayIMGroup_Other, "avatar": Core.CreateHeadImgUrl(sender_info.ID, 150, false, sender_info.HeadIMG), "sign": "" }); } // 顯示到LayIM消息面板 layim.getMessage({ username: sender_info.Nickname, avatar: Core.CreateHeadImgUrl(msg.Sender.ID, 150, false, sender_info.HeadIMG), id: msg.Sender.ID.toString(), type: "friend", cid: msg.ID.toString(), content: LayIM_ParseMsg(msg.Content) }); } else if (msg.Receiver.Type == 1) { // 羣消息 if (!LayIM_GroupExists(receiver_info.ID.toString())) { // 羣聊列表中不包括該羣,加入到羣聊中 layim.addList({ "type": "group", "groupname": receiver_info.Nickname, "id": receiver_info.ID.toString(), "avatar": Core.CreateGroupImgUrl(receiver_info.HeadIMG, receiver_info.IsTemp) }); } // 顯示到LayIM消息面板 layim.getMessage({ username: sender_info.Nickname, avatar: Core.CreateHeadImgUrl(msg.Sender.ID, 150, false, sender_info.HeadIMG), id: msg.Receiver.ID.toString(), type: "group", cid: msg.ID.toString(), content: LayIM_ParseMsg(msg.Content) }); } } // 監聽新消息 Core.OnNewMessage.Attach(LayIM_OnNewMessage);
發送消息只須要調用服務端的WebIM.NewMessage方法便可,發送前,須要對消息進行預處理,把LayIM的標誌(圖片,文件和表情)轉換成HTML,還須要調用Core.TranslateMessage,該函數用於將消息中的圖片(<img ...>),文件([FILE:...])轉換成服務端能夠處理的附件,具體代碼以下:
function LayIM_SendMsg_GetFileName(fileurl) { var filename_regex = /FileName\=([^\s\x28\x29\x26]+)/ig; filename_regex.lastIndex = 0 var ret = filename_regex.exec(fileurl); if (ret == null || ret.length <= 1) { return ""; } return ret[1]; } function LayIM_SendMsg(data) { var msgdata = { Action: "NewMessage", Sender: parseInt(data.mine.id, 10), Receiver: parseInt(data.to.id, 10), DelTmpFile: 0, Content: "" }; var content = data.mine.content; // 轉換圖片消息 content = content.replace( /img\x5B([^\x5B\x5D]+)\x5D/ig, function(imgtext, src) { var filename = LayIM_SendMsg_GetFileName(src); return String.format('<img src="{0}">', Core.CreateDownloadUrl(filename)); } ); // 轉換文件消息 content = content.replace( /file\x28([^\x28\x29]+)\x29\x5B([^\x5B\x5D]+)\x5D/ig, function (filetext, fileurl, ope) { var path = unescape(LayIM_SendMsg_GetFileName(fileurl)); return Core.CreateFileHtml([path]); } ); // 將消息中的圖片(<img ...>),文件([FILE:...])轉換成服務端能夠處理的附件 content = Core.TranslateMessage(content, msgdata); // 轉換表情 content = content.replace( /face\[([^\s\[\]]+?)\]/g, function (face, face_type) { var face_file = LayIM_FaceToFile[face_type]; if(face_file != undefined) { return String.format('<img src="{0}/{1}">', Core.GetUrl("layim/images/face"), face_file); } } ); msgdata.Content = content; Core.SendCommand( function (ret) { var message = ret; }, function (ex) { var errmsg = String.format("因爲網絡緣由,消息\"{0}\"發送失敗,錯誤信息:{1}", text, ex.Message); }, Core.Utility.RenderJson(msgdata), "Core.Web WebIM", false ); }
Lesktop有如下幾種異常狀態:
(1) 在其餘瀏覽器或客戶端登陸,此時會收到強制下線通知(GLOBAL:OFFLINE)
(2) 服務端已升級,爲簡化服務端開發,Lesktop服務端要求客戶端和前端都用對應的最新版本,不兼容舊版本。服務端網站升級後,升級前未退出從新鏈接上的客戶端和web端都會收到不兼容異常通知(IncompatibleException)。PC須要重啓升級,WEB端須要重登錄(發佈版全部靜態資源都放在名稱爲版本號的文件夾中,重登錄不會讀取到緩存的資源)
(3) 驗證身份異常,服務端網站可能會由於某種緣由從新啓動,此時會從新生成cookie加密密鑰,會致使已在線的客戶端沒法從cookie獲取身份信息,此時客戶端會收到驗證異常通知(UnauthorizedException)
移動端處理異常方法很簡單,收到異常通知後,馬上重定向到offline.aspx頁面,顯示異常消息和從新登陸按鈕,以下圖所示:
至此,接入LayIM的工做就基本完成,前端代碼基本都在mobile.js中。
由於LayIM不是開源的,所以git上不包括LayIM的源代碼,須要自行購買,而後將src下的全部文件放到CurrentVersion/layim下: