最近研究了一下WebIM,现将学习笔记记录于此。

一、WebIM采用技术

本篇实现的WebIM是对现有技术的整合,它包含了如下技术:
seajs:用于JavaScript模块化编程,seajs简介及用途可以看这儿:http://blog.csdn.net/fengshuiyue/article/details/51177458
layim:阿里大牛贤心制作的一款webim聊天界面,很美观,源码下载地为http://sentsin.com/layui/layim/
JsJac:基于jabber/xmpp的javascript实现,在此完成与后台servlet(JabberHttpBind)的数据交互。下载地址为https://github.com/sstrigler/JSJaC/
2)后台:
JabberHTTPBind:用于和Openfire进行通信,下载地址为http://stefan-strigler.de/jhb/
3)服务器:
openfire:开源的聊天服务器

二、通信技术简介

1.xmpp

1)webIM采用的是标准通信协议XMPP(Extensible Messageing and Presence Protocol:可扩展消息与存在协议),它目前是主流的四种IM(IM:instant messaging,即时消息)协议之一,其他三种分别为:即时信息和空间协议(IMPP)、空间和即时信息协议(PRIM)、针对即时通讯和空间平衡扩充的进程开始协议SIP(SIMPLE)。
2)xmpp是采用TCP进行传输的xml流,这不同于QQ的方式(QQ是用二进制流进行传输的)、不同于MSN的方式(MSN采用的是纯文本指令加空格加参数加换行苻的方式),下图是传输XMPP经过的协议栈:

3)xmpp地址格式:
一个实体在xmpp网络结构中被称为一个接点,它有唯一的标示符jabber identifier(JID),即实体地址,用来表示一个Jabber用户,但是也可以表示其他内容,例如一个聊天室。一个有效的JID包括下列元素:

域名(domain identifier);
节点(node identifier);
源(resource identifier);

JID完整格式: node@domain/resource
4)xmpp格式
xmpp中定义了3个顶层XML元素:

Message:用于在两个jabber用户之间发送信息
Presence:用来表明用户的状态
IQ:一种请求/响应机制,从一个实体从发送请求,另外一个实体接受请求,并进行响应.例如,client在stream的上下文中插入一个元素,向Server请求得到自己的好友列表,Server返回一个里面是请求的结果.

① Message实例

发送端发送的信息:

<?xml version="1.0" encoding="utf-8"?><body xmlns="http://jabber.org/protocol/httpbind" rid="424681" sid="Okig9EHLCTA15a-BZ3NRDqOW" key="08ca4f4beeaa588cd170d700fa78a01c8d469df1"><message xmlns="jabber:client"to="webchattest@openfire402/webim" type="chat"><body>您好,webchattest</body></message>
</body>

接收端接收到的信息:

<!--webchattest接收到的admin的离线信息 -->
<?xml version="1.0" encoding="utf-8"?><body xmlns="http://jabber.org/protocol/httpbind"><message xmlns="jabber:client"from="admin@openfire402/webim" to="webchattest@openfire402/webim" type="chat"><body>您好,webchattest</body><delay xmlns="urn:xmpp:delay" from="openfire402" stamp="2016-06-11T04:52:23.153Z"></delay></message>
</body>
<!--webchattest在线时接收到的admin发送的信息 -->
<?xml version="1.0" encoding="utf-8"?><body xmlns="http://jabber.org/protocol/httpbind"><message xmlns="jabber:client" from="admin@openfire402/webim" to="webchattest@openfire402/webim" type="chat"><body>webchattest上线后admin发的信息</body></message>
</body>

message属性介绍:

from属性:设置消息发送方自身的FullJID(node@domain/resource)
to属性:设置消息接收方的Bare JID(node@domain),通常第一次发送方无法确知接收方的Full JID,通过服务器中转路由时由服务器根据Base JID映射接收方的Full JID;但如果这个消息是在回复之前接收到的消息,则to属性应该包含对方完整的Full JID;如此设计的好处在于:当to属性设定为Full JID时可以帮助服务器省却了接收者资源定位(接入定位),在一个IM服务集群环境中这种定位通常意味着一次分布式缓存读取操作。
type属性:XMPP约定了type的枚举值,包括:

  • chat:表明在一个点对点会话环境中的聊天消息。
  • groupchat:表明在一个多人会话环境中的聊天消息。
  • headline: 通常一些系统通知、警告、实时数据更新采用此类型,这类消息不期待客户端回复或响应,具有很高的实时性,不需要离线存储。
  • normal: 默认的消息类型(缺乏type属性时),通常表达一种要求接收方必须确认的消息,一般用于系统提示强制用户确认或取消等。
  • error: 表示一个错误消息,可能由服务端发送给客户端,也可能是另一个客户接收端回应给客户发送端,此类消息也不需要离线存储。

message子元素:

<subject>表明一个消息主题,通常客户端实现显示在聊天窗口标题栏处
<body>消息内容部分
<subject>和<subject>都允许包含多个元素标签,不同的标签根据xml:lang表达了不同的语言(XMPP可是一个国际化协议)

下面这是完整的message消息格式:

<message
    from='node@domain/resource'to='node@domain'type='chat'xml:lang='en'><subject>hello!</subject><subject xml:lang='zh'>你好</subject><body>welcome to meet you</body><body xml:lang='zh'>很高兴认识你</body>
</message>

② Presence
用来表明用户的状态,如:online、away、dnd(请勿打扰)等。当用户离线或改变自己的状态时,就会在stream的上下文中插入一个Presence元素,来表明自身的状态.结构如下所示:

  <presence>from='node@domain/resource'to='node@domain'<status>online</status>
</presence>

presence 元素可以取下面几种值:

  • probe :用于向接受消息方发送特殊的请求
  • subscribe:当接受方状态改变时,自动向发送方发送presence信息。

③ IQ

一种请求/响应机制,从一个实体从发送请求,另外一个实体接受请求,并进行响应.例如,client在stream的上下文中插入一个元素,向Server请求得到自己的好友列表,Server返回一个里面是请求的结果。结构如下所示:

<iq xmlns="jabber:client" from="openfire402" id="537-29" to="webchattest@openfire402/webim" type="get"><ping xmlns="urn:xmpp:ping"></ping>
</iq>

IQ 主要的属性是type。包括:

  • get:获取当前域值;
  • set:设置或替换get查询的值;
  • result:说明成功的响应了先前的查询;
  • error:查询和响应中出现的错误;

2.openfire

本文采用的是openfire4.0.2版本的,openfire的安装配置简单记录如下:
1)在官网http://www.igniterealtime.org/downloads/index.jsp#openfire 下载openfire,安装即可
2)在“openfire安装目录/bin”目录下运行openfired.exe程序,运行效果如下图:

3)第一次登陆http://localhost:9090/ 会进入openfire配置页面,基本上使用默认值即可,下面介绍需要注意的几个地方
a.服务器设置
需要注意的是 的设置,这块儿会关系到 JID(node@domain/resource) domain的取值

b.数据库设置
如果选择 标准数据库连接 ,则需要导入openfire的sql文件,该文件存储在:openfire安装目录\resources\database 目录下,如下图:

三、webIM实现

由于使用的都是现成的技术,故本篇的webIM重在前端的编程

1.封装JsJac通信类

该类采用了该博主(http://www.cnblogs.com/hoojo)js端 通信类,该类定义如下:

define(function(require, exports,module){require('../base_module/jsjac/jsjac.js');require('../base_module/jquery-1.11.1.min.js');var util = require('../util/util.js');var chatConnConfig = {httpbase: '',//window.contextPath + "/JHB/", //请求后台http-bind服务器urldomain: '', //window["serverDomin"], //"192.168.5.231", // 192.168.5.231 当前有效域名username: "",pass: "",timerval: 2000, // 设置请求超时resource: "webim", // 链接资源标识register: false // 是否注册},chatObj = null, //聊天实体//writeReceiveMessage = null;chatConnection = {},onlineStatus = {available: '在线',chat: '欢迎聊天',away: '离开',xa: '不可用',dnd: '请勿打扰',invisible: '隐身',unavailable: '离线'};chatConnection.connected = function(){}/*** chatConnCfg = {*      httpbase: '',//window.contextPath + "/JHB/", //请求后台http-bind服务器urldomain: '', //window["serverDomin"], //"192.168.5.231", // 192.168.5.231 当前有效域名timerval: 2000, // 设置请求超时resource: ''}*/init = function(chatConnCfg,chatobjparam){chatObj = chatobjparam;//chatConnConfig.oDbg = new JSJaCConsoleLogger(4);if(chatConnCfg){$.extend(chatConnConfig, chatConnConfig, chatConnCfg);//chatConnConfig.httpbase = chatConnCfg.httpbase;//chatConnConfig.domain = chatConnCfg.domain;//chatConnConfig.resource = chatConnCfg.resource;//chatConnConfig.timerval = chatConnCfg.timerval;//chatConnConfig.writeReceiveMessage = chatConnCfg.writeReceiveMessage;}// Debugger pluginif (typeof (Debugger) == "function") {chatConnConfig.oDbg = new Debugger(2, chatConnConfig.resource);chatConnConfig.oDbg.start();} else {chatConnConfig.oDbg = function () {};chatConnConfig.oDbg.log = function () {};}try {if (JSJaCCookie.read("btype").getValue() == "binding") {chatConnection = new JSJaCHttpBindingConnection({ "oDbg": chatConnConfig.oDbg});//rdbgerjac.chat.setupEvent(remote.connection);setupEvent(chatConnection)if (chatConnection.resume()) {}} } catch (e) {} // reading cookie failed - never mind}/*** loginPerson为对象* loginPerson = {username:'',password:'',register:false  //是否注册改用户}*/login = function (loginPerson) {try {// 链接参数var connectionConfig = chatConnConfig;// Debugger consoleif (typeof (oDbg) != "undefined") {connectionConfig.oDbg = oDbg;}chatConnection = new JSJaCHttpBindingConnection(connectionConfig);// 安装(注册)Connection事件模型setupEvent(chatConnection);// setup args for connect methodif (loginPerson) {connectionConfig.username = loginPerson.username;connectionConfig.pass = loginPerson.password;connectionConfig.register = loginPerson.register;}// 连接服务器chatConnection.connect(connectionConfig);changeStatus("away", "away", 1, "chat");} catch (e) {//登录错误信息显示alert(e.toString());} finally {return false;}},setupEvent = function (con) {con.registerHandler('message', handleMessage);con.registerHandler('presence', handlePresence);con.registerHandler('iq', handleIQ);con.registerHandler('onconnect', handleConnected);con.registerHandler('onerror', handleError);con.registerHandler('status_changed', handleStatusChanged);con.registerHandler('ondisconnect', handleDisconnected);con.registerIQGet('query', NS_VERSION, handleIqVersion);con.registerIQGet('query', NS_TIME, handleIqTime);},// 改变用户状态changeStatus = function (type, status, priority, show) {type = type || "unavailable";status = status || "online";priority = priority || "1";show = show || "chat";var presence = new JSJaCPresence();presence.setType(type); // unavailable invisiblepresence.setStatus(status); // onlinepresence.setPriority(priority); // 1presence.setShow(show); // chatif (chatConnection) {chatConnection.send(presence);}},// 发送远程消息sendMessage = function (msg, to) {try {if (msg == "") {return false;}var tosendstr = "";if (to) {tosendstr= to + '@' + chatConnConfig.domain + '/' +chatConnConfig.resource;} else {// 向chat接收信息区域写消息if (chatObj.writeReceiveMessage) {
//                    var html = "你没有指定发送者的名称";
//                    var msgobj = {//                          receiveType:'noSendUser',
//                          objtime:'',
//                          objname:'',
//                          objface:'',
//                          content:html
//                    };
//                    chatObj.writeReceiveMessage(msgobj);}return false;}// 构建jsjac的message对象var message = new JSJaCMessage();message.setTo(new JSJaCJID(tosendstr));message.setType("chat"); // 单独聊天,默认为广播模式message.setBody(msg);// 发送消息chatConnection.send(message);return false;} catch (e) {//var html = "<div class='msg error''>Error: " + e.message + "</div>";alert(e.message);return false;}},// 退出、断开链接logout = function () {var presence = new JSJaCPresence();presence.setType("unavailable");if (chatConnection) {chatConnection.send(presence);chatConnection.disconnect();}},errorHandler = function (event) {var e = event || window.event;//alert(e);显示错误信息if (chatConnection && chatConnection.connected()) {chatConnection.disconnect();}return false;},unloadHandler = function () {var con = chatConnection;if (typeof con != "undefined" && con && con.connected()) {// save backend typeif (con._hold) { // must be binding(new JSJaCCookie("btype", "binding")).write();} if (con.suspend) {con.suspend();}}},// 重新连接服务器reconnection = function () {chatConnConfig.register = false;if (chatConnection.connected()) {chatConnection.disconnect();}login();}/* ########################### Handler Event ############################# */   handleIQ =  function (aIQ) {chatConnection.send(aIQ.errorReply(ERR_FEATURE_NOT_IMPLEMENTED));var content = aIQ.xml().htmlEnc();var nowstr = util.getDatetime();var msgobj = {receiveType:'handleIQ',objtime:nowstr,objname:'',objface:'',content:content};chatObj.writeReceiveMessage(msgobj);},handleMessage = function (aJSJaCPacket) {var user = aJSJaCPacket.getFromJID().toString();var userName = user.split("@")[0];var content = aJSJaCPacket.getBody();var nowstr = util.getDatetime();var msgobj = {receiveType:'handleMessage',objtime:nowstr,objname:userName,objface:'',content:content};if(content == undefined || content == null || content == ''){}else{chatObj.writeReceiveMessage(msgobj);}},handlePresence = function (aJSJaCPacket) {var user = aJSJaCPacket.getFromJID();var userName = user.toString().split("@")[0];var status = '';var html = "<div class=\"msg\">";if (!aJSJaCPacket.getType() && !aJSJaCPacket.getShow()) {html += "<b>" + userName + " 上线了.</b>";} else {html += "<b>" + userName + " 设置 presence 为: ";if (aJSJaCPacket.getType()) {html += aJSJaCPacket.getType() + ".</b>";} else {html += aJSJaCPacket.getShow() + ".</b>";}if (aJSJaCPacket.getStatus()) {html += " (" + aJSJaCPacket.getStatus().htmlEnc() + ")";}}html += "</div>";// 向chat接收信息区域写消息var nowstr = util.getDatetime();var msgobj = {receiveType:'handlePresence',objtime:nowstr,objname:userName,objface:'',content:html};chatObj.writeReceiveMessage(msgobj);},handleError = function (event) {var e = event || window.event;var html = "An error occured:<br />" + ("Code: " + e.getAttribute("code") + "\nType: " + e.getAttribute("type") + "\nCondition: " + e.firstChild.nodeName).htmlEnc();var content = "";switch (e.getAttribute("code")) {case "401":content = "登陆验证失败!";break;// 当注册发现重复,表明该用户已经注册,那么直接进行登陆操作            case "409"://content = "注册失败!\n\n请换一个用户名!";chatConnection.reconnection();break;case "503":content = "无法连接到IM服务器,请检查相关配置!";break;case "500":var content = "服务器内部错误!\n\n连接断开!<br/><a href='javascript: self.parent.remote.jsjac.chat.reconnection();'>重新连接</a>";// 向chat接收信息区域写消息break;default:break;}if (content) {var nowstr = util.getDatetime();var msgobj = {receiveType:'handleError',objtime:nowstr,objname:chatConnConfig.username,objface:'',content:content};chatObj.writeReceiveMessage(msgobj);}if (chatConnection.connected()) {chatConnection.disconnect();}},// 状态变化触发事件handleStatusChanged = function (status) {//remote.console.info("<div>当前用户状态: " + status + "</div>");//remote.dbger.log("当前用户状态: " + status);if (status == "disconnecting") {var html = "<b style='color:red;'>你离线了!</b>";// 向chat接收信息区域写消息var nowstr = util.getDatetime();var msgobj = {receiveType:'handleStatusChanged',objtime:nowstr,objname:chatConnConfig.username,objface:'',content:html};chatObj.writeReceiveMessage(msgobj);}},// 建立链接触发事件方法handleConnected = function () {chatConnection.send(new JSJaCPresence());},// 断开链接触发事件方法handleDisconnected = function () {},handleIqVersion = function (iq) {chatConnection.send(iq.reply([iq.buildNode("name", chatConnConfig.resource), iq.buildNode("version", JSJaC.Version), iq.buildNode("os", navigator.userAgent)]));return true;},handleIqTime = function (iq) {var now = new Date();chatConnection.send(iq.reply([iq.buildNode("display", now.toLocaleString()), iq.buildNode("utc", now.jabberDate()), iq.buildNode("tz", now.toLocaleString().substring(now.toLocaleString().lastIndexOf(" ") + 1))]));return true;}//exports.writeReceiveMessage = writeReceiveMessage;exports.init = init;exports.login = login;exports.sendMessage = sendMessage;exports.changeStatus = changeStatus;exports.logout = logout;exports.chatObj = chatObj;$(window).bind({unload: unloadHandler,error: errorHandler,beforeunload: logout});
});

2.调用layim.js实现聊天界面类

define(function(require, exports,module){//require('../base_module/lay/lib.js');require('../base_module/jquery-1.11.1.min.js');require('../base_module/lay/layer/layer.min.js');serverChat = require('./serverchat.js');var util = require('../util/util.js');var config = {msgurl: '私信地址',chatlogurl: '聊天记录url前缀',aniTime: 200,right: -232,api: {friend: 'js/layim/friend.json', //好友列表接口group: 'js/layim/group.json', //群组列表接口 chatlog: 'js/layim/chatlog.json', //聊天记录接口groups: 'js/layim/groups.json', //群组成员接口sendurl: '' //发送消息接口},user: { //当前用户信息name: '测试用户',face: 'http://tp3.sinaimg.cn/3850354634/180/40048989275/0'},//自动回复内置文案,也可动态读取数据库配置autoReplay: ['您好,我现在有事不在,一会再和您联系。', '你没发错吧?','洗澡中,请勿打扰,偷窥请购票,个体四十,团体八折,订票电话:一般人我不告诉他!','你好,我是主人的美女秘书,有什么事就跟我说吧,等他回来我会转告他的。','我正在拉磨,没法招呼您,因为我们家毛驴去动物保护协会把我告了,说我剥夺它休产假的权利。','<(@ ̄︶ ̄@)>','你要和我说话?你真的要和我说话?你确定自己想说吗?你一定非说不可吗?那你说吧,这是自动回复。','主人正在开机自检,键盘鼠标看好机会出去凉快去了,我是他的电冰箱,我打字比较慢,你慢慢说,别急……','(*^__^*) 嘻嘻,是贤心吗?'],chating: {},hosts: (function(){var dk = location.href.match(/\:\d+/);dk = dk ? dk[0] : '';return 'http://' + document.domain + dk + '/';})(),json: function(url, data, callback, error){return $.ajax({type: 'POST',url: url,data: data,dataType: 'json',success: callback,error: error});},stopMP: function(e){e ? e.stopPropagation() : e.cancelBubble = true;}}, dom = [$(window), $(document), $('html'), $('body')], xxim = {};/*** obj = {receiveType:  //handleIQ,handleMessage,handlePresence,handleError,handleStatusChangedcontent:}*///主界面tabxxim.tabs = function(index){var node = xxim.node;node.tabs.eq(index).addClass('xxim_tabnow').siblings().removeClass('xxim_tabnow');node.list.eq(index).show().siblings('.xxim_list').hide();if(node.list.eq(index).find('li').length === 0){xxim.getDates(index);}};//节点xxim.renode = function(){var node = xxim.node = {tabs: $('#xxim_tabs>span'),list: $('.xxim_list'),online: $('.xxim_online'),setonline: $('.xxim_setonline'),onlinetex: $('#xxim_onlinetex'),xximon: $('#xxim_on'),layimFooter: $('#xxim_bottom'),xximHide: $('#xxim_hide'),xximSearch: $('#xxim_searchkey'),searchMian: $('#xxim_searchmain'),closeSearch: $('#xxim_closesearch'),layimMin: $('#layim_min')}; };//主界面缩放xxim.expend = function(){var node = xxim.node;if(xxim.layimNode.attr('state') !== '1'){xxim.layimNode.stop().animate({right: config.right}, config.aniTime, function(){node.xximon.addClass('xxim_off');try{localStorage.layimState = 1;}catch(e){}xxim.layimNode.attr({state: 1});node.layimFooter.addClass('xxim_expend').stop().animate({marginLeft: config.right}, config.aniTime/2);node.xximHide.addClass('xxim_show');});} else {xxim.layimNode.stop().animate({right: 1}, config.aniTime, function(){node.xximon.removeClass('xxim_off');try{localStorage.layimState = 2;}catch(e){}xxim.layimNode.removeAttr('state');node.layimFooter.removeClass('xxim_expend');node.xximHide.removeClass('xxim_show');});node.layimFooter.stop().animate({marginLeft: 0}, config.aniTime);}};//初始化窗口格局xxim.layinit = function(){var node = xxim.node;//主界面try{if(!localStorage.layimState){       config.aniTime = 0;localStorage.layimState = 1;}if(localStorage.layimState === '1'){xxim.layimNode.attr({state: 1}).css({right: config.right});node.xximon.addClass('xxim_off');node.layimFooter.addClass('xxim_expend').css({marginLeft: config.right});node.xximHide.addClass('xxim_show');}}catch(e){layer.msg(e.message, 5, -1);}};//聊天窗口xxim.popchat = function(param){var node = xxim.node, log = {};log.success = function(layero){layer.setMove();xxim.chatbox = layero.find('#layim_chatbox');log.chatlist = xxim.chatbox.find('.layim_chatmore>ul');log.chatlist.html('<li data-id="'+ param.id +'" type="'+ param.type +'"  id="layim_user'+ param.type + param.id +'"><span>'+ param.name +'</span><em>×</em></li>')xxim.tabchat(param, xxim.chatbox);//最小化聊天窗xxim.chatbox.find('.layer_setmin').on('click', function(){var indexs = layero.attr('times');layero.hide();node.layimMin.text(xxim.nowchat.name).show();});//关闭窗口xxim.chatbox.find('.layim_close').on('click', function(){var indexs = layero.attr('times');layer.close(indexs);xxim.chatbox = null;config.chating = {};config.chatings = 0;});//关闭某个聊天log.chatlist.on('mouseenter', 'li', function(){$(this).find('em').show();}).on('mouseleave', 'li', function(){$(this).find('em').hide();});log.chatlist.on('click', 'li em', function(e){var parents = $(this).parent(), dataType = parents.attr('type');var dataId = parents.attr('data-id'), index = parents.index();var chatlist = log.chatlist.find('li'), indexs;config.stopMP(e);delete config.chating[dataType + dataId];config.chatings--;parents.remove();$('#layim_area'+ dataType + dataId).remove();if(dataType === 'group'){$('#layim_group'+ dataType + dataId).remove();}if(parents.hasClass('layim_chatnow')){if(index === config.chatings){indexs = index - 1;} else {indexs = index + 1;}xxim.tabchat(config.chating[chatlist.eq(indexs).attr('type') + chatlist.eq(indexs).attr('data-id')]);}if(log.chatlist.find('li').length === 1){log.chatlist.parent().hide();} });//聊天选项卡log.chatlist.on('click', 'li', function(){var othis = $(this), dataType = othis.attr('type'), dataId = othis.attr('data-id');xxim.tabchat(config.chating[dataType + dataId]);});//发送热键切换log.sendType = $('#layim_sendtype'), log.sendTypes = log.sendType.find('span');$('#layim_enter').on('click', function(e){config.stopMP(e);log.sendType.show();});log.sendTypes.on('click', function(){log.sendTypes.find('i').text('')$(this).find('i').text('√');});xxim.transmit();};log.html = '<div class="layim_chatbox" id="layim_chatbox">'+'<h6>'+'<span class="layim_move"></span>'+'    <a href="'+ param.url +'" class="layim_face" target="_blank"><img src="'+ param.face +'" ></a>'+'    <a href="'+ param.url +'" class="layim_names" target="_blank">'+ param.name +'</a>'+'    <span class="layim_rightbtn">'+'        <i class="layer_setmin"></i>'+'        <i class="layim_close"></i>'+'    </span>'+'</h6>'+'<div class="layim_chatmore" id="layim_chatmore">'+'    <ul class="layim_chatlist"></ul>'+'</div>'+'<div class="layim_groups" id="layim_groups"></div>'+'<div class="layim_chat">'+'    <div class="layim_chatarea" id="layim_chatarea">'+'        <ul class="layim_chatview layim_chatthis"  id="layim_area'+ param.type + param.id +'"></ul>'+'    </div>'+'    <div class="layim_tool">'+'        <i class="layim_addface" title="发送表情"></i>'+'        <a href="javascript:;"><i class="layim_addimage" title="上传图片"></i></a>'//+'        <a href="javascript:;"><i class="layim_addfile" title="上传附件"></i></a>'+'        <a href="" target="_blank" class="layim_seechatlog"><i></i>聊天记录</a>'+'    </div>'+'    <textarea class="layim_write" id="layim_write"></textarea>'+'    <div class="layim_send">'+'        <div class="layim_sendbtn" id="layim_sendbtn">发送<span class="layim_enter" id="layim_enter"><em class="layim_zero"></em></span></div>'+'        <div class="layim_sendtype" id="layim_sendtype">'+'            <span><i>√</i>按Enter键发送</span>'+'            <span><i></i>按Ctrl+Enter键发送</span>'+'        </div>'+'    </div>'+'</div>'+'</div>';if(config.chatings < 1){$.layer({type: 1,border: [0],title: false,shade: [0],area: ['620px', '493px'],move: ['.layim_chatbox .layim_move', true],moveType: 1,closeBtn: false,offset: [(($(window).height() - 493)/2)+'px', ''],page: {html: log.html}, success: function(layero){log.success(layero);}})} else {log.chatmore = xxim.chatbox.find('#layim_chatmore');log.chatarea = xxim.chatbox.find('#layim_chatarea');log.chatmore.show();log.chatmore.find('ul>li').removeClass('layim_chatnow');log.chatmore.find('ul').append('<li data-id="'+ param.id +'" type="'+ param.type +'" id="layim_user'+ param.type + param.id +'" class="layim_chatnow"><span>'+ param.name +'</span><em>×</em></li>');log.chatarea.find('.layim_chatview').removeClass('layim_chatthis');log.chatarea.append('<ul class="layim_chatview layim_chatthis" id="layim_area'+ param.type + param.id +'"></ul>');xxim.tabchat(param);}//群组log.chatgroup = xxim.chatbox.find('#layim_groups');if(param.type === 'group'){log.chatgroup.find('ul').removeClass('layim_groupthis');log.chatgroup.append('<ul class="layim_groupthis" id="layim_group'+ param.type + param.id +'"></ul>');xxim.getGroups(param);}//点击群员切换聊天窗log.chatgroup.on('click', 'ul>li', function(){xxim.popchatbox($(this));});};//定位到某个聊天队列xxim.tabchat = function(param){var node = xxim.node, log = {}, keys = param.type + param.id;xxim.nowchat = param;xxim.chatbox.find('#layim_user'+ keys).addClass('layim_chatnow').siblings().removeClass('layim_chatnow');xxim.chatbox.find('#layim_area'+ keys).addClass('layim_chatthis').siblings().removeClass('layim_chatthis');xxim.chatbox.find('#layim_group'+ keys).addClass('layim_groupthis').siblings().removeClass('layim_groupthis');xxim.chatbox.find('.layim_face>img').attr('src', param.face);xxim.chatbox.find('.layim_face, .layim_names').attr('href', param.href);xxim.chatbox.find('.layim_names').text(param.name);xxim.chatbox.find('.layim_seechatlog').attr('href', config.chatlogurl + param.id);log.groups = xxim.chatbox.find('.layim_groups');if(param.type === 'group'){log.groups.show();} else {log.groups.hide();}$('#layim_write').focus();};//弹出聊天窗xxim.popchatbox = function(othis){var node = xxim.node, dataId = othis.attr('data-id'), param = {id: dataId, //用户IDtype: othis.attr('type'),name: othis.find('.xxim_onename').text(),  //用户名face: othis.find('.xxim_oneface').attr('src'),  //用户头像href: config.hosts + 'user/' + dataId //用户主页}, key = param.type + dataId;if(!config.chating[key]){xxim.popchat(param);config.chatings++;} else {xxim.tabchat(param);}config.chating[key] = param;var chatbox = $('#layim_chatbox');if(chatbox[0]){node.layimMin.hide();chatbox.parents('.xubox_layer').show();}};//请求群员xxim.getGroups = function(param){var keys = param.type + param.id, str = '',groupss = xxim.chatbox.find('#layim_group'+ keys);groupss.addClass('loading');config.json(config.api.groups, {}, function(datas){if(datas.status === 1){var ii = 0, lens = datas.data.length;if(lens > 0){for(; ii < lens; ii++){str += '<li data-id="'+ datas.data[ii].id +'" type="one"><img src="'+ datas.data[ii].face +'"><span class="xxim_onename">'+ datas.data[ii].name +'</span></li>';}} else {str = '<li class="layim_errors">没有群员</li>';}} else {str = '<li class="layim_errors">'+ datas.msg +'</li>';}groupss.removeClass('loading');groupss.html(str);}, function(){groupss.removeClass('loading');groupss.html('<li class="layim_errors">请求异常</li>');});};//聊天模版xxim.msgTpl = 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>';};//消息接收xxim.writeReceiveMessage = function(obj){var node = xxim.node, log = {};node.sendbtn = $('#layim_sendbtn');node.imwrite = $('#layim_write');var keys = xxim.nowchat.type + xxim.nowchat.id;var imarea = xxim.chatbox.find('#layim_area'+ keys);if(xxim.nowchat.id === obj.objname){obj.objface = xxim.nowchat.face;}else{obj.objface = 'images/3.png';}if(obj.receiveType === 'handleIQ'){}else if(obj.receiveType === 'handleMessage'){xxim.messageTip(1);imarea.append(xxim.msgTpl({time: obj.objtime,name: obj.objname,face: obj.objface,content: obj.content}));}else if(obj.receiveType === 'handlePresence'){//对聊天对象to的头像状态的修改}else if(obj.receiveType === 'handleError'){imarea.append(webxxim.xxim.msgTpl({time: obj.objtime,name: obj.objname,face: obj.objface,content: obj.content}),'me');}else if(obj.receiveType === 'handleStatusChanged'){//对自己头像状态的修改}else{}node.imwrite.val('').focus();imarea.scrollTop(imarea[0].scrollHeight);}//消息传输xxim.transmit = function(){var node = xxim.node, log = {};node.sendbtn = $('#layim_sendbtn');node.imwrite = $('#layim_write');//发送log.send = function(){var data = {content: node.imwrite.val(),id: xxim.nowchat.id,sign_key: '', //密匙_: +new Date};if(data.content.replace(/\s/g, '') === ''){layer.tips('说点啥呗!', '#layim_write', 2);node.imwrite.focus();} else {//此处皆为模拟var keys = xxim.nowchat.type + xxim.nowchat.id;            var imarea = xxim.chatbox.find('#layim_area'+ keys);var nowstr = util.getDatetime();imarea.append(xxim.msgTpl({time: nowstr,name: config.user.name,face: config.user.face,content: data.content}, 'me'));node.imwrite.val('').focus();imarea.scrollTop(imarea[0].scrollHeight);/发送信息到服务器///serverChat.sendMessage(data.content,xxim.nowchat.id);////              setTimeout(function(){//                  imarea.append(xxim.msgTpl({//                      time: '2014-04-26 0:38',
//                      name: xxim.nowchat.name,
//                      face: xxim.nowchat.face,
//                      content: config.autoReplay[(Math.random()*config.autoReplay.length) | 0]
//                  }));
//                  imarea.scrollTop(imarea[0].scrollHeight);
//              }, 500);/*that.json(config.api.sendurl, data, function(datas){});*/}};node.sendbtn.on('click', log.send);node.imwrite.keyup(function(e){if(e.keyCode === 13){log.send();}});};//事件xxim.event = function(){var node = xxim.node;//主界面tabnode.tabs.eq(0).addClass('xxim_tabnow');node.tabs.on('click', function(){var othis = $(this), index = othis.index();xxim.tabs(index);});//列表展收node.list.on('click', 'h5', function(){var othis = $(this), chat = othis.siblings('.xxim_chatlist'), parentss = othis.parent();if(parentss.hasClass('xxim_liston')){chat.hide();parentss.removeClass('xxim_liston');} else {chat.show();parentss.addClass('xxim_liston');}});//设置在线隐身node.online.on('click', function(e){config.stopMP(e);node.setonline.show();});node.setonline.find('span').on('click', function(e){var index = $(this).index();config.stopMP(e);if(index === 0){node.onlinetex.html('在线');node.online.removeClass('xxim_offline');} else if(index === 1) {node.onlinetex.html('隐身');node.online.addClass('xxim_offline');}node.setonline.hide();});node.xximon.on('click', xxim.expend);node.xximHide.on('click', xxim.expend);//搜索node.xximSearch.keyup(function(){var val = $(this).val().replace(/\s/g, '');if(val !== ''){node.searchMian.show();node.closeSearch.show();//此处的搜索ajax参考xxim.getDatesnode.list.eq(3).html('<li class="xxim_errormsg">没有符合条件的结果</li>');} else {node.searchMian.hide();node.closeSearch.hide();}});node.closeSearch.on('click', function(){$(this).hide();node.searchMian.hide();node.xximSearch.val('').focus();});//弹出聊天窗config.chatings = 0;node.list.on('click', '.xxim_childnode', function(){var othis = $(this);xxim.popchatbox(othis);});//点击最小化栏node.layimMin.on('click', function(){$(this).hide();$('#layim_chatbox').parents('.xubox_layer').show();});//document事件dom[1].on('click', function(){node.setonline.hide();$('#layim_sendtype').hide();});};//请求列表数据xxim.getDates = function(index){var api = [config.api.friend, config.api.group, config.api.chatlog],node = xxim.node, myf = node.list.eq(index);myf.addClass('loading');config.json(api[index], {}, function(datas){if(datas.status === 1){var i = 0, myflen = datas.data.length, str = '', item;if(myflen > 1){if(index !== 2){for(; i < myflen; i++){str += '<li data-id="'+ datas.data[i].id +'" class="xxim_parentnode">'+'<h5><i></i><span class="xxim_parentname">'+ datas.data[i].name +'</span><em class="xxim_nums">('+ datas.data[i].nums +')</em></h5>'+'<ul class="xxim_chatlist">';item = datas.data[i].item;for(var j = 0; j < item.length; j++){if(item[j].name == config.user.name)continue;str += '<li data-id="'+ item[j].id +'" class="xxim_childnode" type="'+ (index === 0 ? 'one' : 'group') +'"><img src="'+ item[j].face +'" class="xxim_oneface"><span class="xxim_onename">'+ item[j].name +'</span></li>';}str += '</ul></li>';}} else {str += '<li class="xxim_liston">'+'<ul class="xxim_chatlist">';for(; i < myflen; i++){str += '<li data-id="'+ datas.data[i].id +'" class="xxim_childnode" type="one"><img src="'+ datas.data[i].face +'"  class="xxim_oneface"><span  class="xxim_onename">'+ datas.data[i].name +'</span><em class="xxim_time">'+ datas.data[i].time +'</em></li>'; }str += '</ul></li>';}myf.html(str);} else {myf.html('<li class="xxim_errormsg">没有任何数据</li>');}myf.removeClass('loading');} else {myf.html('<li class="xxim_errormsg">'+ datas.msg +'</li>');}}, function(){myf.html('<li class="xxim_errormsg">请求失败</li>');myf.removeClass('loading');});};//渲染骨架xxim.view = function(){var xximNode = xxim.layimNode = $('<div id="xximmm" class="xxim_main">'+'<div class="xxim_top" id="xxim_top">'+'  <div class="xxim_search"><i></i><input id="xxim_searchkey" /><span id="xxim_closesearch">×</span></div>'+'  <div class="xxim_tabs" id="xxim_tabs"><span class="xxim_tabfriend" title="好友"><i></i></span><span class="xxim_tabgroup" title="群组"><i></i></span><span class="xxim_latechat"  title="最近聊天"><i></i></span></div>'+'  <ul class="xxim_list" style="display:block"></ul>'+'  <ul class="xxim_list"></ul>'+'  <ul class="xxim_list"></ul>'+'  <ul class="xxim_list xxim_searchmain" id="xxim_searchmain"></ul>'+'</div>'+'<ul class="xxim_bottom" id="xxim_bottom">'+'<li class="xxim_online" id="xxim_online">'+'<i class="xxim_nowstate"></i><span id="xxim_onlinetex">在线</span>'+'<div class="xxim_setonline">'+'<span><i></i>在线</span>'+'<span class="xxim_setoffline"><i></i>隐身</span>'+'</div>'+'</li>'+'<li class="xxim_mymsg" id="xxim_mymsg" title="我的私信"><i></i><a href="'+ config.msgurl +'" target="_blank"></a></li>'+'<li class="xxim_seter" id="xxim_seter" title="设置">'+'<i></i>'+'<div class="">'+'</div>'+'</li>'+'<li class="xxim_hide" id="xxim_hide"><i></i></li>'+'<li id="xxim_on" class="xxim_icon xxim_on"></li>'+'<div class="layim_min" id="layim_min"></div>'+'</ul>'+'</div>');dom[3].append(xximNode);xxim.renode();xxim.getDates(0);xxim.event();xxim.layinit();};// 消息提示xxim.messageTip = function (countparam) {if (countparam % 4 != 0) {window.focus();document.title = "你来了新消息,请查收!";countparam ++;window.setTimeout(function(){xxim.messageTip(countparam)}, 1000);} else {document.title = "";                }}//exports.webim = xxim;/*** obj = {username:password: * }*/exports.initWebim = function(obj){xxim.view();var chatConnConfig = {httpbase: "http://localhost:8080/webim/JHB/", //请求后台http-bind服务器urldomain: 'openfire402', // //"192.168.5.231", // 192.168.5.231 当前有效域名username: "",pass: "",timerval: 2000, // 设置请求超时resource: "webim", // 链接资源标识register: true}//serverChat.writeReceiveMessage = xxim.writeReceiveMessage;serverChat.init(chatConnConfig,xxim);var loginPerson = {username:obj.username,password:obj.password,register:false  //是否注册改用户}serverChat.login(loginPerson);if(obj.username == 'webchattest'){config.user.face = 'http://tp1.sinaimg.cn/1571889140/180/40030060651/1';}config.user.name = loginPerson.username;}});

实现效果如下图:

源码可在此下载。

构建WebIM聊天程序相关推荐

  1. node mongoose_如何使用Express,Mongoose和Socket.io在Node.js中构建实时聊天应用程序

    node mongoose by Arun Mathew Kurian 通过阿伦·马修·库里安(Arun Mathew Kurian) 如何使用Express,Mongoose和Socket.io在N ...

  2. nw.js 打包换桌面图标_我如何使用CometChat和NW.js构建桌面聊天应用程序(以及方法)

    nw.js 打包换桌面图标 This is not your typical "paste this here" and "paste that there"- ...

  3. 构建iphone聊天应用程序

    架构一个 iPhone 聊天应用程序 目前已有 4000 万台 iPhones 在用,您无疑对编写 iOS 应用程序感兴趣.但是从何着手呢?大多数应用程序都会连接网络,那么一个跨越两端的项目(比如说聊 ...

  4. Openfire jsjac构建webIM

    [size=large] 在上一篇文章中,我们已经介绍如何用Openfire和jwchat构建webIM,但是我在搭建的过程中,总是感觉用户在登陆的时候速度非常慢,而且后期维护不好做 那么现在我在介绍 ...

  5. websockets_如何将WebSockets与AWS API Gateway和Lambda一起使用来构建实时应用程序

    websockets by Janitha Tennakoon 通过詹妮莎·特纳库恩 如何将WebSockets与AWS API Gateway和Lambda一起使用来构建实时应用程序 (How to ...

  6. 使用Go和WebSockets构建实时聊天服务器

    使用Go和WebSockets构建实时聊天服务器 源代码连接 Go Chat 现在web应用变得越来越复杂,前端开发人员的工资也是水涨船高.现在的web程序有些是可以实时更新的,用户无需主动调用服务器 ...

  7. 只需五步,快速构建Python聊天室

    在本文中,我们来谈一谈如何构建一个Python聊天室. 注意:你不需要安装任何额外的Python包. 作者 |  Dark Soulz 译者 | 弯月,责编 | 郑丽媛 头图 | CSDN 下载自东方 ...

  8. Java实验:编写网络聊天程序(图形界面)

    课程名称 高级Java程序设计 实验项目 Java网络编程 实验目的: 使用客户机/服务器模式.基于TCP协议编写一对多"群聊"程序.其中客户机端单击"连接服务器&quo ...

  9. Node.js + Web Socket 打造即时聊天程序嗨聊(上)

    前端一直是一块充满惊喜的土地,不仅是那些富有创造性的页面,还有那些惊赞的效果及不断推出的新技术.像node.js这样的后端开拓者直接将前端人员的能力扩大到了后端.瞬间就有了一统天下的感觉,来往穿梭于前 ...

  10. [前端] Node.js + Web Socket 打造即时聊天程序嗨聊

    前端一直是一块充满惊喜的土地,不仅是那些富有创造性的页面,还有那些惊赞的效果及不断推出的新技术.像node.js这样的后端开拓者直接将前端人员的能力扩大到了后端.瞬间就有了一统天下的感觉,来往穿梭于前 ...

最新文章

  1. Windows10 搭建java环境——JDK11的安装与eclipse的安装
  2. could not load inserted library: /usr/lib/libgmalloc.dylib
  3. codeblocks的错误提示框不见了
  4. 问题集锦(41-42)
  5. 开启php,php开启openssl的方法
  6. python pytest allure_python测试框架pytest和测试报告allure的联合使用-----测试套件
  7. c++两个数组对比去掉重复的元素_30 数组案例
  8. C++ 基类私有成员会被继承吗
  9. 【算法】排序_计数排序
  10. 太原理工计算机学科评估,太原理工大学学科评估结果及排名情况怎样
  11. 抓包工具charles下载安装(破解版)
  12. 安装打印机时出现无法安装,打印处理器不存在
  13. python 利用matplolib给绘制的地图添加方框,将所需的区域圈出来
  14. QT教程—1.1Qt入门
  15. osu计算机科学硕士,OSU的Computer Science and Engineering「俄亥俄州立大学计算机科学与工程系」...
  16. 网络技术安全开发安卓APP
  17. node 文件重命名
  18. 如何判断微信/判断支付宝退款超期
  19. Ukey证书校验流程和使用注意事项
  20. Python之路【第二十三篇】:数据库基础

热门文章

  1. 计算机教师简介招聘情况,招聘教师个人简历模板
  2. SECS协议基础知识
  3. 《北京市住房租赁条例》
  4. python3传智播客_3.Ubuntu安装以及配置(传智播客.黑马程序员python学科)
  5. Android 蓝牙配对、连接和通信
  6. pycharm连接github
  7. 计算机考研408的优势和劣势,为什么说计算机考研408是大趋势
  8. PAYPAL支付开发简介
  9. Teststand自定义测试报告
  10. cesium obj格式转换为gltf、glb