介绍环信
环信 Web IM SDK 为PC/移动 Web 应用,提供完善的即时通信功能开发能力,屏蔽其内部复杂细节,对外提供较为简洁的 API 接口,方便第三方应用快速集成即时通信功能。
完成功能
环信的SDK集成和配置基本按照官网给的文档就可以实现。本文仅完成了根据用户名互相发收文字信息的功能,更多功能可在此基础上参考官方文档、API。
web IM的SDK集成和配置
首先需要购买环信服务,申请appkey,使用账号和密码申请环信账号。
// 以下部分是在前端部分的集成和配置
step1:在官网下载WEIM SDK+体验demo(下载自己所需要的版本即可)
step2:创建自己的项目,将下载的文件解压,并将sdk目录下的三个文件复制到自己的项目中:
step3:创建html文件,用于展示聊天界面,在该html中使用使用引入外部js文件的方式,将step2中的三个文件引入进来。
step4:创建webIMConfig.js文件,用于对webim的配置:
// 配置 WebIM.config = { xmppURL: 'http://im-api-v2.easemob.com/ws', // xmpp Server地址 apiURL: 'http://a1.easemob.com', // rest Server地址 appkey: '', // App key https: false, // 是否使用https isHttpDNS: true, // 3.0 SDK支持,防止DNS劫持从服务端获取XMPPUrl、restUrl isMultiLoginSessions: false, // 是否开启多页面同步收消息,注意,需要先联系商务开通此功能 isAutoLogin: true, // 自动出席,(如设置为false,则表示离线,无法收消息,需要在登录成功后手动调用conn.setPresence()才可以收消息) isDebug: false, // 打开调试,会自动打印log,在控制台的console中查看log autoReconnectNumMax: 2, // 断线重连最大次数 autoReconnectInterval: 2, // 断线重连时间间隔 heartBeatWait: 4500, // 使用webrtc(视频聊天)时发送心跳包的时间间隔,单位ms delivery: true, // 是否发送已读回执 Host: "", } // 创建连接 var conn = {}; conn = WebIM.conn = new WebIM.default.connection({ appKey: WebIM.config.appkey, isHttpDNS: WebIM.config.isHttpDNS, isMultiLoginSessions: WebIM.config.isMultiLoginSessions, host: WebIM.config.Host, https: WebIM.config.https, url: WebIM.config.xmppURL, apiUrl: WebIM.config.apiURL, isAutoLogin: WebIM.config.isAutoLogin, heartBeatWait: WebIM.config.heartBeatWait, autoReconnectNumMax: WebIM.config.autoReconnectNumMax, autoReconnectInterval: WebIM.config.autoReconnectInterval, isStropheLog: WebIM.config.isStropheLog, delivery: WebIM.config.delivery })
以上内容都是直接从官网文档复制的,注意修改的地方有三个(代码中用红色标出的部分):
1.appkey:要替换成自己申请的appkey;
2.Host:配置成自己的服务器的主机号+端口号;
3.new webIM.default.connection,官网文档中给出的是new webIM.connection,但是报connection未定义的错误,我将webIM打印出来发现这个connection在default下边,所以改成了这样。直接复制代码的话可能会有这类似的问题,可以注意一下。
同样在step2中引入的webIMSDK3.0.6.js也有同样的错误,我也在webIM后边加上了.default。
step5:添加回调监听,官网文档里复制的:
conn.listen({ onOpened: function(message) { //连接成功回调 // 如果isAutoLogin设置为false,那么必须手动设置上线,否则无法收消息 // 手动上线指的是调用conn.setPresence(); 如果conn初始化时已将isAutoLogin设置为true // 则无需调用conn.setPresence(); console.log("连接成功") }, onClosed: function(message) {}, //连接关闭回调 onTextMessage: function(message) { console.log(message); receiverMessage(message) }, //收到文本消息 onEmojiMessage: function(message) {}, //收到表情消息 onPictureMessage: function(message) {}, //收到图片消息 onCmdMessage: function(message) {}, //收到命令消息 onAudioMessage: function(message) {}, //收到音频消息 onLocationMessage: function(message) {}, //收到位置消息 onFileMessage: function(message) {}, //收到文件消息 onVideoMessage: function(message) { var node = document.getElementById('privateVideo'); var option = { url: message.url, headers: { 'Accept': 'audio/mp4' }, onFileDownloadComplete: function(response) { var objectURL = WebIM.utils.parseDownloadResponse.call(conn, response); node.src = objectURL; }, onFileDownloadError: function() { console.log('File down load error.') } }; WebIM.utils.download.call(conn, option); }, //收到视频消息 onPresence: function(message) { handlePresence(message); }, //处理“广播”或“发布-订阅”消息,如联系人订阅请求、处理群组、聊天室被踢解散等消息 onRoster: function(message) {}, //处理好友申请 onInviteMessage: function(message) {}, //处理群组邀请 onOnline: function() {}, //本机网络连接成功 onOffline: function() { console.log("offline") }, //本机网络掉线 onError: function(message) { console.log('onError: ', message); }, //失败回调 onBlacklistUpdate: function(list) { //黑名单变动 // 查询黑名单,将好友拉黑,将好友从黑名单移除都会回调这个函数,list则是黑名单现有的所有好友信息 console.log(list); }, onRecallMessage: function(message) {}, //收到撤回消息回调 onReceivedMessage: function(message) {}, //收到消息送达服务器回执 onDeliveredMessage: function(message) {}, //收到消息送达客户端回执 onReadMessage: function(message) {}, //收到消息已读回执 onCreateGroup: function(message) {}, //创建群组成功回执(需调用createGroupNew) onMutedMessage: function(message) {} //如果用户在A群组被禁言,在A群发消息会走这个回调并且消息不会传递给群其它成员 });
实现客服功能
以上已经将环信配置完毕了,接下来使用环信的API制作客服功能。
这里我们申请了两个环信账号,一个作为客服,一个作为客户。
先来看一下效果:
客服端:
客户端:
前端样式非常简单,我直接将代码放在后边。注意在进行对话之前要先登录。由于这个项目会接入到商城中,因此不用显式登陆,会写一个接口传入用户名和密码,并自动完成登陆功能。
以下讲述几个制作中的难点:
难点1:接收和发送的信息展示在页面里。
为了让消息交替错落的展示,每一条消息都是一个<div>块级元素,这样就能自动完成换行。当接收或者发送消息的时候,在页面中插入一个相应的<div>。
难点2:当消息过多的时候,能够自动向上滚动,以显示最新消息。
使用的scrollTop动画来完成。给scrollTop设置一个很大的值,这样页面就能总是能滑动显示出最新的消息。
难点3:客服端的侧边显示来自不同用户的消息,并且显示消息未读小红点。
当收到信息时,我们收到的消息中包含一个from字段,表示消息来自哪个用户,根据这个字段将消息插入到对应的对话界面中。当当前显示的对话界面不是该用户时,就要出现小红点提示有未读信息。
添加一个全局变量talkTo,其中保存当前对话方的用户名,表示当前在与谁对话。通过点击消息列表中的用户来改变这个变量的值。我们用这个值来完成消息的发送等功能。同时也可以用这个值来判断出当前展示的对话界面,从而判断是否显示消息未读的小红点。
难点4:显示与不同用户的交流界面
使用<div>包裹用户名+用户对话界面,并给这个div添加带有用户信息的独特id。默认这个<div>是display:none。当通过侧边列表项选择对话方时,将对应的<div>设置为display:block
在官方文档中提供了消息的发送和接收的示例函数,可以直接复制过来再稍加改动,添加一点显示方面的代码就可以了。
代码
客户端和客服端代码基本一致,只是客户端没有旁边的列表,也不用显示不同的对话界面,所以html和css上稍有差异,js中对消息的处理和显示也稍简单一点。
他们都公用环信的配置、初始化和监听函数,尽量减少两者在监听中的差异,不同之处在各自的js中处理。
项目代码如下:
客服端:
HTML
<!DOCTYPE html> <!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]--> <!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]--> <!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]--> <!--[if gt IE 8]><!--> <html> <!--<![endif]--> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="index.css"> </head> <body> <!-- 消息列表 --> <ul class="sidebar" id="roster" onclick="showDialogist()"> <div>消息列表</div> </ul> <div class="message"> <!-- 信息显示 --> <div class="display" id="display"> <!-- 默认对话界面 --> <div> <div class="dialogist">欢迎来到客户服务平台</div> <div class="content"> </div> </div> </div> <!-- 信息编辑 --> <div class="edit"> <textarea id="sendContent"></textarea> <button onclick="sendMessage()">发送信息</button> </div> </div> <script type='text/javascript' src='lib/jquery-3.2.1.js'></script> <script type='text/javascript' src='lib/webimSDK3.0.6.js'></script> <script type='text/javascript' src='lib/EMedia_x1v1.js'></script> <!-- <script type='text/javascript' src='lib/EMedia_sdk-dev.js'></script> --> <script type='text/javascript' src='WebIMConfig.js'></script> <script type='text/javascript' src='index.js'></script> </body> </html>
CSS
body { margin: 0; } /* 侧边栏 */ .sidebar { position: fixed; top: 0; left: 0; width: 250px; height: 100%; background: #444; margin: 0; padding: 0; } .sidebar div { width: 100%; text-align: center; color: #FFF; font-size: 18px; font-weight: bold; margin-top: 16px; margin-bottom: 19px; } .sidebar li { padding: 18px; display: flex; align-items: center; position: relative; cursor: pointer; } .sidebar img { width: 45px; height: 45px; margin-right: 16px; } .sidebar .name { margin-right: 5px; color: #c8c9cc; font-size: 18px; width: 130px; overflow: hidden; text-overflow: ellipsis; } .sidebar .redIcon { width: 12px; height: 12px; background-color: red; border-radius: 50%; position: absolute; top: 3px; left: 56px; } /* 侧边栏标签鼠标移入样式 */ .sidebar li:hover { background-color: #666; color: #FFF; } /* 侧边栏标签选中样式 */ .sidebar-selected, .sidebar .sidebar-selected:hover { background: #FFF; } .sidebar-selected .name { color: #333; } /* 信息部分 */ .message { margin-left: 250px; width: calc(100% - 250px); } /* 信息显示部分 */ .message .display { width: 100%; } .message .display .dialogist { width: 100%; border-bottom: 1px solid #333; padding: 16px; font-size: 20px; } .message .display .content { width: 100%; height: 385px; padding-bottom: 16px; overflow-y: scroll; } .message .display .head { width: 30px; height: 30px; border-radius: 50%; margin: 5px 16px; } .message .display .time { color: #bbb; font-size: 12px; margin: 5px 8px; } .message .display .textBox { max-width: 250px; min-height: 20px; border-radius: 5px; padding: 8px 16px; box-shadow: 0 0 2px 1px #ddd; } .message .display .left { display: flex; align-items: flex-start; justify-content: flex-start; padding-left: 10px; margin-top: 16px; } .message .display .right { display: flex; align-items: flex-start; justify-content: flex-end; padding-right: 10px; margin-top: 16px; } .message .display .left .textBox { background-color: yellowgreen; } .message .display .right .textBox { background-color: #fff; } /* 信息编辑部分 */ .message .edit { border-top: 1px solid #333; height: 230px; width: 100%; } .message .edit textarea { width: 100%; height: calc(100% - 70px); resize: none; padding: 10px; font-size: 16px; border: 0; outline: none; } .message .edit button { float: right; background-color: #333; border: 0; color: #fff; padding: 8px 16px; cursor: pointer; margin-right: 30px; } .message .edit button:hover { background-color: #666; }
JS
var username = "";//客服账号 var password = "";//客服密码 var talkTo = "";//当前对话方 var curTalkList = new Set();//当前对话用户列表 window.onload = function() { this.login(); } // 登陆 function login() { //登陆API var options = { apiUrl: WebIM.config.apiURL, user: username, pwd: password, appKey: WebIM.config.appkey }; conn.open(options); } // 退出 function logOut() { conn.close(); } // 单聊发送文本信息 //官方文档示例函数,稍加修改,与发送消息显示函数连接到一起 function sendPrivateText(myTime, content) { var id = conn.getUniqueId(); // 生成本地消息id var msg = new WebIM.default.message('txt', id); // 创建文本消息 msg.set({ msg: content, // 消息内容 to: talkTo, // 接收消息对象(用户id) roomType: false, ext: { "time": myTime }, //扩展消息 success: function(id, serverMsgId) { console.log('send private text Success'); }, // 对成功的相关定义,sdk会将消息id登记到日志进行备份处理 fail: function(e) { console.log("Send private text error"); } // 对失败的相关定义,sdk会将消息id登记到日志进行备份处理 }); msg.body.chatType = 'singleChat'; conn.send(msg.body); }; // 显示已发送信息 function sendMessage() { var myTime = new Date().toLocaleString(); var content = document.getElementById("sendContent").value; document.getElementById("sendContent").value = ""; $("#" + talkTo + "_i").children(".content").append('<div class="right">' + '<span class="time">' + myTime + '</span>' + '<div class="textBox">' + content + '</div>' + '<img class="head" src="./img/logo.png"></div>') $("#" + talkTo + "_i").children(".content").animate({ scrollTop: 9999 }, 200) sendPrivateText(myTime, content); } // 显示接收到的信息 // 客服端:在列表中显示消息 function receiverMessage(message) { var name = message.from, time = message.ext.time, content = message.data; if (curTalkList.has(name)) { $("#" + name + "_i").children(".content").append('<div class="left">' + '<img class="head" src="./img/logo.png">' + '<div class="textBox">' + content + '</div>' + '<span class="time">' + time + '</span></div>') if (talkTo == name) { $("#" + name).children(".redIcon").attr("style", "display:block;"); } } else { // 添加消息列表中的信息 curTalkList.add(name); $("#roster").append('<li id="' + name + '"><div class="redIcon"></div><img src="./img/logo.png"><div class="name">' + name + '</div></li>'); // 添加消息显示框中的信息 $("#display").append('<div style="display:none;" id="' + name + '_i"><div class="dialogist">' + name + '</div>' + '<div class="content">' + '<div class="left">' + '<img class="head" src="./img/logo.png">' + '<div class="textBox">' + content + '</div>' + '<span class="time">' + time + '</span></div>' + '</div></div>') } $("#" + name + "_i").children(".content").animate({ scrollTop: 9999 }, 200) } // 点击列表中的用户,显示对应的对话界面 function showDialogist() { $("#roster").children("li").attr("class", ""); $("#display").children("div").attr("style", "display:none;"); var cur = event.target.closest("li").id; talkTo = cur; $("#" + cur).attr("class", "sidebar-selected"); $("#" + cur).children(".redIcon").attr("style", "display:none;"); $("#" + cur + "_i").attr("style", "display:block;"); }
客户端:
HTML
<!DOCTYPE html> <!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]--> <!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]--> <!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]--> <!--[if gt IE 8]><!--> <html> <!--<![endif]--> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="client.css"> </head> <body> <div class="message"> <!-- 信息显示 --> <div class="display"> <div class="dialogist">商城-客服</div> <div class="content" id="display"> </div> </div> <!-- 信息编辑 --> <div class="edit"> <textarea id="sendContent"></textarea> <button onclick="sendMessage()">发送信息</button> </div> </div> <script type='text/javascript' src='lib/jquery-3.2.1.js'></script> <script type='text/javascript' src='lib/webimSDK3.0.6.js'></script> <script type='text/javascript' src='lib/EMedia_x1v1.js'></script> <!-- <script type='text/javascript' src='lib/EMedia_sdk-dev.js'></script> --> <script type='text/javascript' src='WebIMConfig.js'></script> <script type='text/javascript' src='client.js'></script> </body> </html>
CSS
body { margin: 0; } /* 信息部分 */ .message { width: 100%; } /* 信息显示部分 */ .message .display { width: 100%; } .message .display .dialogist { width: calc(100% - 32px); border-bottom: 1px solid #333; padding: 16px; font-size: 20px; } .message .display .content { width: 100%; height: 385px; padding-bottom: 16px; overflow-y: scroll; } .message .display .head { width: 30px; height: 30px; border-radius: 50%; margin: 5px 16px; } .message .display .time { color: #bbb; font-size: 12px; margin: 5px 8px; } .message .display .textBox { max-width: 250px; min-height: 20px; border-radius: 5px; padding: 8px 16px; box-shadow: 0 0 2px 1px #ddd; } .message .display .left { display: flex; align-items: flex-start; justify-content: flex-start; padding-left: 10px; margin-top: 16px; } .message .display .right { display: flex; align-items: flex-start; justify-content: flex-end; padding-right: 10px; margin-top: 16px; } .message .display .left .textBox { background-color: yellowgreen; } .message .display .right .textBox { background-color: #fff; } /* 信息编辑部分 */ .message .edit { border-top: 1px solid #333; height: 230px; width: 100%; } .message .edit textarea { width: calc(100% - 20px); height: calc(100% - 70px); resize: none; padding: 10px; font-size: 16px; border: 0; outline: none; } .message .edit button { float: right; background-color: #333; border: 0; color: #fff; padding: 8px 16px; cursor: pointer; margin-right: 30px; } .message .edit button:hover { background-color: #666; }
JS
// 登陆 var username = "";//客户用户名 var password = "";//客户密码 var talkTo = "";//客服用户名 window.onload = function() { this.login(); } function login() { var options = { apiUrl: WebIM.config.apiURL, user: username, pwd: password, appKey: WebIM.config.appkey }; conn.open(options); } // 退出 function logOut() { conn.close(); } // 单聊发送文本信息 function sendPrivateText(myTime, content) { var id = conn.getUniqueId(); // 生成本地消息id var msg = new WebIM.default.message('txt', id); // 创建文本消息 msg.set({ msg: content, // 消息内容 to: talkTo, // 接收消息对象(用户id) roomType: false, ext: { "time": myTime }, //扩展消息 success: function(id, serverMsgId) { console.log('send private text Success'); }, // 对成功的相关定义,sdk会将消息id登记到日志进行备份处理 fail: function(e) { console.log("Send private text error"); } // 对失败的相关定义,sdk会将消息id登记到日志进行备份处理 }); msg.body.chatType = 'singleChat'; conn.send(msg.body); }; // 显示已发送信息 function sendMessage() { var myTime = new Date().toLocaleString(); var content = document.getElementById("sendContent").value; document.getElementById("sendContent").value = ""; $("#display").append('<div class="right">' + '<span class="time">' + myTime + '</span>' + '<div class="textBox">' + content + '</div>' + '<img class="head" src="./img/logo.png"></div>') $("#display").animate({ scrollTop: 9999 }, 200) sendPrivateText(myTime, content); } // 显示接收到的信息 function receiverMessage(message) { var time = message.ext.time, content = message.data; $("#display").append('<div class="left">' + '<img class="head" src="./img/logo.png">' + '<div class="textBox">' + content + '</div>' + '<span class="time">' + time + '</span></div>') $("#display").animate({ scrollTop: 9999 }, 200) }