〇、前言
公司 H5 项目需要接入 IM ,其他平台目前已经对接了 Udesk 提供的 IM 服务,H5 也需要保持一致,开发之前早就听安卓和 iOS 同事说 Udesk 的坑很多,源码他们都改了好几版,不知道 H5 的会不会也这样刺激,抱着忐忑的心情准备试一试。
本文记录了一次 Vue 版本 H5 接入 Udesk WebIM 网页插件的踩坑过程,一方面是自己记录一下,另外一方面是希望能帮助到即将踩坑的童鞋,避免踩坑。
一、注册 Udesk
如果没有账号需要进行注册。
Udesk官网:https://www.udesk.cn
注册之后可以免费向客服获取到试用的账号,自己或者公司的账号要使用 IM 插件的功能需要付费开通权限。
二、使用 IM 插件
这里列举使用的主要几个过程 具体详细过程可参考udesk文本IM对接文档:http://www.udesk.cn/doc/thirdparty/webim/
1.执行 Udesk
提供的函数
Udesk 提供了一个创建 script 加载远端资源的函数:
(function (a, h, c, b, f, g) {
a.UdeskApiObject = f
a[f] =
a[f] ||
function () {
(a[f].d = a[f].d || []).push(arguments)
}
g = h.createElement(c)
g.async = 1
g.src = b
c = h.getElementsByTagName(c)[0]
c.parentNode.insertBefore(g, c)
})(
window,
document,
'script',
'https://assets-cli.udesk.cn/im_client/js/udeskApi.js',
'ud'
)
接入过百度统计的童鞋肯定会觉得似曾相识。
其作用是创建一个用于加载 Udesk WebIM 网页插件
的 script
标签,插入到 head
中,
由于给 script
标签添加了 async
属性,当 js 资源加载完成后会立即执行。
执行后会在window上挂载一个 ud 对象,初始化的时候需要向 ud 传入参数。
2.初始化 ud
ud({
"code": "xxxx",
"link": "https://xxx.xxxx.cn/im_client/?web_plugin_id=1",
"isInvite": true,
"mode": "inner",
"color": "#307AE8",
"pos_flag": "srb",
"language": "en-us",
"onlineText": "联系客服,在线咨询",
"offlineText": "客服下班,请留言",
"mobile": { //为响应式布局,提供pc、mobile自定制
"mode": "blank",
"color": "#307AE8",
"pos_flag": "crb",
"onlineText": "联系客服,在线咨询",
"offlineText": "客服下班,请留言"
}
})
初始化可传的常用参数是这些,除了 code
和 link
必传,其他的可以根据需求选填。
在 vue 中我们可以编写一个 udsk 工具函数
// udesk.js
export default {
init () {
if (!window.ud) {
this.createUdeskScript()
}
},
createUdeskScript () {
(function (a, h, c, b, f, g) {
a.UdeskApiObject = f
a[f] =
a[f] ||
function () {
(a[f].d = a[f].d || []).push(arguments)
}
g = h.createElement(c)
g.async = 1
g.src = b
g.id = useUdeskScriptId
c = h.getElementsByTagName(c)[0]
c.parentNode.insertBefore(g, c)
})(
window,
document,
'script',
'https://assets-cli.udesk.cn/im_client/js/udeskApi.js',
'ud'
)
const ud = window.ud
const udParams = {
code,
link,
... // 其他参数
}
}
ud(udParams)
}
}
在 main.js
中引入使用即可
// main.js
import Udesk from './utils/udesk'
Udesk.init()
3.Udesk 全局对象
初始化完成后会把udesk的挂载在window上,打印出来是这样子的:
检查dom节点,会发现 udesk
创建了 id 为 udesk_container
的 div
,结构如下。
id
为udesk_btn
和 udesk_panel
的 div
容器分别是打开IM会话面板的按钮(可自定义位置和图片样式等)和 IM 会话面板。
IM 的会话面板通过 iframe 嵌入的方式进行显示。会话面板如下图:
到这里,最基础的 IM 功能基本上可用了。但是,如果要补充细节的话,我们还需要做的可(zhen)能(de)还有很多。
三、Udsk 信息按钮的自定义
为什么要自定义?
1.这次项目中,需求是需要信息按钮下方会有另外一颗按钮悬浮在右下角,样式风格统一,如下:
Udesk 官方后台提供了 IM 信息按钮定位的配置,可以自己在后台调节位置,但坑爹的是不可以调节未读红点的样式,而且定位会导致和另外一个 icon 的对齐有误差。
2.未读显示不同页面样式不一样,比如这样:
3.Udesk 初始化后同一个页面的样式是固定的,单页面应用(Vue,React)要不同页面不同样式,所以用 Udesk 配置自动生产的信息按钮不满足我们的需求。
综上三点决自己写未读信息的按钮样式,根据提供的接口回调处理未读数。
四、同步未读信息数
由于多个页面都放了信息未读数的入口,因此我们需要在收到信息的时候同步到每个页面,未读数目用 Vuex 管理是不错的选择。
ud 的初始化中给到了一个 onUnread
回调函数,我们可以在里面处理未读数目。考虑到 H5 项目用户很大几率收到未读消息后切出去,因此很有必要缓存未读数。
onUnread: function (data) {
const count = data.count
Store.commit('SET_UNREADCOUNT', count)
_this.setLocal(count) //最后缓存到本地 下次打开保留上次的未读数目
}
同时,在初始化之后要从缓存中拉取未读数
//udParams 中的 onReady
onReady: function() {
// 从缓存中获取未读数目
const onUnreadCount = _this.getLocal()
if (onUnreadCount && onUnreadCount * 1 > 0) {
Store.commit('SET_UNREADCOUNT', onUnreadCount)
}
},
getLocal () {
return localStorage.getItem('onUnread')
}
这样子,在需要用到未读数的组件或者页面中就可以从 store 中直接取出来用,简单省心。
五、遇到的坑
1.Vue 项目中打开会话面板点击浏览器回退按钮,我们期待的正常情况是关闭会话面板,回到之前的页面,但是 Vue 中不是这样子,是页面路由回退,会话面板还在。
分析原因:导致此问题的原因是因为 Vue 是单页面应用,浏览器的回退前进是控制 Vue 的路由,整个页面没有去新,而且会话面板打开状态点击回退不会触发关闭会话面板关闭,需要手动点击最小化,会话面板才会消失。
怎么解决呢?
查了下文档,Udesk 提供了两个方法:hidePanel
和 showPanel
,分别是隐藏会话面板和打开会话面板。
文档推荐的使用方法:ud("hidePanel")
,ud("showPanel")
,除了文档推荐的这种方法,还可以用 ud.showPanel()
和 ud.hidePanel()
。
配合这两个方法,我们就可以解决上面的问题了。
开“淦”!!
新增一个页面用于储存路由,假设这个页面叫做 UdeskPanel
。
这个页面不写任何样式,它的作用只是用来处理 Udsk 的会话面板逻辑的。路由命中这个页面就调用 showPanel
方法,页面离开就调用 hidePanel
方法,这样子上面的就解决了。
为了统一管理 Udesk 的方法,我们把显示隐藏会话面板的方法集成在 udesk.js
中:
// udesk.js
export default {
init () {
if (!window.ud) {
this.createUdeskScript()
}
},
openUdPanel () {
if (window.ud) {
this.show()
const ud = window.ud
ud('showPanel')
}
},
hideUdPanel () {
if (window.ud) {
const ud = window.ud
ud('hidePanel')
}
},
createUdeskScript () {
(function (a, h, c, b, f, g) {
a.UdeskApiObject = f
a[f] =
a[f] ||
function () {
(a[f].d = a[f].d || []).push(arguments)
}
g = h.createElement(c)
g.async = 1
g.src = b
g.id = useUdeskScriptId
c = h.getElementsByTagName(c)[0]
c.parentNode.insertBefore(g, c)
})(
window,
document,
'script',
'https://assets-cli.udesk.cn/im_client/js/udeskApi.js',
'ud'
)
const ud = window.ud
const udParams = {
code,
link,
... // 其他参数
}
}
ud(udParams)
}
}
UdeskPanel 页面的代码:
// UdeskPanel.vue
<template>
<section class="UdeskPanel"></section>
</template>
<script>
import Udesk from '../utils/udesk'
export default {
name: 'UdeskPanel',
data () {
return {}
},
mounted () {
Udesk.openUdPanel()
},
beforeRouteLeave (to, from, next) {
Udesk.hideUdPanel()
next()
},
methods: {}
}
</script>
上面的问题解决了,新的问题来了,用户点击 Udesk 会话面板的最小化关闭了会话面板,我们停留在了 UdeskPanel
的空白页面。
这不是我们想要的。
所以我们还应该捕获关闭会话面板的事件,查了文档,ud 提供了一个 onToggle
的回调方法。检测当前会话面板是否打开,visible
为 true
时则是打开状态。
onToggle: function(data) {
if (!data.visible) {
// 对话窗口关闭
} else {
// 对话窗口打开
}
}
利用这个方法,我们可以配合 Vuex
全局维护一个会话面板是否打开的变量,UdeskPanel
页面中监听这个变量,当为 false
的时候去操作路由回退,前面的代码就变成下面的样子:
// UdeskPanel.vue
<template>
<section class="UdeskPanel"></section>
</template>
<script>
import Udesk from '../utils/udesk'
import { mapGetters } from 'vuex'
export default {
name: 'UdeskPanel',
data () {
return {}
},
mounted () {
Udesk.openUdPanel()
},
computed: {
...mapGetters(['udTraceIsUP']),
showPanel () {
return this.$store.state.udesk.showPanel
}
},
watch: {
showPanel (newVal, oldVal) {
if (newVal === false) {
this.$router.go(-1)
}
}
},
beforeRouteLeave (to, from, next) {
Udesk.hideUdPanel()
next()
},
methods: {}
}
</script>
似乎已经解决问题了。
但是,如果用户直接打开的页面是会话面板的这个页面,点击最小化,页面还是会在当前。
原因是当页面没有上一个路由记录时候, this.$router.go(-1)
就不会达到我们的要求了,因此需要其他操作离开当前页面,最好的方式是返回到首页。再优化一下代码:
showPanel(newVal, oldVal) {
if (newVal === false) {
if (window.history.length <= 2) {
this.$router.push('/') return false
} else {
this.$router.go( - 1)
}
}
}
这样子就能兼容直接打开会话面板页面的场景了。
当然,也可以判断直接打开的时候重定向到其他页面,可以根据需求去调整。
2.同一个用户会话后刷新页面会创建新的会话,被误当成新用户
这个问题是上线之后才发现的。(还好发现得快 🐶 )
当时发现后立刻回滚了,不然客服 MM 的内心肯定崩溃,会有铺天盖地的会话进入客服系统后台。
之所以导致上线后才发现,原因是因为开发过程中,设置了指定的客服测试,当时同一个客服接入会话,用户在刷新时候不会生成新的会话。然而去掉指定客服后,再刷新,或者从其他入口进入后会触发新的会话。
这并不是我们想要的!
为了解决这个问题,去查了文档发现 session_key
这个字段。内心狂喜 🤭,重复建立会话肯定是因为没有标识会话的唯一性质,按照文档,每次初始化之前生产一个 session_key
缓存起来不就OK了,哈哈。😁
but !
重复会话的问题还是会出现。。。
经过对 Udesk 售后客服不断骚扰的骚扰问答,解决无果。。。
最后无奈,找到他们的技术告知是需要标识用户而不是标识会话,我们的场景需要的是区分用户而不是会话 😨 !
再翻一翻文档,内心接着狂喜 😁 也就是之前提到的 session_key
需要替换为 web_token
。
好的,找到了方向就一步步尝试修改,看是否有用。
看了下文档和他们的例子, web_token
是挂载在 udParams
(初始化传入的参数对象)的 customer
属性下。
customer
对象长这个样子:
// SHA1示例
ud({
"customer": {
"nonce": "9ca6fff5a509fb887ac72cf5c92010e7",
"signature": "9B2619225AA6476DC1EB80DBB8801E1575EBE39C",
"timestamp": "1455675719000",
"web_token": "test@udesk.cn"
}
})
其中 signature
需要用到加密算法。
加密算法我选用他默认的 SHA1
,找了一个下载量比较多的轮子 js-sha1
npm
安装后编写生成签名的方法:
import sha1 from 'js-sha1'
// signature加密算法(三步) http://www.udesk.cn/doc/thirdparty/webim/#-_4
getSignature (nonce, timestamp, webToken) {
let signStr = `nonce=${nonce}×tamp=${timestamp}&web_token=${webToken}&${key}`
signStr = sha1(signStr)
signStr = signStr.toUpperCase()
return signStr
}
由于 H5 项目没有用户系统,也就没有用户 ID 之类的标识,为此我们利用随机数+时间戳的方式来生成 webToken
getWebToken (timestamp) {
// 客户唯一标示,推荐使用邮箱、手机号等 仅支持字母、数字及下划线,禁用特殊字符
let webToken
const localSessionkey = localStorage.getItem('UDWebToken')
if (!isEmpty(localSessionkey)) {
webToken = localSessionkey
} else {
webToken = makeRandomStr(6) + timestamp
localStorage.setItem('UDWebToken', webToken)
}
return webToken
},
初始化的时候我们对应修改:
// udesk.js
export default {
init () {
if (!window.ud) {
this.createUdeskScript()
}
},
openUdPanel () {
if (window.ud) {
this.show()
const ud = window.ud
ud('showPanel')
}
},
hideUdPanel () {
if (window.ud) {
const ud = window.ud
ud('hidePanel')
}
},
createUdeskScript () {
(function (a, h, c, b, f, g) {
a.UdeskApiObject = f
a[f] =
a[f] ||
function () {
(a[f].d = a[f].d || []).push(arguments)
}
g = h.createElement(c)
g.async = 1
g.src = b
g.id = useUdeskScriptId
c = h.getElementsByTagName(c)[0]
c.parentNode.insertBefore(g, c)
})(
window,
document,
'script',
'https://assets-cli.udesk.cn/im_client/js/udeskApi.js',
'ud'
)
const ud = window.ud
const Timestamp = new Date().getTime()
const Nonce = makeRandomStr(32)
const Webtoken = this.getWebToken(Timestamp)
const udParams = {
code,
link,
customer: {
nonce: Nonce,
timestamp: Timestamp,
web_token: Webtoken,
signature: this.getSignature(Nonce, Timestamp, Webtoken)
},
... // 其他参数
}
}
ud(udParams)
}
}
改造完之后测试,不管怎么刷新和切换,都不会触发新的会话了。客服MM也不用担心同一个用户浏览一次H5页面开了一堆会话了。 👏 👏 👏
3. 商品咨询对象上传 javascript 接入方式传参有问题
所谓商品咨询对象,是用户从商品详情点击进去 IM 会话面板建立会话,客服能从后台看到用户正在浏览的商品内容。Udsk 提供了一个方法供我们上次咨询对象。
但是使用下来,javascript
接入的方式动态传入参数,Udesk 后台根本捕获不到商品信息,只有写死为字符串的时候才可以,具体原因未知。
尝试使用 url 方式传参
upLoadTrace (data) {
const iframe = document.getElementById('udesk_iframe')
const iframeSrc = iframe.src
const { title, url, imageurl, city, address } = data
const newSrc = iframeSrc +
`&product_title=${encodeURIComponent(title)}` +
`&product_url=${encodeURIComponent(url)}` +
`&product_image=${encodeURIComponent(imageurl)}` +
`&${encodeURIComponent('product_城市')}=${encodeURIComponent(city)}` +
(isEmpty(address) ? '' : `&${encodeURIComponent('product_地址')}=${encodeURIComponent(address)}`)
iframe.src = newSrc
}
4.解决 iframe src 修改后回退需要多次的问题
使用 url 方式传参在后台成功看到咨询的商品对象。但是又带了一个新的问题,通过浏览器返回离开 UdeskPanel
页面时,会出现跳转异常,返回的界面并不是我们之前的页面。
最终得知:
当更改iframe
的 src
属性后,调用router.go(-1),不能实现路由后退上一级,而是将iframe当作一个窗口文档,调用了该窗口文档的 window.history.go(-1)
,并未更改父级项目的路由后退功能。
我们需要用iframe的 window.location.replace
方式去改变 iframe
将访问的内容。
因此上面的 iframe.src = newSrc
需要改为:iframe.contentWindow.location.replace(newSrc)
这样子问题得到了解决。
5.Udesk 并未提供用户发送信息之后的回调
项目需要统计用户发送会话的次数,对推广做深度优化,但是 Udesk 并未提供这个接口,多次尝试发现他们提供的回调处理需要配合 iframe 中的 js 进行postMessage 的交互,单纯的修改初始化后的 js 是不行的。统计需求也就无法实现。好的一方面是给 Udesk 提了工单,他们说正在规划这个功能,希望早日上线吧。
六、总结
果不其然 Udesk 的坑真不少。一方面:Udesk 的文档的易读性真的很差,阅读起来很不方便。另外一方面,Udesk 提供的接口太少,要自定义的话会遇到很多问题。当然,如果直接用 Udesk 提供的基础功能,不额外修改,也还算凑合。
文章完结。感谢阅读,希望对你能有所帮助。
「 tips:转载请注明原文链接:https://juejin.im/post/6858106657416282125/ 」
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: Yalin 原文链接:https://juejin.im/post/6858106657416282125