当 Vue H5 项目需要接入 Udesk WebIM 网页插件

3,990 阅读8分钟

〇、前言

公司 H5 项目需要接入 IM ,其他平台目前已经对接了 Udesk 提供的 IM 服务,H5 也需要保持一致,开发之前早就听安卓和 iOS 同事说 Udesk 的坑很多,源码他们都改了好几版,不知道 H5 的会不会也这样刺激,抱着忐忑的心情准备试一试。

本文记录了一次 Vue 版本 H5 接入 Udesk WebIM 网页插件的踩坑过程,一方面是自己记录一下,另外一方面是希望能帮助到即将踩坑的童鞋,避免踩坑。

一、注册 Udesk

如果没有账号需要进行注册。

Udesk官网:www.udesk.cn

注册之后可以免费向客服获取到试用的账号,自己或者公司的账号要使用 IM 插件的功能需要付费开通权限。

二、使用 IM 插件

这里列举使用的主要几个过程 具体详细过程可参考udesk文本IM对接文档:www.udesk.cn/doc/thirdpa…

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": "客服下班,请留言"
  }
})

初始化可传的常用参数是这些,除了 codelink 必传,其他的可以根据需求选填。

在 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_containerdiv ,结构如下。

idudesk_btnudesk_paneldiv 容器分别是打开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 提供了两个方法:hidePanelshowPanel,分别是隐藏会话面板和打开会话面板。

文档推荐的使用方法: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 的回调方法。检测当前会话面板是否打开,visibletrue 时则是打开状态。

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}&timestamp=${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 页面时,会出现跳转异常,返回的界面并不是我们之前的页面。 最终得知:

当更改iframesrc 属性后,调用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:转载请注明原文链接:juejin.cn/post/685810…