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

关于套餐
聊天页所有套餐都能用。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>
还有两件事:
- 把 H5 部署的域名加进 站点 → 你的站点 → 基本设置 → 允许的域名,插件只在列出的域名上显示。
- 已登录的用户,在登录成功后调用
SealReply.identify({ external_id, identity_hash, name, email }),退出登录时调用SealReply.reset()。不想要默认按钮,嵌入代码加上data-launcher="none",再在自己的按钮里调用SealReply.open()。
这条路和普通网站装在线客服完全一样,更多平台的写法见网站怎么添加在线客服。
微信小程序里能用托管聊天页吗?
基本不能。小程序的 web-view 只能打开在后台配置过的业务域名,而配置业务域名要下载一个校验文件、放到这个域名的根目录下,第三方的聊天页域名你没法往上放文件。按微信官方的 web-view 文档,个人主体和海外主体的小程序也不支持 web-view;业务域名还要求完成 ICP 备案。

所以 uni-app 项目同时出小程序时,常见的分工是:
- App 和 H5:用托管聊天页,客服在一个工作台里接待。
- 微信小程序:用小程序自带的客服消息(
<button open-type="contact">),在微信的客服工具里回复;或者在小程序里展示一个“复制客服链接”的入口,让用户到浏览器里打开。
上面 openSupport 里 #ifdef MP 的分支就是给小程序留的位置,按你选的方案替换。
第 4 步:上线前在真机上测这 5 项
模拟器和电脑浏览器测不出权限和存储的问题,打包后用一台安卓、一台 iPhone 各走一遍:
- 能打开:点“联系客服”,看到聊天窗口,而不是“客服暂时无法打开”。看到这句话就检查开关是否开启、域名是否验证、链接里的
sk_…是否完整。 - 认得回访:发一条消息,退出 App 再进来,还能看到刚才的对话。每次都是新访客,说明本地存储被清了。
- 已验证:登录状态下打开,客服在工作台里看到这位用户带“已验证”标记。没有的话多半是签名不对:密钥用错了站点,或签名的 ID 和传的
external_id不一致。 - 发图片:点输入框旁边的“+”选一张图。安卓上选不了,可以改用
plus.runtime.openURL(链接)在系统浏览器里打开,代价是用户会离开 App。 - 发语音: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 把订单号、商品名带过去,客服一打开对话就知道用户在问什么。还在比较客服工具的话,可以先看小网站在线客服系统怎么选。