H5 与 APP 原生通信方案 - NativeBridge 实现
概述
在 uni-app 项目中,H5 页面嵌入 APP 的 web-view 时,需要调用原生能力(如微信分享)。本文档总结了一套基于 URL Scheme 拦截 的 NativeBridge 通信方案。
架构原理
javascript
┌─────────────────────────────────────────────────────────────┐
│ APP 壳 (h5-full-webview) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 1. mounted 时注入 window.NativeBridge 到 H5 webview │ │
│ │ 2. overrideUrlLoading 拦截 bridge:// 协议 │ │
│ │ 3. 解析 URL 参数,调用原生 API(如 plus.share) │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↓ evalJS 注入 │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ H5 页面 (webview) │ │
│ │ 1. 调用 window.NativeBridge.share(data) │ │
│ │ 2. Bridge 内部执行 window.location.href = 'bridge://...'│ │
│ │ 3. APP 壳拦截并处理 │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
核心实现
1. APP 壳端(h5-full-webview/index.vue)
1.1 注入 NativeBridge
javascript
mounted() {
// #ifdef APP-PLUS
this.$nextTick(() => {
setTimeout(() => {
const currentWebview = this.$scope.$getAppWebview();
const wv = currentWebview.children()[0];
if (!wv) return;
// 监听 webview 加载完成
wv.addEventListener('loaded', () => {
this.injectNativeBridge();
});
}, 500);
});
// #endif
},
methods: {
injectNativeBridge() {
if (this.bridgeInjected) return; // 防止重复注入
const wv = this.getWebview();
if (!wv) return;
// 通过 evalJS 注入 Bridge 对象
const bridgeJs = `
(function() {
if (window.NativeBridge) return 'already';
window.NativeBridge = {
share: function(data) {
var json = typeof data === 'string' ? data : JSON.stringify(data || {});
window.location.href = 'bridge://share?data=' + encodeURIComponent(json);
}
// 可扩展其他方法:scan, getLocation, etc.
};
return 'ok';
})();
`;
wv.evalJS(bridgeJs, (result) => {
console.log('[NativeBridge] 注入结果:', result);
this.bridgeInjected = true;
});
// 拦截 bridge:// 协议
wv.overrideUrlLoading({ mode: 'reject' }, (e) => {
const url = e.url;
if (url && url.startsWith('bridge://share')) {
this.handleShare(url);
}
// 其他 bridge:// 协议可在此扩展
});
},
handleShare(url) {
try {
const queryStr = url.split('?')[1] || '';
const params = new URLSearchParams(queryStr);
const dataStr = params.get('data');
const shareData = dataStr ? JSON.parse(decodeURIComponent(dataStr)) : null;
if (shareData) {
this.showSharePanel(shareData);
}
} catch (err) {
console.error('[NativeBridge] 解析分享数据失败:', err);
}
},
showSharePanel(shareData) {
const scene = shareData.scene || 'WXSceneSession'; // 默认微信好友
const share = {
content: {
type: 'web',
href: shareData.url,
title: shareData.title,
summary: shareData.content,
thumbs: [shareData.imageUrl],
scene: scene, // WXSceneSession=好友, WXSceneTimeline=朋友圈
},
};
plus.share.getServices((services) => {
const wxService = services.find((s) => s.id === 'weixin');
if (wxService) {
wxService.send(share, () => {
uni.showToast({ title: "分享成功", icon: "none" });
}, (err) => {
uni.showToast({ title: "分享失败", icon: "none" });
});
}
}, (err) => {
uni.showToast({ title: "分享功能不可用", icon: "none" });
});
}
}
2. H5 页面端(teacher-activity/index.vue)
2.1 调用 NativeBridge
javascript
// 显示分享渠道选择弹框
share() {
this.showSharePopup = true;
},
// 用户选择渠道后执行分享
doShare(scene) {
this.showSharePopup = false;
// #ifdef H5
const shareData = {
...this.getShareData(),
scene: scene // WXSceneSession 或 WXSceneTimeline
};
if (window.NativeBridge && window.NativeBridge.share) {
window.NativeBridge.share(shareData);
} else {
uni.showToast({ title: "分享功能暂不可用", icon: "none" });
}
// #endif
},
getShareData() {
return {
url: typeof window !== 'undefined' ? window.location.href : '',
title: "答题赢好礼",
content: "通途控股9月投教主题活动,答题赢好礼~",
imageUrl: "https://d.tongtuzhengxin.com/img/teacher-activity/circle.png",
};
}
2.2 分享弹框 UI
vue
<template>
<!-- 分享渠道弹框 -->
<view class="share-mask" v-if="showSharePopup" @click="showSharePopup = false">
<view class="share-popup" @click.stop>
<view class="share-popup-title">分享到</view>
<view class="share-popup-options">
<view class="share-option" @click="doShare('WXSceneSession')">
<view class="share-icon share-icon-wechat">
<text class="icon-text">微</text>
</view>
<text class="share-label">微信好友</text>
</view>
<view class="share-option" @click="doShare('WXSceneTimeline')">
<view class="share-icon share-icon-moments">
<text class="icon-text">朋</text>
</view>
<text class="share-label">朋友圈</text>
</view>
</view>
<view class="share-popup-cancel" @click="showSharePopup = false">取消</view>
</view>
</view>
</template>
关键要点
1. 为什么用 URL Scheme 而不是 evalJS 回调?
- evalJS 回调在某些环境下不触发(HBuilder 调试模式、特定 Android 版本)
- URL Scheme 是单向通信,更可靠,不依赖回调机制
- 数据通过 URL 参数传递,解析简单直观
2. overrideUrlLoading 的 mode: 'reject'
javascript
wv.overrideUrlLoading({ mode: 'reject' }, callback)
mode: 'reject':阻止 webview 实际导航到该 URL,只触发回调- 这样可以拦截
bridge://而不让页面跳转
3. 防止重复注入
javascript
if (this.bridgeInjected) return;
- HMR(热更新)会导致
loaded事件多次触发 - 用标志位防止重复注入 NativeBridge
4. 防重复触发
H5 端和 APP 端都加了节流:
javascript
// H5 端
if (this._sharePending) return;
this._sharePending = true;
setTimeout(() => { this._sharePending = false; }, 3000);
// APP 端
if (this.sharePending) return;
this.sharePending = true;
setTimeout(() => { this.sharePending = false; }, 3000);
扩展其他原生能力
在 injectNativeBridge 中添加新方法:
javascript
const bridgeJs = `
(function() {
if (window.NativeBridge) return;
window.NativeBridge = {
// 分享
share: function(data) {
var json = typeof data === 'string' ? data : JSON.stringify(data || {});
window.location.href = 'bridge://share?data=' + encodeURIComponent(json);
},
// 扫码(示例)
scan: function() {
window.location.href = 'bridge://scan';
},
// 获取位置(示例)
getLocation: function() {
window.location.href = 'bridge://getLocation';
}
};
})();
`;
在 overrideUrlLoading 中处理:
javascript
wv.overrideUrlLoading({ mode: 'reject' }, (e) => {
const url = e.url;
if (url.startsWith('bridge://share')) {
this.handleShare(url);
} else if (url.startsWith('bridge://scan')) {
this.handleScan();
} else if (url.startsWith('bridge://getLocation')) {
this.handleGetLocation();
}
});
调试技巧
1. 查看注入是否成功
H5 控制台:
javascript
console.log(window.NativeBridge); // 应该输出 {share: ƒ}
2. 查看 URL 拦截
APP 壳日志:
perl
[NativeBridge] 拦截到 URL: bridge://share?data=%7B%22url%22%3A...
3. 常见问题排查
| 问题 | 原因 | 解决 |
|---|---|---|
NativeBridge 不存在 |
注入失败或时机不对 | 检查 loaded 事件是否触发,增加延时 |
evalJS 回调不执行 |
调试环境兼容性问题 | 改用 URL Scheme 方式(本方案) |
| 分享面板不弹出 | 标准基座无微信 SDK | 打自定义基座(包含微信分享模块) |
| 重复触发 | 用户快速点击或 HMR | 加节流标志位 |
总结
这套 NativeBridge 方案的核心思想:
- APP 壳注入 JS 对象 到 H5 webview
- H5 调用对象方法 → 触发
window.location.href = 'bridge://...' - APP 壳拦截 URL → 解析参数 → 调用原生 API
优点:
- 不依赖 evalJS 回调,兼容性好
- 数据通过 URL 传递,简单直观
- 易于扩展其他原生能力
适用场景:
- uni-app APP-PLUS 环境
- H5 页面嵌入 APP web-view
- 需要调用原生能力(分享、扫码、定位等)