文档结构概览
| 章节 | 内容 |
|---|---|
| 一、架构总览 | 通信模型图、三种通信方式对比、核心限制说明 |
| 二、微信小程序 | web-view 配置、JSSDK 引入、postMessage 实时通信三方案(URL轮询/开放标签/实时postMessage)、完整代码 |
| 三、支付宝小程序 | web-view 配置、URL参数通信机制(与微信的关键差异)、完整代码 |
| 四、抖音小程序 | web-view 配置、JSSDK 引入、特有注意事项(分享/视频/激励视频)、完整代码 |
| 五、统一跨平台封装 | H5侧 Bridge 类(平台检测/统一API/URL监听)+ 小程序侧统一ActionHandler |
| 六、通信安全 | 三平台域名白名单配置、HTTPS 要求、签名防伪机制 |
| 七、排坑指南 | 三平台+通用的常见问题表格 |
| 八、能力对照速查表 | 支付/定位/扫码/选图/用户信息/手机号/分享/导航 跨平台对比 |
核心结论:H5 通过 postMessage(微信/抖音)或 URL 参数(支付宝)发起请求,小程序逻辑层执行原生 API,结果通过 URL 参数回传 H5。
H5 嵌入微信 / 支付宝 / 抖音小程序 WebView:调用原生能力完整方案
适用场景:H5 页面通过小程序的
<web-view>组件加载,需要在 H5 侧调用小程序原生能力(如支付、定位、扫码、获取用户信息等)。
目录
- 一、架构总览
- [二、微信小程序 WebView 方案](#二、微信小程序 WebView 方案 "#%E4%BA%8C%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F-webview-%E6%96%B9%E6%A1%88")
- [三、支付宝小程序 WebView 方案](#三、支付宝小程序 WebView 方案 "#%E4%B8%89%E6%94%AF%E4%BB%98%E5%AE%9D%E5%B0%8F%E7%A8%8B%E5%BA%8F-webview-%E6%96%B9%E6%A1%88")
- [四、抖音小程序 WebView 方案](#四、抖音小程序 WebView 方案 "#%E5%9B%9B%E6%8A%96%E9%9F%B3%E5%B0%8F%E7%A8%8B%E5%BA%8F-webview-%E6%96%B9%E6%A1%88")
- 五、统一跨平台封装方案
- 六、通信安全与域名白名单
- 七、常见问题与排坑指南
- 八、完整示例代码
一、架构总览
1.1 基本通信模型
scss
┌──────────────────────────────────────────────────┐
│ 小程序容器 (Native) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 小程序逻辑层 (JS) │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ <web-view> 页面 │ │ │
│ │ │ - bindmessage / onMessage │ │ │
│ │ │ - wx.miniProgram / my. / tt. │ │ │
│ │ └──────────────┬──────────────────────┘ │ │
│ │ │ postMessage │ │
│ │ ┌──────────────▼──────────────────────┐ │ │
│ │ │ H5 WebView 中的网页 │ │ │
│ │ │ - sdk注入: wx.miniProgram.navigateBack │ │
│ │ │ - 引入对应 JSSDK │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ Native API (相机/支付/定位等) │
└──────────────────────────────────────────────────┘
1.2 三种通信方式对比
| 通信方式 | 方向 | 微信 | 支付宝 | 抖音 |
|---|---|---|---|---|
| JSSDK 直接调用 | H5 → 小程序 | wx.miniProgram.* |
my.*(仅小程序逻辑层) |
tt.miniProgram.* |
| postMessage | H5 → 小程序 | wx.miniProgram.postMessage |
不支持(需 URL 参数) | tt.miniProgram.postMessage |
| URL 参数 / hash | H5 → 小程序 | wx.miniProgram.navigateTo |
my.navigateTo(小程序侧) |
tt.miniProgram.navigateTo |
| bindmessage 回调 | 小程序 → H5 | 页面后退/分享/销毁时触发 | ❌ 无直接通道 | 页面后退时触发 |
1.3 核心限制
- 所有平台 :H5 内无法直接调用
wx.requestPayment、my.getLocation等原生 API,必须通过 postMessage 转发到小程序逻辑层执行。 - postMessage 触发时机:微信和抖音的 postMessage 不是实时触发的,而是在特定时机(页面后退、分享、销毁)才由小程序收到。
- 支付宝 :web-view 内的 H5 无法使用
my.*API,只能通过 URL query 参数传递数据。
二、微信小程序 WebView 方案
2.1 web-view 组件基础配置
小程序页面 wxml:
html
<!-- pages/webview/index.wxml -->
<web-view
src="{{url}}"
bindmessage="onWebviewMessage"
bindload="onWebviewLoad"
binderror="onWebviewError">
</web-view>
小程序页面 js:
javascript
// pages/webview/index.js
Page({
data: {
url: ''
},
onLoad(options) {
let url = decodeURIComponent(options.url || '');
url = this.appendQuery(url, { token: wx.getStorageSync('token') });
this.setData({ url });
},
// ★ 核心:接收 H5 postMessage 的回调
onWebviewMessage(e) {
const messages = e.detail.data;
const lastMsg = messages[messages.length - 1];
if (!lastMsg) return;
switch (lastMsg.type) {
case 'getLocation':
this.handleGetLocation();
break;
case 'scanCode':
this.handleScanCode();
break;
case 'requestPayment':
this.handlePayment(lastMsg.payload);
break;
case 'chooseImage':
this.handleChooseImage(lastMsg.payload);
break;
case 'getUserInfo':
this.handleGetUserInfo();
break;
default:
console.warn('Unknown message type:', lastMsg.type);
}
},
handleGetLocation() {
wx.getLocation({
type: 'gcj02',
success: (res) => {
this.reloadWebviewWithParams({
latitude: res.latitude,
longitude: res.longitude
});
},
fail: (err) => {
this.reloadWebviewWithParams({ locationError: err.errMsg });
}
});
},
handleScanCode() {
wx.scanCode({
success: (res) => {
this.reloadWebviewWithParams({ scanResult: res.result });
}
});
},
handlePayment(payload) {
wx.requestPayment({
timeStamp: payload.timeStamp,
nonceStr: payload.nonceStr,
package: payload.package,
signType: payload.signType || 'MD5',
paySign: payload.paySign,
success: () => {
this.reloadWebviewWithParams({ paymentResult: 'success' });
},
fail: (err) => {
this.reloadWebviewWithParams({ paymentResult: 'fail', paymentError: err.errMsg });
}
});
},
handleChooseImage(payload) {
wx.chooseImage({
count: payload.count || 1,
sizeType: payload.sizeType || ['compressed'],
sourceType: payload.sourceType || ['album', 'camera'],
success: (res) => {
this.reloadWebviewWithParams({
tempFilePaths: JSON.stringify(res.tempFilePaths)
});
}
});
},
handleGetUserInfo() {
wx.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
this.reloadWebviewWithParams({
userInfo: JSON.stringify(res.userInfo)
});
}
});
},
reloadWebviewWithParams(params) {
const currentUrl = this.data.url;
const baseUrl = currentUrl.split('?')[0];
const newUrl = this.appendQuery(baseUrl, params);
this.setData({ url: newUrl });
},
appendQuery(url, params) {
const urlObj = url.includes('?') ? url.split('?') : [url, ''];
const existingParams = new URLSearchParams(urlObj[1] || '');
Object.entries(params).forEach(([key, value]) => {
existingParams.set(key, encodeURIComponent(value));
});
return urlObj[0] + '?' + existingParams.toString();
}
});
2.2 H5 侧引入微信 JSSDK
方式一:CDN 引入(推荐)
html
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
方式二:npm 引入
bash
npm install weixin-js-sdk
2.3 H5 侧核心 API 调用
javascript
class WechatMiniProgramBridge {
static isInMiniProgram() {
return new Promise((resolve) => {
if (typeof wx === 'undefined' || !wx.miniProgram) {
resolve(false);
return;
}
wx.miniProgram.getEnv((res) => {
resolve(res.miniprogram === true);
});
});
}
static postMessage(data) {
if (!wx || !wx.miniProgram) {
console.error('Not in WeChat mini-program environment');
return;
}
wx.miniProgram.postMessage({ data });
}
static navigateTo(url) {
wx.miniProgram.navigateTo({ url });
}
static navigateBack(delta = 1) {
wx.miniProgram.navigateBack({ delta });
}
static redirectTo(url) {
wx.miniProgram.redirectTo({ url });
}
static switchTab(url) {
wx.miniProgram.switchTab({ url });
}
static reLaunch(url) {
wx.miniProgram.reLaunch({ url });
}
}
2.4 ★ postMessage 实时通信解决方案(关键)
由于微信小程序的 bindmessage 只在页面后退/分享/销毁时触发,如果需要"实时"获取原生能力结果,有以下方案:
方案 A:URL 参数轮询(最常用,推荐)
javascript
class WechatMessageBridge {
constructor() {
this.callbacks = {};
this.lastUrl = location.href;
this.startPolling();
}
send(type, payload) {
wx.miniProgram.postMessage({ data: { type, payload } });
}
startPolling() {
window.addEventListener('hashchange', () => {
this.parseUrlParams();
});
setInterval(() => {
if (location.href !== this.lastUrl) {
this.lastUrl = location.href;
this.parseUrlParams();
}
}, 500);
}
parseUrlParams() {
const params = new URLSearchParams(location.search);
const result = params.get('nativeResult');
if (result) {
try {
const data = JSON.parse(decodeURIComponent(result));
this.cleanUrlParams();
this.onResult(data);
} catch (e) {
console.error('Parse native result failed:', e);
}
}
}
cleanUrlParams() {
const url = new URL(location.href);
url.searchParams.delete('nativeResult');
history.replaceState(null, '', url.toString());
}
onResult(data) {
if (this.callbacks[data.type]) {
this.callbacks[data.type](data.payload);
}
}
on(type, callback) {
this.callbacks[type] = callback;
}
}
方案 B:JS-SDK 开放标签(微信 7.0.12+ 支持)
html
<wx-open-launch-weapp
id="launch-btn"
username="gh_xxxxxxxx"
path="pages/index/index">
<script type="text/wxtag-template">
<button style="padding:10px 20px;background:#07c160;color:#fff;border:none;border-radius:4px;">
打开小程序
</button>
</script>
</wx-open-launch-weapp>
<script>
document.getElementById('launch-btn').addEventListener('launch', (e) => {
console.log('小程序已打开');
});
document.getElementById('launch-btn').addEventListener('error', (e) => {
console.error('打开失败:', e.detail);
});
</script>
方案 C:实时 postMessage(仅支持基础库 2.16.1+)
javascript
wx.miniProgram.postMessage({
data: { type: 'ping' },
success: () => console.log('Message sent in real-time'),
fail: (err) => console.error('PostMessage failed:', err)
});
2.5 微信 JSSDK 完整能力清单(H5 中可直接调用)
| 能力分类 | API | 说明 |
|---|---|---|
| 页面导航 | wx.miniProgram.navigateTo |
跳转小程序页面 |
wx.miniProgram.navigateBack |
返回上一页 | |
wx.miniProgram.redirectTo |
关闭当前页跳转 | |
wx.miniProgram.reLaunch |
重启小程序 | |
wx.miniProgram.switchTab |
切换 Tab | |
| 环境判断 | wx.miniProgram.getEnv |
判断是否在小程序中 |
| 数据通信 | wx.miniProgram.postMessage |
向小程序发送消息 |
| 开放标签 | <wx-open-launch-weapp> |
H5 中打开小程序页面 |
<wx-open-subscribe> |
H5 中唤起订阅消息 |
注意: 支付、定位、扫码等原生能力必须在小程序逻辑层调用,H5 无法直接使用。
三、支付宝小程序 WebView 方案
3.1 web-view 组件基础配置
小程序页面 axml:
html
<!-- pages/webview/index.axml -->
<web-view
src="{{url}}"
onMessage="onWebviewMessage"
onLoad="onWebviewLoad"
onError="onWebviewError">
</web-view>
小程序页面 js:
javascript
// pages/webview/index.js
Page({
data: { url: '' },
onLoad(query) {
let url = decodeURIComponent(query.url || '');
const token = my.getStorageSync({ key: 'token' }).data;
if (token) {
url = this.appendQuery(url, { token });
}
this.setData({ url });
},
handleNativeAction(action, payload) {
switch (action) {
case 'getLocation':
my.getLocation({
success: (res) => {
this.reloadWebviewWithResult({
type: 'getLocation',
data: { latitude: res.latitude, longitude: res.longitude }
});
},
fail: (err) => {
this.reloadWebviewWithResult({
type: 'getLocation',
error: err.errorMessage
});
}
});
break;
case 'scan':
my.scan({
type: 'qr',
success: (res) => {
this.reloadWebviewWithResult({
type: 'scan',
data: { code: res.code }
});
}
});
break;
case 'tradePay':
my.tradePay({
tradeNO: payload.tradeNO,
success: (res) => {
this.reloadWebviewWithResult({
type: 'tradePay',
data: { resultCode: res.resultCode }
});
},
fail: (err) => {
this.reloadWebviewWithResult({
type: 'tradePay',
error: err.errorMessage
});
}
});
break;
case 'getPhoneNumber':
my.getPhoneNumber({
success: (res) => {
this.reloadWebviewWithResult({
type: 'getPhoneNumber',
data: { response: res.response }
});
}
});
break;
case 'chooseImage':
my.chooseImage({
count: payload.count || 1,
success: (res) => {
this.reloadWebviewWithResult({
type: 'chooseImage',
data: { apFilePaths: res.apFilePaths }
});
}
});
break;
}
},
reloadWebviewWithResult(result) {
const currentUrl = this.data.url;
const baseUrl = currentUrl.split('?')[0];
const encoded = encodeURIComponent(JSON.stringify(result));
const newUrl = this.appendQuery(baseUrl, { nativeResult: encoded });
this.setData({ url: newUrl });
},
appendQuery(url, params) {
const [base, qs] = url.includes('?') ? url.split('?') : [url, ''];
const searchParams = new URLSearchParams(qs);
Object.entries(params).forEach(([k, v]) => searchParams.set(k, v));
return base + '?' + searchParams.toString();
}
});
3.2 H5 侧通信机制
支付宝小程序与微信的关键差异:H5 中无法直接引入 my.* SDK 进行页面跳转或 postMessage。
javascript
class AlipayMiniProgramBridge {
static isInAlipayMiniProgram() {
const ua = navigator.userAgent;
return /AlipayClient/i.test(ua) && /miniProgram/i.test(ua);
}
static requestNative(action, payload = {}) {
const actionParam = `__mpAction=${encodeURIComponent(action)}`;
const payloadParam = `__mpPayload=${encodeURIComponent(JSON.stringify(payload))}`;
const currentUrl = location.href;
const separator = currentUrl.includes('?') ? '&' : '?';
location.href = currentUrl + separator + actionParam + '&' + payloadParam + '&__ts=' + Date.now();
}
static onNativeResult(callback) {
const urlParams = new URLSearchParams(location.search);
const resultStr = urlParams.get('nativeResult');
if (resultStr) {
try {
const result = JSON.parse(decodeURIComponent(resultStr));
callback(null, result);
const cleanUrl = location.href.replace(/[?&]nativeResult=[^&]*/, '');
history.replaceState(null, '', cleanUrl);
} catch (e) {
callback(e, null);
}
}
}
}
3.3 支付宝 web-view 可用能力汇总
| 能力 | 实现方式 | 说明 |
|---|---|---|
| 支付 | URL 参数触发小程序 my.tradePay |
需预先生成 tradeNO |
| 定位 | URL 参数触发小程序 my.getLocation |
结果通过 URL 回传 |
| 扫码 | URL 参数触发小程序 my.scan |
- |
| 获取手机号 | URL 参数触发小程序 my.getPhoneNumber |
加密数据需服务端解密 |
| 选图/拍照 | URL 参数触发小程序 my.chooseImage |
返回临时路径 |
| 获取用户信息 | URL 参数触发小程序 my.getOpenUserInfo |
需用户授权 |
四、抖音小程序 WebView 方案
4.1 web-view 组件基础配置
小程序页面 ttml:
html
<web-view
src="{{url}}"
bindmessage="onWebviewMessage"
bindload="onWebviewLoad"
binderror="onWebviewError">
</web-view>
小程序页面 js:
javascript
Page({
data: { url: '' },
onLoad(options) {
let url = decodeURIComponent(options.url || '');
const token = tt.getStorageSync('token');
if (token) {
url = this.appendQuery(url, { token });
}
this.setData({ url });
},
onWebviewMessage(e) {
const messages = e.detail.data;
const lastMsg = messages[messages.length - 1];
if (!lastMsg) return;
switch (lastMsg.type) {
case 'getLocation':
tt.getLocation({
type: 'gcj02',
success: (res) => {
this.reloadWebviewWithResult({
type: 'getLocation',
data: { latitude: res.latitude, longitude: res.longitude }
});
},
fail: (err) => {
this.reloadWebviewWithResult({
type: 'getLocation',
error: err.errMsg
});
}
});
break;
case 'scanCode':
tt.scanCode({
success: (res) => {
this.reloadWebviewWithResult({
type: 'scanCode',
data: { result: res.result }
});
}
});
break;
case 'requestPayment':
tt.requestPayment({
orderInfo: lastMsg.payload.orderInfo,
service: lastMsg.payload.service,
success: () => {
this.reloadWebviewWithResult({
type: 'requestPayment',
data: { result: 'success' }
});
},
fail: (err) => {
this.reloadWebviewWithResult({
type: 'requestPayment',
error: err.errMsg
});
}
});
break;
case 'chooseImage':
tt.chooseImage({
count: lastMsg.payload.count || 1,
success: (res) => {
this.reloadWebviewWithResult({
type: 'chooseImage',
data: { tempFilePaths: res.tempFilePaths }
});
}
});
break;
case 'getUserInfo':
tt.getUserInfo({
success: (res) => {
this.reloadWebviewWithResult({
type: 'getUserInfo',
data: { userInfo: res.userInfo }
});
}
});
break;
case 'share':
tt.shareAppMessage({
title: lastMsg.payload.title,
desc: lastMsg.payload.desc,
imageUrl: lastMsg.payload.imageUrl,
path: lastMsg.payload.path
});
break;
}
},
reloadWebviewWithResult(result) {
const currentUrl = this.data.url;
const baseUrl = currentUrl.split('?')[0];
const encoded = encodeURIComponent(JSON.stringify(result));
const newUrl = this.appendQuery(baseUrl, { __ttResult: encoded });
this.setData({ url: newUrl });
},
appendQuery(url, params) {
const [base, qs] = url.includes('?') ? url.split('?') : [url, ''];
const searchParams = new URLSearchParams(qs);
Object.entries(params).forEach(([k, v]) => searchParams.set(k, v));
return base + '?' + searchParams.toString();
}
});
4.2 H5 侧引入抖音 JSSDK
CDN 引入:
html
<script src="https://s3.pstatp.com/toutiao/tmajssdk/jssdk-1.0.1.js"></script>
npm 引入:
bash
npm install @douyin-microapp/typings
4.3 H5 侧核心 API 调用
javascript
class DouyinMiniProgramBridge {
static isInMiniProgram() {
const ua = navigator.userAgent;
return /ttminiapp/i.test(ua);
}
static postMessage(data) {
if (typeof tt === 'undefined' || !tt.miniProgram) {
console.error('Not in Douyin mini-program environment');
return;
}
tt.miniProgram.postMessage({ data });
}
static navigateTo(url) {
tt.miniProgram.navigateTo({ url });
}
static navigateBack(delta = 1) {
tt.miniProgram.navigateBack({ delta });
}
static redirectTo(url) {
tt.miniProgram.redirectTo({ url });
}
static reLaunch(url) {
tt.miniProgram.reLaunch({ url });
}
static switchTab(url) {
tt.miniProgram.switchTab({ url });
}
}
4.4 抖音 web-view 特有注意事项
- postMessage 限制:与微信相同,非实时触发,需配合 URL 参数轮询。
- 分享能力:抖音支持 H5 通过 postMessage 触发分享。
- 视频相关 :抖音小程序有丰富的视频能力(
tt.createVideoContext等),但仅在逻辑层可用。 - 激励视频 :
tt.createRewardedVideoAd仅可通过 postMessage 触发。 - URL Scheme 跳转 :支持通过
snssdk1128://scheme 从 H5 唤起抖音 App。 - 实时通信增强(tt ≥ 2.45.0):支持实时 postMessage。
五、统一跨平台封装方案
javascript
class MiniProgramBridge {
static PLATFORM = {
WECHAT: 'wechat',
ALIPAY: 'alipay',
DOUYIN: 'douyin',
BROWSER: 'browser'
};
static _platform = null;
static detectPlatform() {
if (this._platform) return this._platform;
const ua = navigator.userAgent;
if (/micromessenger/i.test(ua)) {
this._platform = /miniprogram/i.test(ua)
? this.PLATFORM.WECHAT
: this.PLATFORM.BROWSER;
return this._platform;
}
if (/alipayclient/i.test(ua)) {
this._platform = /miniProgram/i.test(ua)
? this.PLATFORM.ALIPAY
: this.PLATFORM.BROWSER;
return this._platform;
}
if (/aweme/i.test(ua) || /ttminiapp/i.test(ua)) {
this._platform = /ttminiapp/i.test(ua)
? this.PLATFORM.DOUYIN
: this.PLATFORM.BROWSER;
return this._platform;
}
this._platform = this.PLATFORM.BROWSER;
return this._platform;
}
static isWechatMP() {
const ua = navigator.userAgent;
return /micromessenger/i.test(ua) && /miniprogram/i.test(ua);
}
static isAlipayMP() {
const ua = navigator.userAgent;
return /alipayclient/i.test(ua) && /miniProgram/i.test(ua);
}
static isDouyinMP() {
return /ttminiapp/i.test(navigator.userAgent);
}
// ★ 请求原生能力
static async invokeNative(action, payload = {}) {
const platform = this.detectPlatform();
if (platform === this.PLATFORM.BROWSER) {
return Promise.reject(new Error('Not in mini-program environment'));
}
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
return new Promise((resolve, reject) => {
this._addPendingRequest(requestId, resolve, reject);
const data = { type: action, payload, requestId };
switch (platform) {
case this.PLATFORM.WECHAT:
if (wx && wx.miniProgram) wx.miniProgram.postMessage({ data });
break;
case this.PLATFORM.ALIPAY:
this._triggerAlipayAction(data);
break;
case this.PLATFORM.DOUYIN:
if (tt && tt.miniProgram) tt.miniProgram.postMessage({ data });
break;
}
setTimeout(() => {
this._resolveRequest(requestId, null,
new Error(`Action "${action}" timeout`));
}, 30000);
});
}
static navigateTo(url) {
const platform = this.detectPlatform();
switch (platform) {
case this.PLATFORM.WECHAT:
wx.miniProgram.navigateTo({ url });
break;
case this.PLATFORM.ALIPAY:
location.href = `${location.href.split('?')[0]}?__mpNavigate=${encodeURIComponent(url)}`;
break;
case this.PLATFORM.DOUYIN:
tt.miniProgram.navigateTo({ url });
break;
default:
location.href = url;
}
}
static navigateBack(delta = 1) {
const platform = this.detectPlatform();
switch (platform) {
case this.PLATFORM.WECHAT:
wx.miniProgram.navigateBack({ delta });
break;
case this.PLATFORM.ALIPAY:
history.back();
break;
case this.PLATFORM.DOUYIN:
tt.miniProgram.navigateBack({ delta });
break;
default:
history.back();
}
}
// --- 内部方法 ---
static _pendingRequests = {};
static _addPendingRequest(id, resolve, reject) {
this._pendingRequests[id] = { resolve, reject, timestamp: Date.now() };
}
static _resolveRequest(id, data, error) {
const req = this._pendingRequests[id];
if (!req) return;
delete this._pendingRequests[id];
if (error) req.reject(error);
else req.resolve(data);
}
static _triggerAlipayAction(data) {
const encoded = encodeURIComponent(JSON.stringify(data));
const currentUrl = location.href.split('#')[0];
const separator = currentUrl.includes('?') ? '&' : '?';
location.href = currentUrl + separator + `__mpAction=${encoded}&__ts=${Date.now()}`;
}
// ★ 初始化 URL 参数监听(处理小程序回传的结果)
static initResultListener() {
const parseAndClean = () => {
const params = new URLSearchParams(location.search);
const resultKeys = ['nativeResult', '__ttResult'];
for (const key of resultKeys) {
const resultStr = params.get(key);
if (resultStr) {
try {
const result = JSON.parse(decodeURIComponent(resultStr));
if (result.requestId) {
this._resolveRequest(
result.requestId,
result.data || result,
result.error ? new Error(result.error) : null
);
}
} catch (e) {
console.error('Parse native result failed:', e);
}
const cleanUrl = location.href
.replace(new RegExp(`[?&]${key}=[^&]*`), '')
.replace(/[?&]__ts=\d+/, '');
history.replaceState(null, '', cleanUrl);
return;
}
}
};
parseAndClean();
window.addEventListener('hashchange', parseAndClean);
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
parseAndClean();
}
}, 1000);
}
// --- 便捷方法 ---
static async getLocation() { return this.invokeNative('getLocation'); }
static async scanCode(options = {}) { return this.invokeNative('scanCode', options); }
static async requestPayment(params) { return this.invokeNative('requestPayment', params); }
static async chooseImage(options = {}) { return this.invokeNative('chooseImage', { count: options.count || 1 }); }
static async getUserInfo() { return this.invokeNative('getUserInfo'); }
static async getPhoneNumber() { return this.invokeNative('getPhoneNumber'); }
static async share(options) { return this.invokeNative('share', options); }
}
// H5 页面初始化
MiniProgramBridge.initResultListener();
小程序逻辑层统一处理
javascript
// pages/webview/index.js (微信版为例)
Page({
data: { url: '' },
onLoad(options) {
const url = decodeURIComponent(options.url || '');
this.setData({ url });
},
onWebviewMessage(e) {
const messages = e.detail.data;
const lastMsg = messages[messages.length - 1];
if (!lastMsg) return;
this.handleAction(lastMsg);
},
handleAction(msg) {
const { type, payload, requestId } = msg;
const handlers = {
requestPayment: () => {
wx.requestPayment({
...payload,
success: () => this.responseToH5(requestId, { result: 'success' }, type),
fail: (err) => this.responseToH5(requestId, null, type, err.errMsg)
});
},
getLocation: () => {
wx.getLocation({
type: 'gcj02',
success: (res) => this.responseToH5(requestId, {
latitude: res.latitude,
longitude: res.longitude
}, type),
fail: (err) => this.responseToH5(requestId, null, type, err.errMsg)
});
},
scanCode: () => {
wx.scanCode({
...payload,
success: (res) => this.responseToH5(requestId, {
result: res.result, scanType: res.scanType
}, type),
fail: (err) => this.responseToH5(requestId, null, type, err.errMsg)
});
},
chooseImage: () => {
wx.chooseImage({
count: payload.count || 1,
sizeType: payload.sizeType || ['compressed'],
sourceType: payload.sourceType || ['album', 'camera'],
success: (res) => this.responseToH5(requestId, {
tempFilePaths: res.tempFilePaths
}, type),
fail: (err) => this.responseToH5(requestId, null, type, err.errMsg)
});
},
getUserInfo: () => {
wx.getUserProfile({
desc: '用于完善用户信息',
success: (res) => this.responseToH5(requestId, {
userInfo: res.userInfo
}, type),
fail: (err) => this.responseToH5(requestId, null, type, err.errMsg)
});
},
share: () => {
this.responseToH5(requestId, { shared: true }, type);
}
};
if (handlers[type]) {
handlers[type]();
} else {
console.warn('Unknown action type:', type);
this.responseToH5(requestId, null, type, 'Unknown action type');
}
},
responseToH5(requestId, data, type, error) {
const result = { requestId, type, data, error };
const encoded = encodeURIComponent(JSON.stringify(result));
const currentUrl = this.data.url;
const baseUrl = currentUrl.split('?')[0];
const cleanUrl = baseUrl + (currentUrl.includes('?')
? '?' + currentUrl.split('?')[1]
.replace(/[?&]nativeResult=[^&]*/g, '')
.replace(/^&/, '')
: '');
const separator = cleanUrl.includes('?') ? '&' : '?';
this.setData({ url: cleanUrl + separator + `nativeResult=${encoded}` });
}
});
六、通信安全与域名白名单
6.1 各平台域名白名单配置
| 平台 | 配置位置 | 限制 | 说明 |
|---|---|---|---|
| 微信 | 小程序后台 → 开发 → 开发管理 → 开发设置 → 业务域名 | 最多 200 个,需 HTTPS | web-view 只能加载已配置的业务域名 |
| 支付宝 | 小程序后台 → 开发设置 → H5 域名白名单 | 需 HTTPS | 必须配置才能加载 |
| 抖音 | 小程序后台 → 开发 → 开发设置 → web-view 域名 | 需 HTTPS,需 ICP 备案 | 最多 100 个 |
6.2 安全要点
┌────────────────────────────────────────────────────┐
│ 安全防护层 │
│ │
│ 1. 域名白名单 → 仅允许可信域名在 WebView 加载 │
│ 2. HTTPS → 通信加密,防止中间人攻击 │
│ 3. Token 验证 → 每次调用小程序原生 API 前验证 Token │
│ 4. 参数签名 → postMessage 数据加签防止伪造 │
│ 5. 频率限制 → 防止恶意频繁调用原生能力 │
└────────────────────────────────────────────────────┘
javascript
// H5 侧:添加签名防伪
class SecureBridge {
static signMessage(data, secret) {
const payload = JSON.stringify(data);
const timestamp = Date.now();
const nonce = Math.random().toString(36).slice(2, 10);
const signStr = `${payload}|${timestamp}|${nonce}|${secret}`;
const signature = this._hmacSha256(signStr, secret);
return { ...data, timestamp, nonce, signature };
}
static postMessage(type, payload) {
const data = this.signMessage(
{ type, payload },
window.__MP_SECRET__
);
wx.miniProgram.postMessage({ data });
}
static _hmacSha256(message, secret) {
// 简化示意,实际使用 crypto-js 或 Web Crypto SubtleCrypto
return btoa(message + secret);
}
}
javascript
// 小程序侧:验证签名
function verifyMessage(msg) {
const { type, payload, timestamp, nonce, signature } = msg;
const now = Date.now();
if (Math.abs(now - timestamp) > 5 * 60 * 1000) {
return { valid: false, error: 'Timestamp expired' };
}
const expectedSign = computeSignature(type, payload, timestamp, nonce);
if (signature !== expectedSign) {
return { valid: false, error: 'Signature mismatch' };
}
return { valid: true };
}
七、常见问题与排坑指南
7.1 微信小程序
| 问题 | 原因 | 解决方案 |
|---|---|---|
H5 中 wx.miniProgram 为 undefined |
未引入 JSSDK | 确保 <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"> 正确加载 |
| postMessage 不触发 | 非实时触发 | 配合 URL hash/参数传递实时数据 |
| web-view 白屏 | 域名未配置白名单 | 小程序后台 → 开发设置 → 配置业务域名 |
wx.config 失败 |
JS 接口安全域名未配置 | 公众号后台配置 JS 接口安全域名 |
7.2 支付宝小程序
| 问题 | 原因 | 解决方案 |
|---|---|---|
H5 无法调用 my.* API |
web-view 不支持 H5 调用客户端 API | 通过 URL 参数通信 |
| 支付无回调 | tradePay 仅小程序逻辑层可用 |
H5 通过 URL 触发,结果通过 URL 回传 |
7.3 抖音小程序
| 问题 | 原因 | 解决方案 |
|---|---|---|
tt.miniProgram 未定义 |
JSSDK 未引入 | 确保引入 jssdk-1.0.1.js |
| 域名白名单 | 需 ICP 备案 | 提前完成备案 |
7.4 通用问题
| 问题 | 说明 |
|---|---|
| Cookie 无法跨域 | 小程序 WebView 和浏览器 Cookie 隔离,需通过 URL 参数传 token |
| localStorage 不可靠 | 每次新 WebView 实例可能清空,建议用小程序 Storage |
| 页面刷新丢失状态 | 小程序 reload webview 会导致 H5 重新加载 |
| CORS 跨域 | H5 请求后端 API 需配置 CORS 或 Nginx 反向代理 |
| 调试困难 | 建议使用 vConsole 或各平台 DevTools 远程调试 |
八、跨平台能力对照速查表
| 能力 | 微信 H5 → 小程序 | 支付宝 H5 → 小程序 | 抖音 H5 → 小程序 |
|---|---|---|---|
| 支付 | postMessage → wx.requestPayment |
URL 参数 → my.tradePay |
postMessage → tt.requestPayment |
| 定位 | postMessage → wx.getLocation |
URL 参数 → my.getLocation |
postMessage → tt.getLocation |
| 扫码 | postMessage → wx.scanCode |
URL 参数 → my.scan |
postMessage → tt.scanCode |
| 选图 | postMessage → wx.chooseImage |
URL 参数 → my.chooseImage |
postMessage → tt.chooseImage |
| 用户信息 | postMessage → wx.getUserProfile |
URL 参数 → my.getOpenUserInfo |
postMessage → tt.getUserInfo |
| 手机号 | <button open-type="getPhoneNumber"> |
URL 参数 → my.getPhoneNumber |
postMessage → tt.getPhoneNumber |
| 分享 | postMessage → onShareAppMessage |
小程序侧配置 | postMessage → tt.shareAppMessage |
| 导航跳转 | wx.miniProgram.navigateTo ✅ |
不支持,需 URL 参数告知 | tt.miniProgram.navigateTo ✅ |
| 页面返回 | wx.miniProgram.navigateBack ✅ |
history.back() |
tt.miniProgram.navigateBack ✅ |
附录:URL 参数通信 vs postMessage 对比
| 维度 | URL 参数 | postMessage |
|---|---|---|
| 实时性 | ✅ 实时(页面 reload) | ❌ 延迟触发(微信/抖音) |
| 数据量 | ❌ URL 长度限制(~2KB) | ✅ 无明确限制 |
| 用户体验 | ❌ 页面闪烁/刷新 | ✅ 无感 |
| 复杂度 | ✅ 简单直接 | ❌ 需配合轮询/URL 回传 |
| 微信支持 | ✅ | ✅ |
| 支付宝支持 | ✅(主要方式) | ❌ |
| 抖音支持 | ✅ | ✅ |
| 推荐场景 | 低频操作(支付、授权) | 高频或对体验要求高的场景 |
总结: H5 嵌入小程序 WebView 调用原生能力的核心思路是 "H5 发起请求 → 小程序逻辑层执行 → 结果回传 H5" 。微信和抖音可通过
postMessage+ URL 参数回传实现,支付宝则主要依赖 URL 参数通信。封装统一的 Bridge 层可以大幅简化多平台适配工作。