乐趣区

关于前端:落地前端WebSocketlayim客服聊天功能详解

贤心大佬做的 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 聊天功前端能实现的所有内容了。从音讯的收到发、心跳重连详解、额定性能实现,整个篇幅有些长,写文章不易,如果对你有帮忙,心愿能反对一下(给个赞),谢谢~

退出移动版