跳至主要内容

uni-app WebView 上传入口通知

本教程说明如何在 uni-app 的 <web-view> 中嵌入独立访客页,并在访客点击附件入口时 接收一条通知。适用于 uni-app App-vue 和 App-nvue 项目。

这条通知只表示“上传入口被点击”。文件选择、文件读取和上传仍由独立访客页负责, 宿主不能因此再次打开选择器,也不能把它当作系统权限结果。

最小接入步骤

  1. <web-view> 中加载独立访客页,并确认页面已经提供约定的 window.webUni.postMessage 适配器。
  2. App-vue 绑定 @message,App-nvue 按目标基座绑定 @onPostMessage
  3. 校验并记录 visitor-upload-click,保持宿主被动,不要因此再次打开选择器或申请权限。

接入后的时序

访客点击附件入口
→ 独立页发送 { type: "visitor-upload-click" }
→ 独立页继续执行 input.click()
→ WebView / 系统打开文件选择器
→ 独立页处理 change 和上传

使用直接访客页链接,并替换实际的域名和 APP_ID。若要隐藏访客页左上角返回按钮, 可以追加 source=webview

https://page.visitor-chat.com/direct/{你的APP_ID}?source=webview

固定协议

独立页通过约定的 window.webUni 适配器发送消息,必须保留 data 外层封装:

window.webUni?.postMessage({
data: { type: 'visitor-upload-click' },
});

默认 payload 必须是:

{"type":"visitor-upload-click"}

App-vue 通常在 event.detail.data 中收到 payload,部分基座会再包装成数组。消息不传输 文件、凭证、上传结果或系统权限状态。permission 仅为前向兼容预留,当前附件入口不会 发送;如果保留校验,只允许 microphonecameraalbum,未知值直接忽略。

这是尽力而为的同步事件,不需要 ACK。Bridge 不存在或发送异常时,独立页仍必须继续 自己的文件选择流程。

App-vue 接入

在 WebView 上绑定 @message,先校验未知数据再消费:

<template>
<web-view :src="visitorUrl" @message="handleWebViewMessage" />
</template>

<script setup lang="ts">
const visitorUrl = 'https://page.visitor-chat.com/direct/YOUR_APP_ID?source=webview';

type UploadPermission = 'microphone' | 'camera' | 'album';
interface UploadClickMessage {
type: 'visitor-upload-click';
permission?: UploadPermission;
}

const permissions = new Set<UploadPermission>([
'microphone',
'camera',
'album',
]);

function isUploadClickMessage(value: unknown): value is UploadClickMessage {
if (!value || typeof value !== 'object') return false;
const candidate = value as Record<string, unknown>;
return candidate.type === 'visitor-upload-click' &&
(candidate.permission === undefined ||
(typeof candidate.permission === 'string' &&
permissions.has(candidate.permission as UploadPermission)));
}

function normalizeMessages(value: unknown): unknown[] {
if (Array.isArray(value)) {
return value.flatMap((entry) => normalizeMessages(entry));
}
if (value && typeof value === 'object' && !('type' in value) && 'data' in value) {
return normalizeMessages((value as { data: unknown }).data);
}
return [value];
}

function handleWebViewMessage(event: { detail?: { data?: unknown } }) {
const messages = normalizeMessages(event.detail?.data);

for (const message of messages) {
if (!isUploadClickMessage(message)) continue;

// 这里只记录或更新状态,不要调用 uni.chooseImage/chooseFile。
console.info('[visitor] upload click', message);
}
}
</script>

event.detail.data 可能是单个对象,也可能是数组,具体取决于 uni-app 基座版本;示例 项目还会拆开额外的 data 封装。应兼容这些形式,忽略空值和未知 type,不要用宽泛的 类型断言代替校验。

App-nvue 接入

部分 App-nvue 基座使用实时事件 @onPostMessage

下面的处理函数复用上一节 App-vue 示例中的 isUploadClickMessagenormalizeMessages

<template>
<web-view
:src="visitorUrl"
style="flex: 1"
@onPostMessage="handleNvueWebViewMessage"
/>
</template>

<script setup lang="ts">
function handleNvueWebViewMessage(event: { detail?: unknown }) {
const detail = event?.detail;
const rawData =
detail && typeof detail === 'object' && 'data' in detail
? (detail as { data?: unknown }).data ?? detail
: detail;

for (const message of normalizeMessages(rawData)) {
if (isUploadClickMessage(message)) {
console.info('[visitor] upload click', message);
}
}
}
</script>

事件名称、payload 位置和实时性会受 uni-app、HBuilderX 及调试基座版本影响。请以目标 版本的组件类型提示和设备实测为准;H5 浏览器结果不能证明 App-Plus 行为。如果目标 App-vue 基座只在页面后退、销毁或分享等生命周期事件触发 @message,不要把它当作点击 级实时通道,应改用该基座支持的实时消息 API 或改用 App-nvue 接入。

准备 window.webUni

webUni 是项目约定的页面侧适配器,不是浏览器或 uni-app 自动提供的全局 API。请与独立 访客页接入方确认以下一种方式:

  1. App 基座在页面可交互前注入带有 postMessage 的对象;
  2. 页面加载自托管的 DCloud 官方 WebView SDK,在 UniAppJSBridgeReady 后为其 API 建立别名。

适配器示例(SDK 路径仅为示例):

<script src="/vendor/uni.webview.1.5.8.js"></script>
<script>
(() => {
function install() {
if (typeof window.webUni?.postMessage === 'function') return;
const postMessage =
window.uni?.postMessage ?? window.uni?.webView?.postMessage;
if (typeof postMessage !== 'function') return;
window.webUni = {
postMessage: postMessage.bind(
window.uni?.postMessage ? window.uni : window.uni?.webView,
),
};
}

document.addEventListener('UniAppJSBridgeReady', install, { once: true });
install();
})();
</script>

已有 webUni 时不要覆盖,也不要同时实现两套发送逻辑;生产 App-Plus 不要把 window.parent.postMessage fallback 当作通信协议。如果使用 HTML5+ evalJSappendJsFile 注入脚本,只允许注入已批准的独立访客域名。evalJS() 返回不代表页面已经 安装适配器,应使用页面 ready 日志或受控联调消息确认。

Bridge 就绪前发生的点击可能无法通知宿主,但独立页仍必须打开自己的文件输入。如果要求 每次点击都可达,应显式延迟启用附件入口或设计有界队列,不能从本协议中推导出来。

安全与容错

  • 设置 src 前只允许已批准的 HTTPS 访客域名,并继续校验后续页面跳转。
  • App-vue 回调通常没有浏览器 event.origin,因此 App-Plus 主要依赖 URL 和导航白名单 隔离消息来源。
  • H5 模拟还应校验 event.originevent.source;这只是浏览器 fixture,不是 App-Plus 生产通信方案。
  • 解析失败时忽略消息,只记录不含敏感数据的诊断信息。
  • 用户取消选择属于正常分支,不应输出上传错误。
  • 一次真实点击最多消费一条通知;收到通知后绝不能再次打开选择器或申请权限。

本地模拟与验收

uniapp-demo 示例项目包含 App-vue 页面、本地 mock 访客页、payload 归一化、来源校验和 消息记录。进入该项目目录执行:

pnpm install
./node_modules/.bin/uni --host 127.0.0.1 --port 5188

打开命令输出的 H5 地址,点击 mock 访客页中的“选择附件并通知宿主”。预期结果:

  • 宿主新增一条 visitor-upload-click 记录;
  • 默认 JSON 不包含 permission
  • mock 访客页仍执行 HTML <input type="file">
  • 取消选择不会生成错误,也不会撤回已发送的通知。

H5 加载真实访客页时可以配置:

VITE_VISITOR_URL="https://page.visitor-chat.com/direct/YOUR_APP_ID?source=webview" \
./node_modules/.bin/uni --host 127.0.0.1 --port 5188

构建 App-Plus:

./node_modules/.bin/uni build -p app-plus

public/mock-visitor.html 由 H5 开发服务器提供,App-Plus 构建不会自动将它作为本地页 打包。App-Plus 联调请配置可访问的访客 URL,或按项目规则将本地 HTML 放入 hybrid/html / static,并确认目标基座可以加载。

发布前请在目标 iOS 和 Android 真机上验证,并记录系统、HBuilderX、调试基座版本、回调 到达时机、选择器行为和导航白名单结果。H5 构建结果不能替代真机证据。

未覆盖范围

本协议不定义 Native 代替文件选择或上传、相机/相册/麦克风权限申请、上传进度或取消、ACK 以及通用 Native ↔ WebView Bridge。需要这些能力时请单独设计协议,不要扩展 visitor-upload-click 的语义。

参考资料