当 Vue H5 项目需要接入 Udesk WebIM 网页插件 __Vue.js
发布于 4 年前 作者 banyungong 1744 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

〇、前言

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

初始化可传的常用参数是这些,除了 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:转载请注明原文链接:https://juejin.im/post/6858106657416282125/

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: Yalin 原文链接:https://juejin.im/post/6858106657416282125

回到顶部