贤心大佬做的web端聊天layim总体是不错的,界面简洁、易用,然而只有根底的界面demo,想要能真正用起来还要许多工作,上面咱们就应用websocket联合layim来实现一个聊天零碎。当然聊天界面也能够本人做一套新的,只有写好本人的api、钩子就行。

WebSocket是什么?

websocketTCP上的一种双向通信协定,应用http实现了一部分握手,而后须要http响应101状态码进行以后协定降级为websocket即可双向发送或承受信息(不仅客户端能够发送给服务端,服务端也能够本人被动发送信息给客户端),websocket比传统的无状态http开销更小,http不仅有着比websocket更大的申请头,而且只能单向被动发信息,如果依赖http应用长轮询还会造成肯定的服务器压力问题,音讯有提早。所以应用websocket是实现web聊天性能的首选。
罕用的函数有:WebSocket.onmessage(当接管到音讯),WebSocket.send(音讯发送),WebSocket.close(敞开连贯),WebSocket.onopen(当连贯建设)。

更多websocket原理细节不是本篇文章的重点,并且本篇次要介绍聊天性能前端局部的实现步骤,没有退出服务端内容,默认服务端已实现了websocket收发性能及音讯散发问题。

初始化聊天窗界面、建设连贯:

创立一个chatDialog.js,次要内容是聊天性能的次要逻辑,首先引入layuilayim模块,并按照文档初始化layim的界面:

chatDialog.js

layui.use(["layim", "table", "form", "layer",'upload'], function (layim) {  var kfId = 'xiaoming';  layim.config({    //简洁模式(不显示主面板)    brief: true,    uploadImage: {      url: "", //(返回的数据格式见下文)      type: "", //默认post    },    mine: {      username: kfId, //我的昵称      id: kfId, //我的ID      status: "online", //在线状态 online:在线、hide:隐身      sign: "", //我的签名      avatar: "../../images/kficon.png", //我的头像    },    chatLog: "../DialogueManagement/DialogueRecord/DialogueRecordInfo.html", //聊天记录地址  });  request({    url: "personalConfig/getInfo",    method: "get",    success: function (res) {      layim.config({        brief: true,        uploadImage: {          url: "", //(返回的数据格式见下文)          type: "", //默认post        },        mine: {          username: res.data.name, //我的昵称          avatar: fileServiceUrl + res.data.image, //我的头像          id: kfId,        },        chatLog: "../DialogueManagement/DialogueRecord/DialogueRecordInfo.html", //聊天记录地址      });    },    error: function (res) {      console.log("网络谬误");    },  });});

这里定义了一个简洁模式的聊天性能,通过申请接口获取到用户信息(昵称、头像)又从新更新了一遍以后用户的信息。kfId就是以后用户的惟一账号。这里chatLog相当于在右侧新开了一个DialogueRecordInfo页面,能够在DialogueRecordInfo独自写音讯记录的列表逻辑,这里就不介绍了。

聊天窗界面有了,接下来咱们创立websocket,首先定义一个websocket的地址(也就是后端websocket服务的地址),并拼接url(这里有个规定:当是http的状况下就应用ws,https对应应用wss)前面加上以后登录用户的kfId示意以后xiaoming用户申请建设websocket连贯(代表xiaoming登录聊天,以让后端确定xiaoming曾经登录,能够承受其他人的音讯),定义一个webSocket全局变量寄存实例:

  var chatServerUrl = "http://11.11.11.12:9981/"; //聊天服务地址  var kfId = 'xiaoming';  var url =    chatServerUrl.replace("http", "ws").replace("https", "wss") +    "websocket/" +    kfId; //聊天websocket服务地址  webSocket = null;  createWebSocket();  /**   * websocket启动   */  function createWebSocket() {    if (kfId) {      if ("WebSocket" in window) {        try {          webSocket = new WebSocket(url);          initWs(webSocket);        } catch (e) {          console.log("catch" + e);          console.log("------连贯失败,聊天连贯已敞开...正在重连");          reconnect();        }      } else {        // 浏览器不反对 WebSocket        alert(          "您的浏览器不反对客服发送聊天信息 WebSocket! 请应用新版谷歌浏览器关上网站"        );      }    } else {      if (webSocket.readyState == WebSocket.CLOSED) {        reconnect();        console.log(webSocket.readyState, WebSocket.CLOSED);      } else {        console.log("no account");      }    }  }  function initWs() {    webSocket.onopen = function () {      if (webSocket.readyState == 1) {        heartCheck.start();      }      alert("胜利进入会话音讯接管状态,onopen...");      window.onbeforeunload = function () {        webSocket.close();      };    };    //连贯谬误    webSocket.onerror = function (r) {      console.log("连贯谬误onerror,聊天连贯已敞开...开始重连" + r);      console.log("断开");      reconnect();    };    webSocket.onclose = function (e) {      // 敞开 websocket,清空信息板       console.log("聊天连贯已敞开onclose...开始重连---");      heartCheck.timeoutObj && clearTimeout(heartCheck.timeoutObj);      heartCheck.serverTimeoutObj && clearTimeout(heartCheck.serverTimeoutObj);      console.log(        "websocket 断开: " + e.code + " " + e.reason + " " + e.wasClean      );      reconnect();    };  }

createWebSocket用于创立一个websocket实例,在创立时退出了try用于捕捉到创立时的谬误,在这一过程中产生谬误就会执行reconnect()进行断线重连,断线重连前面会说。
当创立胜利时执行initWs()定义一些websocket不同状态下的钩子函数,当websocket onopen的时候连贯建设并webSocket.readyState为1时示意连贯胜利,开始eartCheck.start()心跳,心跳检测前面会说。

window.onbeforeunload时,也就是网页敞开之前须要敞开掉以后websocket的连贯webSocket.close,目标是告知后端以后用户退出了聊天(强制),同时也是为了防止一些奇怪的问题呈现。

webSocket.onerror是在应用websocket期间产生了一些谬误,这里也是须要进行reconnect()进行断线重连。

webSocket.onclose示意以后连贯敞开了(断开)因为曾经断开了连贯就不须要心跳检测了,clearTimeout(heartCheck.timeoutObj)革除掉心跳检测的计时器,同时也是要进行重连。

初始钩子定义好了,接下来看外围性能音讯接入、音讯收发局部

音讯接入、音讯收发:

承受到的数据因为是字符串,须要转换一下,这里咱们设计status有多种状态,因为onmessage不能只是繁多的接管聊天音讯:
status 1:新音讯,
status 2:有新用户接入聊天,
status 4: 有音讯转接进入(其余客服转接过去)
status 5:你的转接被客服回绝
status 9 :服务器的心跳
胜利进入onmessage就曾经证实服务器心跳失常,持续在initWs函数中退出如下代码:

  function initWs() {    ...      var wsuserObj = {};   //websocket音讯接管    webSocket.onmessage = function (res) {      heartCheck.start();      var status = JSON.parse(res.data).status;      var data = JSON.parse(res.data).data;      console.log(res);      if (status == 1) {        //用户发送音讯        wsuserObj.userId;        layer.msg("您有新音讯");        layim.getMessage({          username: data.userName,          avatar: "../../images/yonghu.png",          id: data.userId,          type: "kefu",          kfId: data.kfId,          kfName: data.kfId,          userId: data.userId,          content: layim.content(data.content),          timestamp: data.createTime,        });        wsuserObj = data;      }      if (status == 2) {        //有新接入        layer.msg("您有新音讯申请接入");        //新接入的id为userid        wsuserObj = data;        wsuserObj.userId = data.id;      }      if (status == 5) {        //转接被拒,音讯退回        addCount();        layer.alert("指标客服坐席不承受转接,会话放弃");        layim.getMessage({          username: data.userName,          avatar: "../../images/yonghu.png",          id: data.id,          type: "kefu",          kfId: kfId,          kfName: kfId,          userId: data.id,          content: "零碎音讯:指标客服坐席不承受音讯转接,会话放弃",          timestamp: new Date().getTime(),        });      }      if (status == 4) {        //收到坐席转接音讯        layer.open({          content:            "您收到一个来自其余客服转接过去的会话音讯,是否承受解决该用户音讯?",          closeBtn: false,          btn: ["承受", "不承受"],          btnAlign: "c",          yes: function (index, layero) {            webSocket.send(              toStr({                respStatus: 4, //4承受  5不承受                userId: data.id,                kfId: kfId,              })            );            layer.close(index);            layim.getMessage({              username: data.userName,              avatar: "../../images/yonghu.png",              id: data.id,              type: "kefu",              kfId: kfId,              kfName: kfId,              userId: data.id,              content: "零碎音讯:转接胜利",            });            layim.show();          },          btn2: function (index, layero) {            webSocket.send(              toStr({                respStatus: 5,                userId: data.id,              })            );            layer.close(index);          },        });      }      if (status == 9) {        console.warn("----心跳失常pong---", new Date());      }    };}

status为1是新的聊天音讯,应用layim.getMessage触发新音讯显示进去的成果,这里data带着一些发送人的信息(网名、内容、惟一id、工夫)为了标识这条信息是这个用户发的。
当有新音讯或新用户接入聊天时都会对全局变量wsuserObj进行一次发送方数据的笼罩,保护一个以后正在聊天的发送方对象。

因为以后如有弹窗的话调用getMessage会把弹窗进行敞开 ,和咱们当初的逻辑抵触,咱们不须要达到这种成果, 所以要在getMessage函数源码最初这里须要批改一下setChatMin逻辑:
layui/lay/modules/layim.js

return setChatMin前退出     if (!$("#layui-layim-min").length) {       return;}

接下来是音讯的发送,发送同样也是须要先调用toStr把参数转换成字符串再发送,当有新用户接入时咱们的设计不是立刻弹出聊天窗口,因为不确定以后客服在不在电脑前,所以只能进行一个提醒“以后有新用户接入”,用户看到时须要点击openDialog(接入)按钮接入聊天能力开始聊天,此时应用webSocket.send发送respStatus:2和本人id和发送方的信息交给后端进行接入:

    ...      var wsuserObj = {};    ...  //objec转string  function toStr(obj) {    return JSON.stringify(obj);  }  //点击接入  $("#openDialog").click(function () {    if (webSocket == null) {      alert("接入失败:ws连贯服务器失败,请查看网络是否失常!");      return;    }    if (!wsuserObj.userId) {      alert("以后无用户音讯!");      return;    }    layer.confirm(      "是否确认接入一个新的用户音讯会话?",      function (index) {        //do something        webSocket.send(          toStr({            respStatus: 2,            kfId: kfId,            userId: wsuserObj.id,            userName: wsuserObj.userName,          })        );        layim.getMessage({          username: wsuserObj.userName,          avatar: "../../images/yonghu.png",          id: wsuserObj.id,          type: "kefu",          kfId: kfId,          kfName: kfId,          userId: wsuserObj.id,          content: "零碎音讯:用户已接入",        });        layer.close(index);        layim.show();      },      function (index) {        //do something        webSocket.send(          toStr({            respStatus: 3, //不接入,给其他人接吧            kfId: kfId,            userId: wsuserObj.userId,            userName: wsuserObj.userName,          })        );        layer.close(index);        reduceCount();      }    );  });  //监听发送音讯  layim.on("sendMessage", function (data) {    var To = data.to;    console.log(data);    if (!webSocket || webSocket.readyState != 1) {      alert("发送失败,会话音讯连贯服务器谬误ws");    }    webSocket.send(      toStr({        kfId: data.mine.id,        kfName: data.mine.username,        userId: data.to.id,        userName: data.to.username,        content: data.mine.content,      })    );  });  //layim建设就绪  layim.on("ready", function (res) {});

能够看到webSocket.send就是websocket发送音讯 非常简单,当我填好内容点击发送按钮时,首先监听聊天窗的发送事件layim.on("sendMessage"),并从回调参数里获取到发送方数据和发送内容就很好办了,间接通过webSocket.send把我和对方的id同时发给后端解决就行了,留神这里要检查一下webSocket.readyState的状态,当为不失常或断开状态下提醒用户不能发消息。
有哪些状态?别离示意什么?

心跳检测、断线重连:

websocket连贯过程中呈现网络中断,或者心跳失败,执行报错时不能对以后已失败的连贯束之高阁,须要从新进行连贯直到连贯失常为止:

    //防止反复连贯    var lockReconnect = false,      tt;    /**     * websocket重连     */    function reconnect() {      if (lockReconnect) {        return;      }      lockReconnect = true;      tt && clearTimeout(tt);      tt = setTimeout(function () {        console.log("重连中...");        lockReconnect = false;        createWebSocket();      }, 5000);    }

当连贯异样时调用reconnect(),每五秒调用一次createWebSocket创立websocket,创立失败时同时也会继续执行reconnect(),lockReconnect标识为了防止多个重连操作。

接下来看看心跳检测,什么是心跳检测?
咱们晓得聊天性能外围是要客户端和服务端必须要同时失常运行,当有一端不失常(如服务器宕机),或者客户端因为某些起因死掉了,此时服务器还不晓得客户端已死掉还在发送多余信息,这时就示意以后聊天性能呈现问题不能再应用了,须要重连操作,所以须要一个机制每隔一段时间去检测一下两端有没有死掉(是否还有心跳?)

    //心跳检测    var heartCheck = {      timeout: 30000, //每隔30秒发送心跳      severTimeout: 5000, //服务端超时工夫      timeoutObj: null,      serverTimeoutObj: null,      start: function () {        var _this = this;        this.timeoutObj && clearTimeout(this.timeoutObj);        this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);        this.timeoutObj = setTimeout(function () {          //这里发送一个心跳,后端收到后,返回一个心跳音讯,          //onmessage拿到返回的心跳就阐明连贯失常          console.warn("----心跳发送ping----", new Date());          webSocket.send(            toStr({              respStatus: 9, //心跳9              kfId: kfId,            })          ); // 心跳包          //计算回答的超时工夫          _this.serverTimeoutObj = setTimeout(function () {            console.error("----心跳超时,敞开连贯----", new Date());            webSocket.close();          }, _this.severTimeout);        }, this.timeout);      },    };

heartCheck.start中重点执行了两个定时器,首先timeoutObj是用于每30秒向服务端发送心跳包(2个入参),当服务器收到也会立即发送心跳status:9给客户端。
客户端心跳发送后立刻执行serverTimeoutObj计时器5秒后如果onmessage 里的heartCheck.start()没执行(没收到服务端的心跳回信),那就会导致serverTimeoutObj计时器没及时革除掉,因为此时是须要服务端的心跳回应后能力进行客户端的下一步心跳,当初没回应,断定为心跳超时,serverTimeoutObj就会敞开掉以后websocket连贯。
如果心跳失常的状况下,也就是服务器在5秒之内回复了音讯,heartCheck.start()将会执行胜利并革除掉了超时检测器,这样就能够进行下一次心跳了,以上操作周而复始。
留神在websocket连贯中状态时执行心跳检测会导致谬误。

到这里聊天性能根本算是实现了也能失常应用了,接下来能够额定加些货色丰盛一下:

退出会话、常用语:

退出会话性能是让用户能被动退出websocket连贯,须要调用后端接口,后端会通过id把你从后端保护的在线列表中踢出来,退出会话和常用语性能layim都没有,能够这里能够本人通过批改layim源码实现,在源码外面手写退出事件逻辑,而后在里面进行注册,当对应事件触发时就会执行里面的回调函数,批改以下代码,退出常用语按钮、把发送傍边的按钮改成endChat “退出此会话”,留神批改layim-event属性:

layui/lay/modules/layim.js

    '<span class="layui-icon layim-tool-face" title="抉择表情" layim-event="face">&#xe60c;</span>',    "{{# if(d.base && d.base.uploadImage){ }}",    '<span class="layui-icon layim-tool-image" title="上传图片" layim-event="image">&#xe60d;<input type="file" name="file"></span>',    "{{# }; }}",    '<span class="layui-icon layui-icon-release" title="转接" layim-event="transfer" ></span>',    '<span class="layui-icon layui-icon-star" title="常用语" layim-event="commonLang" ></span>',    "{{# if(d.base && d.base.chatLog){ }}",    '<span class="layim-tool-log" layim-event="chatLog"><i class="layui-icon">&#xe60e;</i>聊天记录</span>',    "{{# }; }}",    "</div>",    '<div class="layim-chat-textarea"><textarea></textarea></div>',    '<div class="layim-chat-bottom">',    '<div class="layim-chat-send">',    "{{# if(!d.base.brief){ }}",    '<span class="layim-send-close" layim-event="closeThisChat">敞开</span>',    "{{# } }}",    '<span class="layim-send-close" layim-event="endChat">退出此会话</span>',

而后增加原型办法commonLang,同时也在events中增加 commonLang、endChat办法,最初会通过layui.each(call.commonLang)触发chatDialog.js注册的事件:

layui/lay/modules/layim.js

  LAYIM.prototype.close = function(content){    changeChat(null, 1);  };    LAYIM.prototype.show = function (content) {     layui.each(cache.chat, function (i, item) {       popchat(item);     });  };  LAYIM.prototype.commonLang = function (data) {      events.commonLang(data);    };             endChat: function () {        layui.each(call.endChat, function (index, item) {          item && item(thisChat());        });        changeChat(null, 1);      },      commonLang: function (othis,e) {          thatChat = thisChat(),          hide = function () {            layer.close(events.commonLang.index);          };           layui.each(call.commonLang, function (index, item) {             item && item(function (listdom) {                    events.commonLang.index = layer.tips(listdom, othis, {                      tips: 1,                      time: 0,                      fixed: true,                      skin: "layui-box layui-layim-commonLang",                      success: function (layero) {                        layero.find("div.cl-item").on("click", function () {                          focusInsert(thatChat.textarea[0], $(this).text());                          layer.close(events.face.index);                        });                      },                    });             });           });              $(document).off("mousedown", hide).on("mousedown", hide);        $(window).off("resize", hide).on("resize", hide);        stope(e); },

这里的commonLang事件往外传了一个匿名回调函数,函数性能次要是显示一个弹窗在输入框下面,参数是一个列表dom(常用语数据)而后渲染进去,随后监听列表点击,点击时把常用语插入到光标地位处。

源码局部改完接着看看chatDialog.js,在chatDialog.js中注册endChat事件,这里的退出会话就是个简略的接口调用,源码中有敞开弹窗的操作:

  //完结以后会话  layim.on("endChat", function (data) {    if (!data.data.userId) {      layer.msg("以后会话不存在,无奈退出");      return;    }    request({      url: chatServerUrl + "overSession/overUser",      data: {        kfId: kfId,        userId: data.data.userId,      },      method: "get",      success: function (resp) {        layer.msg(resp.msg);      },      error: function (resp) {        layer.msg("退出失败");      },    });  });  //常用语  layim.on("commonLang", function (showDialog) {    commonLangList(showDialog);  });  function commonLangList(cb) {    request({      url: baseUrl + "sessionComm/sessionCommList",      data: JSON.stringify({        content: "",        page: 1,        pageCount: 999,        source: "",      }),      method: "post",      success: function (res) {        var vListDom = "";        res.data.list.forEach(function (item) {          vListDom += '<div class="cl-item">' + item.content + "</div>";        });        cb && cb('<div class="cl-box">' + vListDom + "</div>");      },      error: function (res) {        console.log(res);      },    });  }

常用语这里次要是调用接口拿取常用语数据后,开始拼装常用语列表dom,传入回调函数进行执行(显示出弹窗)。

成果:

退出emoji:

layim的默认表情是一张张图片,这是在同样的layim上应用倒是没问题,但如果音讯是发到挪动端上因为没有layim源码对标识的外部转换性能表情就显示不进去了,这里能够改成emoji跨平台比拟好,同样是要批改源码:

layui/lay/modules/layim.js

  //表情库  var faces = function(){    var arr = [ "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "☺", "", "", "", "", "", "", "", "",     "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",      "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",      "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "☝",      "", "", "✌", "✋", "", "", "", "✊", "", "", "", "", "✍", "", "", "", "", "", "", "", "", "", "", "", "", "", "",      "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ];    // layui.each(alt, function(index, item){    //   arr[item] = layui.cache.dir + 'images/face/'+ index + '.gif';    // });    return arr;  }();

这样emoji就能用啦

chatDialog.js最终代码放到我的GitHub上了:
https://github.com/booms21/la...

以上就是WebSocket+layim聊天功前端能实现的所有内容了。从音讯的收到发、心跳重连详解、额定性能实现, 整个篇幅有些长,写文章不易,如果对你有帮忙,心愿能反对一下(给个赞),谢谢~