uniapp 怎么接入在线客服:App、H5、小程序分别怎么做

不想自己搭 IM,又想让 uni-app 里的用户点一下就能联系客服?App 端用 web-view 打开现成的聊天页就行,但 H5 和小程序各有一个坑,这篇一次讲清。

uniapp 接入在线客服,最省事的做法是不装 SDK,直接打开一个现成的托管聊天页:App 端用 <web-view> 组件加载聊天页链接,H5 端改成跳转到这个链接(或者贴网站安装代码),小程序端因为业务域名的限制基本走不通。下面按平台给出能直接用的代码,前后大约 30 分钟。

要点速览

  • uni-app 里加在线客服有三条路:接 IM 的 SDK 自己做界面、自建开源客服系统、打开现成的托管聊天页。只想让用户能找到客服,第三条最快。
  • App 端:新建一个页面,里面只放一个 <web-view :src="聊天页链接">,默认铺满全屏。
  • H5 端:uni-app 会把 web-view 变成 iframe,而不少托管聊天页禁止被嵌入,结果是白屏。改用 location.href 跳转,或在 index.html 里贴网站安装代码。
  • 小程序端:web-view 要求在后台配置业务域名,并把校验文件放到对方域名根目录,第三方聊天页做不到;个人主体的微信小程序也不能用 web-view。
  • 已登录的用户把 external_id 和服务器算的签名放在链接 # 后面,客服就能看到“已验证”的用户,密钥不要放进 App。

uniapp 在线客服有哪几种接法?

uni-app 项目加在线客服,常见的是下面三种做法,区别在于要不要自己写聊天界面、要不要自己管服务器。

做法 要做的事 能覆盖的平台 适合谁
接 IM 云服务的 SDK 或 UIKit 引入 npm 包、按文档配置、自己处理登录和消息推送 App、H5、小程序都能做 客服是产品核心功能、有专职前端
自建开源客服系统 部署服务端和数据库、自己维护升级 看系统本身支持哪些端 有运维能力、对数据位置有硬性要求
打开托管聊天页 拿到一个链接,在 App 里用 web-view 打开 App、H5;小程序基本不行 想当天上线、客服人数不多的小团队

托管聊天页是在线客服服务商提供的一个全屏网页,打开就是一个聊天窗口,客服在服务商的工作台里回复。它和网站右下角的聊天插件是同一套东西,只是没有浮动按钮,整页都是对话。

本文后面以 SealReply 的“在 App 里使用”为例。别家的托管聊天页思路一样,具体参数看他们的文档。

准备:开启聊天页,拿到链接

开始写代码之前,要先在客服工作台里开启聊天页并复制链接,这一步大约 5 分钟。

  1. 注册一个 SealReply 账号(免费使用),在“设置在线客服”里填站点名称和你的网站地址。
  2. 完成域名验证:只有 App、没有网站也没关系,在 站点 → 你的站点 → 域名验证 里给公司域名加一条 DNS TXT 记录就行。没验证的站点不能开启聊天页。
  3. 打开 站点 → 你的站点 → 安装,在 在 App 里使用 卡片里点 开启 App 聊天页。
  4. 复制 聊天页链接,格式是 https://chat.sealreply.com/sk_…,sk_… 是站点的 key。

The Your website step of Set up your chat, with a site name and website address filled in

关于套餐

聊天页所有套餐都能用。App 里的对话和网站上的一样,计入这个站点的每月会话数,在工作台里也和网站访客放在一起。

聊天页上的消息、翻译、聊天前表单、工作时间、外观,都沿用网站插件的设置,不用为 App 再配一遍。这个开关默认是关的:没开启时打开链接只会看到“客服暂时无法打开”,别人拿到你的站点 key 也冒充不了你的客服页。

第 1 步:App 端用 web-view 打开聊天页

uni-app 的 App 端(安卓、iOS)用 <web-view> 组件打开聊天页即可,不需要原生插件。按 uni-app 官方的 web-view 文档,App 端的 web-view 默认铺满全屏、层级高于其他组件,所以最好单独建一个“联系客服”页面,页面里只放它。

在 pages.json 里加一个页面:

{
  "path": "pages/support/support",
  "style": { "navigationBarTitleText": "联系客服" }
}

页面本身(Vue 3 写法):

<!-- pages/support/support.vue -->
<template>
  <web-view v-if="url" :src="url" />
</template>

<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { getChatUrl } from "@/utils/chat";

const url = ref("");

onLoad(async (query) => {
  url.value = await getChatUrl(query.title ? decodeURIComponent(query.title) : "");
});
</script>

v-if 是为了等链接拼好再创建 web-view,避免先加载一次不带用户信息的页面。原生导航栏的返回按钮和安卓的返回键都能直接退出这个页面,聊天页本身没有关闭按钮,也不需要。

导航栏标题被改了?

App 端的 web-view 默认会把导航栏标题换成网页标题。想一直显示“联系客服”,给组件加上 :update-title="false"(HBuilderX 3.3.8 以上支持)。

第 2 步:拼出带用户信息的聊天页链接

聊天页链接可以在 # 后面带上参数,告诉客服这是哪个用户、从哪个页面来。参数全部可选,不登录的 App 直接用原始链接也能聊。

把拼链接的逻辑放进一个工具文件,App 端和 H5 端共用:

// utils/chat.js
const CHAT_URL = "https://chat.sealreply.com/sk_…"; // 换成你的聊天页链接

// 你自己的登录状态,换成项目里实际存用户的地方
function currentUser() {
  return getApp().globalData.user || null;
}

// 向你的服务器要当前用户的签名,没登录就不要
async function fetchIdentityHash() {
  const res = await uni.request({
    url: "https://api.example.com/support/identity-hash",
    header: { Authorization: "Bearer " + uni.getStorageSync("token") },
  });
  return res.statusCode === 200 ? res.data.identity_hash : "";
}

export async function getChatUrl(pageTitle = "") {
  const user = currentUser();
  const params = {};
  if (user) {
    const hash = await fetchIdentityHash();
    if (hash) {
      params.external_id = String(user.id);
      params.identity_hash = hash;
      if (user.name) params.name = user.name;
      if (user.email) params.email = user.email;
    }
  }
  if (pageTitle) params.page_title = pageTitle;
  const fragment = Object.keys(params)
    .map((k) => `${k}=${encodeURIComponent(params[k])}`)
    .join("&");
  return fragment ? `${CHAT_URL}#${fragment}` : CHAT_URL;
}

几个参数的含义:

参数 作用 不填会怎样
external_id 用户在你系统里的 ID,最多 128 个字符 用户是匿名访客
identity_hash 服务器用密钥对 external_id 算出的签名,和 external_id 一起才有效 客服看不到“已验证”
name、email 显示给客服,资料里已有的不会被覆盖 客服只看到访客编号
page_title 用户从哪个页面点进来,比如 订单 #1024 客服看到的来源是 App

每个值都必须用 encodeURIComponent 编码,上面的代码已经处理了。用户名里有空格、#、& 时不编码,后面的参数会被截断。

服务器上怎么算签名?

签名是用“访客身份验证”的密钥对用户 ID 做 HMAC-SHA256,结果用十六进制表示。密钥在 站点 → 你的站点 → 访客身份验证 开启后复制,只放在你的服务器上。Node.js 的接口大约是这样:

import { createHmac } from "node:crypto";

// 登录后才能调用;requireLogin 换成你自己的登录校验
app.get("/support/identity-hash", requireLogin, (req, res) => {
  const hash = createHmac("sha256", process.env.SEALREPLY_IDENTITY_SECRET)
    .update(String(req.user.id))
    .digest("hex");
  res.json({ identity_hash: hash });
});

签名的字符串必须和 App 传的 external_id 完全一样(不去空格、不改大小写)。为什么只传用户 ID 不安全、PHP 和 Python 怎么写,见在线客服怎么识别登录用户。

密钥不要放进 App

安装包可以被解开,放进 App 的密钥等于公开。identity_hash 也像这个用户的聊天密码,拿到它的人能看到他的聊天记录,所以每次打开时现拼链接,不要写进日志或本地存储。

# 后面的内容不会发给任何服务器,聊天页读到以后会马上从地址栏里去掉。用户退出登录或换了账号,下次打开时不带参数(或带新用户的参数)就行,聊天页会换成新的访客,上一个人的对话不会出现。

第 3 步:H5 端为什么白屏,怎么改?

uni-app 编译到 H5 时,<web-view> 会被转成一个 iframe,这一点在官方文档的平台差异里写明了。而 SealReply 的聊天页(以及不少托管聊天页)在响应头里设置了 frame-ancestors 'none',禁止被别的网页嵌入,防止别的网站把你的客服页套成他们自己的。所以同一个支持页,在 App 里正常,到了浏览器里就是一片空白。

H5 端有两种改法,按需选一种:

改法 怎么做 效果
跳转到聊天页 点“联系客服”时 window.location.href = 聊天页链接 和 App 共用一个链接和签名逻辑,改动最少;用户用浏览器的返回键回到应用
贴网站安装代码 在 index.html 里加插件的嵌入代码 页面角落出现聊天按钮,聊天窗口浮在当前页面上,不离开应用

用条件编译把两个端分开,“联系客服”按钮统一调用这个函数:

// utils/support.js
import { getChatUrl } from "@/utils/chat";

export async function openSupport(pageTitle = "") {
  // #ifdef APP-PLUS
  uni.navigateTo({
    url: "/pages/support/support?title=" + encodeURIComponent(pageTitle),
  });
  // #endif

  // #ifdef H5
  window.location.href = await getChatUrl(pageTitle);
  // #endif

  // #ifdef MP
  uni.showToast({ title: "请在 App 或网页版联系客服", icon: "none" });
  // #endif
}

这里用 location.href 而不是 window.open:中间等了一次网络请求,浏览器会把之后的 window.open 当成弹窗拦掉。

改用网站安装代码怎么放?

H5 端想要右下角的聊天按钮,就把网站的嵌入代码放进 H5 的页面模板。Vue 3 项目固定使用项目根目录的 index.html;Vue 2 项目在 manifest.json 的 h5 → template 里指定一个自定义模板文件(见 uni-app 的 manifest 文档)。

<!-- index.html,放在 </body> 之前 -->
<script src="https://cdn.sealreply.com/widget.js" data-site-key="sk_…" async></script>

还有两件事:

  1. 把 H5 部署的域名加进 站点 → 你的站点 → 基本设置 → 允许的域名,插件只在列出的域名上显示。
  2. 已登录的用户,在登录成功后调用 SealReply.identify({ external_id, identity_hash, name, email }),退出登录时调用 SealReply.reset()。不想要默认按钮,嵌入代码加上 data-launcher="none",再在自己的按钮里调用 SealReply.open()。

这条路和普通网站装在线客服完全一样,更多平台的写法见网站怎么添加在线客服。

微信小程序里能用托管聊天页吗?

基本不能。小程序的 web-view 只能打开在后台配置过的业务域名,而配置业务域名要下载一个校验文件、放到这个域名的根目录下,第三方的聊天页域名你没法往上放文件。按微信官方的 web-view 文档,个人主体和海外主体的小程序也不支持 web-view;业务域名还要求完成 ICP 备案。

A phone app and a browser can open the hosted chat page, while a mini program stays locked

所以 uni-app 项目同时出小程序时,常见的分工是:

  • App 和 H5:用托管聊天页,客服在一个工作台里接待。
  • 微信小程序:用小程序自带的客服消息(<button open-type="contact">),在微信的客服工具里回复;或者在小程序里展示一个“复制客服链接”的入口,让用户到浏览器里打开。

上面 openSupport 里 #ifdef MP 的分支就是给小程序留的位置,按你选的方案替换。

第 4 步:上线前在真机上测这 5 项

模拟器和电脑浏览器测不出权限和存储的问题,打包后用一台安卓、一台 iPhone 各走一遍:

  1. 能打开:点“联系客服”,看到聊天窗口,而不是“客服暂时无法打开”。看到这句话就检查开关是否开启、域名是否验证、链接里的 sk_… 是否完整。
  2. 认得回访:发一条消息,退出 App 再进来,还能看到刚才的对话。每次都是新访客,说明本地存储被清了。
  3. 已验证:登录状态下打开,客服在工作台里看到这位用户带“已验证”标记。没有的话多半是签名不对:密钥用错了站点,或签名的 ID 和传的 external_id 不一致。
  4. 发图片:点输入框旁边的“+”选一张图。安卓上选不了,可以改用 plus.runtime.openURL(链接) 在系统浏览器里打开,代价是用户会离开 App。
  5. 发语音:App 要有麦克风权限(安卓在 manifest.json 的 App 权限配置里勾选录音权限,iOS 要填麦克风的用途说明)。不想处理,就到 站点 → 访客功能 的“访客可以发送”里关掉“语音”。

用户离开 App 后,客服回复了怎么提醒?

托管聊天页只在打开时实时收消息,用户关掉以后客服才回复,聊天页本身没法通知他。可以用这两种办法补上:

  • 邮件:客服回复慢的时候,聊天页会请用户留邮箱,回复会发到邮箱。已登录、链接里带了 email 的用户不用再填。具体怎么设置见人不在电脑前,怎么不漏掉网站上的客户。
  • 你自己的推送:订阅 Webhook 的 message.created 事件,收到客服发的消息时,用事件里的访客 ID 查到你的用户 ID,再用 App 自己的推送通知他。Webhook 是付费套餐的功能,转发脚本的写法可以参考网站咨询推送到飞书、钉钉。

常见问题

uniapp 不装 SDK 能接入在线客服吗?

能。用 <web-view> 打开在线客服服务商的托管聊天页就行,不用引入 npm 包或原生插件。SealReply 的聊天页在 站点 → 你的站点 → 安装 → 在 App 里使用 开启,复制链接放进 web-view 即可。只有需要自己设计聊天界面时,才值得接 IM 的 SDK。

uni-app 的 web-view 打开客服页是空白怎么办?

先看是 App 还是 H5。H5 端的 web-view 是 iframe,聊天页禁止被嵌入,所以必然白屏,要改成 location.href 跳转或贴网站安装代码。App 端白屏,多半是链接拼错了,把最终链接打印出来在手机浏览器里打开对比一下。

微信小程序里能接这种在线客服吗?

托管聊天页在微信小程序里基本用不了。小程序的 web-view 要求配置业务域名、把校验文件放到域名根目录,还要 ICP 备案,第三方聊天页的域名都满足不了;个人主体的小程序也不能用 web-view。小程序端建议用微信自带的客服消息。

用户退出登录后,下一个登录的人会看到之前的聊天吗?

不会。退出登录后打开聊天页时不带参数,或者带新用户的 external_id 和签名,聊天页会自动换成新的访客。已登录的用户只能看到自己账号下的对话,换手机登录也能看到。

App 里的客服对话怎么收费?

SealReply 的聊天页所有套餐都能用,App 里的对话和网站上的一样计入站点的每月会话数,不单独收费。免费套餐也能开启,客服席位和会话数的上限按你的套餐。

一个 uni-app 项目同时出 App 和 H5,要建两个站点吗?

不用。同一个站点 key 可以同时用在 App 聊天页和 H5 的网站代码上,所有对话都进同一个工作台。如果想在工作台里分开看,用 page_title 标明来源,比如 App 里传 App · 订单页。

下一步

App 端能聊了以后,建议接着做两件事:给客服装好提醒,别让 App 用户的消息没人回;用 page_title 把订单号、商品名带过去,客服一打开对话就知道用户在问什么。还在比较客服工具的话,可以先看小网站在线客服系统怎么选。