wx-h5-demo: 新增场景3 分享深链验证(正确宿主页 + 复现首页 bug 的反面页),README 补验证步骤 (demo v26091001)

This commit is contained in:
2026-09-17 21:34:27 +08:00
parent 138f14e6de
commit f4610db21b
6 changed files with 275 additions and 3 deletions

View File

@@ -8,6 +8,7 @@
| --- | --- | --- | | --- | --- | --- |
| **场景1** | 微信(浏览器)打开 H5 链接 → 跳**小程序授权页**授权 → 授权后小程序用 web-view 加载本 H5(结果在 URL)→ “回到 H5” | `miniapp/scenario1-auth-bridge.*`(授权桥页) | | **场景1** | 微信(浏览器)打开 H5 链接 → 跳**小程序授权页**授权 → 授权后小程序用 web-view 加载本 H5(结果在 URL)→ “回到 H5” | `miniapp/scenario1-auth-bridge.*`(授权桥页) |
| **场景2** | 微信跳到**小程序 web-view 的 H5 应用** → H5 `postMessage` 请求授权 → 宿主弹原生授权面板 → 授权 → 更新 `web-view src`(带结果)→ H5 重载显示 | `miniapp/scenario2-host-page.*`(web-view 宿主页) | | **场景2** | 微信跳到**小程序 web-view 的 H5 应用** → H5 `postMessage` 请求授权 → 宿主弹原生授权面板 → 授权 → 更新 `web-view src`(带结果)→ H5 重载显示 | `miniapp/scenario2-host-page.*`(web-view 宿主页) |
| **场景3** | web-view 里的 H5 **分享给微信好友** → 接收者应打开**发送者当时那一页**(含 hash/SPA 深链),而不是小程序首页 | `miniapp/scenario3-share-webview.*`(✅ 正确宿主页)、`miniapp/scenario3-home-bug.*`(❌ 反面首页,复现 bug) |
> 说明:场景2 的宿主页 `scenario2-host-page` 同时是场景1 授权后 redirectTo 的落地页(从 `globalData` 读结果拼 H5 url)。 > 说明:场景2 的宿主页 `scenario2-host-page` 同时是场景1 授权后 redirectTo 的落地页(从 `globalData` 读结果拼 H5 url)。
@@ -15,10 +16,13 @@
``` ```
wx-h5-demo/ wx-h5-demo/
├─ index.html # 示例 H5(自包含):环境检测(webview/微信浏览器/纯浏览器)、 ├─ index.html # 示例 H5(自包含):环境检测(webview/微信浏览器/纯浏览器)、
│ # 场景1 跳小程序授权 或 场景2 postMessage 授权、读回传参数展示。 │ # 场景1 跳小程序授权 或 场景2 postMessage 授权、读回传参数展示、
│ # 场景3 分享深链验证(正确分享 / 复现旧 bug 两个按钮)。
├─ miniapp/ ├─ miniapp/
│ ├─ scenario1-auth-bridge.wxml/.js # 场景1 小程序授权桥页(launch-weapp/URL Scheme 进入) │ ├─ scenario1-auth-bridge.wxml/.js # 场景1 小程序授权桥页(launch-weapp/URL Scheme 进入)
│ └─ scenario2-host-page.wxml/.js # 场景2 web-view 宿主页(也作场景1 落地页) │ ├─ scenario2-host-page.wxml/.js # 场景2 web-view 宿主页(也作场景1 落地页)
│ ├─ scenario3-share-webview.wxml/.js # 场景3 ✅ 正确:onShareAppMessage(options).webViewUrl 拼深链
│ └─ scenario3-home-bug.wxml/.js # 场景3 ❌ 反面:写死 path 的「首页」,复现「分享跳首页」
└─ README.md └─ README.md
``` ```
@@ -35,6 +39,39 @@ wx-h5-demo/
- **微信浏览器(非 web-view)**:给 `LAUNCH_SCHEME` 填一个**小程序 URL Scheme**(后端用 appid+path 换),或把页面放进公众号/网页用 `<wx-open-launch-weapp path="/pages/demo/auth">`(需 jsapi 签名,并在微信内打开)。 - **微信浏览器(非 web-view)**:给 `LAUNCH_SCHEME` 填一个**小程序 URL Scheme**(后端用 appid+path 换),或把页面放进公众号/网页用 `<wx-open-launch-weapp path="/pages/demo/auth">`(需 jsapi 签名,并在微信内打开)。
- 场景1 能真正走通的前提:**你的小程序里要有 `/pages/demo/auth`(= `miniapp/scenario1-auth-bridge.*`)和 `/pages/demo/webview`(=`scenario2-host-page.*`),并在 `app.json` 注册这两个页面**;否则 navigateTo 会「页面不存在」。 - 场景1 能真正走通的前提:**你的小程序里要有 `/pages/demo/auth`(= `miniapp/scenario1-auth-bridge.*`)和 `/pages/demo/webview`(=`scenario2-host-page.*`),并在 `app.json` 注册这两个页面**;否则 navigateTo 会「页面不存在」。
## 验证分享深链(场景3)· 2026-09
> 症状:web-view 里的 H5 分享给微信好友后,接收者点开卡片落到**小程序首页**,不是对应的 H5 页面。
### 先记住平台约束
- web-view 里的 H5 **不能自己拉起分享面板**;卡片只能由承载 `<web-view>` 的**宿主小程序页**的
`onShareAppMessage` 产出。
- `onShareAppMessage(options)` 的 **`options.webViewUrl`** 才是当前 web-view 的真实地址
(含 H5 的 hash / SPA 路由)—— 这是唯一可靠的深链来源。
- `wx.miniProgram.postMessage` **不是实时投递**,只在「后退 / 组件销毁 / 分享」等时机批量派发,
不能拿它的 `href` 当深链来源。
- **禁止**在 H5 分享时调 `wx.miniProgram.navigateBack()`(旧版 `bridge.share` 为了“触发投递”这么干,
它会把 web-view 页弹掉,转发被上一页接管 → 接收者进首页)。
### A. 在真机 CRMEB 小程序里验证(推荐,无需注册 demo 页)
1. 用 CRMEB 小程序打开:`/pages/annex/web_view/index?url=https%3A%2F%2Fmp.yizhezhe.com%2Fwx-h5-demo%2F`
(或在「我的应用 / 应用宫格」里点 `wx-h5-demo`)。
2. H5 里点 ①「生成唯一深链标记」→ 记录页面地址里的 `#/share-probe/xxxx`。
3. 点 ②「✅ 正确分享」→ 本页应**停留不动** → 点右上角「···」转发。
4. 用另一个微信号点开卡片:应打开**同一个地址**(能看到相同 `#/share-probe/xxxx`),不是首页。
5. 反向对照:重来一次,第 3 步改点 ③「❌ 复现旧 bug:postMessage + navigateBack」,
会立刻退回上一页;此时再转发,接收者就落到那个上一页(bug 复现)。
### B. 用独立 demo 小程序页验证
把 `miniapp/scenario3-home-bug.*`(路径 `/pages/demo/home`)和
`miniapp/scenario3-share-webview.*`(路径 `/pages/demo/share-webview`)并入你的小程序并在 `app.json` 注册,
以 `/pages/demo/home` 为起点:本页 → 进 web-view → H5 点 ③ → 退回本页 → 转发(bug 复现);
改点 ② → 停留 web-view → 转发(接收者进 H5)。对照 `scenario3-share-webview.js` 与
`scenario3-home-bug.js` 两个 `onShareAppMessage` 的差别即可。
> 线上真实实现:`CRMEB pages/annex/web_view/index.vue`(容器)与 `pages/index/index.vue`(首页壳),
> 见 `crmeb/docs/分享图片获取逻辑.md` 第四节。
## 注意 ## 注意
- 手机号:前端只拿 `code`,**明文必须后端** `wx.business.getuserphonenumber` 换(旧 `code2Session`+解密已收紧)。 - 手机号:前端只拿 `code`,**明文必须后端** `wx.business.getuserphonenumber` 换(旧 `code2Session`+解密已收紧)。
- 头像:`e.detail.avatarUrl` 是临时 URL,需 `wx.uploadFile` 上传存后端,回传正式地址。 - 头像:`e.detail.avatarUrl` 是临时 URL,需 `wx.uploadFile` 上传存后端,回传正式地址。

View File

@@ -63,6 +63,24 @@
<div class="row"><span class="k">头像</span><span class="v"><span class="avatar" id="rAvatarWrap"><span id="rAvatarText">宝</span></span></span></div> <div class="row"><span class="k">头像</span><span class="v"><span class="avatar" id="rAvatarWrap"><span id="rAvatarText">宝</span></span></span></div>
</div> </div>
<div class="card">
<h2>场景3 · 分享深链验证(web-view 内 H5 分享)</h2>
<div class="row"><span class="k">当前页面</span><span class="v" id="shCur" style="word-break:break-all;text-align:right;max-width:62%">—</span></div>
<div class="row"><span class="k">深链标记</span><span class="v" id="shMark">(未设置)</span></div>
<button class="btn ghost" id="btnShMark">① 生成唯一深链标记(写入 hash 路由)</button>
<button class="btn main" id="btnShOk">② ✅ 正确分享:只 postMessage(本页不跳)</button>
<button class="btn ghost" id="btnShBad" style="color:#C4553A;border-color:#F0D8D2">③ ❌ 复现旧 bug:postMessage + navigateBack</button>
<p class="hint">
<b>判定标准</b>:点 ② 后本页<b>停留不动</b>,再点右上角「···」转发;接收者点开卡片,
打开的应是上面「当前页面」那个地址(含 hash 标记),<b>而不是小程序首页</b>。<br>
点 ③ 会立即退回上一页(旧版 <code>bridge.share()</code> 的 <code>navigateBack</code> 副作用)——
此时再点「···」转发,卡片会由上一页产出,接收者落到上一页:即线上 bug 的复现。<br>
⚠️ 仅在<b>小程序 web-view</b> 内有效(宿主页需按 <code>onShareAppMessage(options).webViewUrl</code> 拼深链,
见 <code>miniapp/scenario3-share-webview.js</code> / CRMEB <code>pages/annex/web_view</code>)。
微信浏览器/纯浏览器只能看 UI。
</p>
</div>
<p class="hint"> <p class="hint">
本页演示<b>共享授权规范</b>:<code>wx-auth.js</code>(<code>wxAuth.init/requestProfile/goMall/readAuth/isAuthorized</code>)。 本页演示<b>共享授权规范</b>:<code>wx-auth.js</code>(<code>wxAuth.init/requestProfile/goMall/readAuth/isAuthorized</code>)。
授权流程:点「授权登录」→ <code>wx.miniProgram.navigateTo('/pages/annex/auth/index')</code> → 授权流程:点「授权登录」→ <code>wx.miniProgram.navigateTo('/pages/annex/auth/index')</code> →
@@ -99,7 +117,7 @@
/* ============================================================ /* ============================================================
① 页面版本号(YYMMDD + 两位序号)—— 用于排查 webview 是否缓存了旧页面 ① 页面版本号(YYMMDD + 两位序号)—— 用于排查 webview 是否缓存了旧页面
============================================================ */ ============================================================ */
var VER = 26090901; var VER = 26091001;
var verNoEl = el('verNo'); var verNoEl = el('verNo');
if (verNoEl) verNoEl.textContent = VER; if (verNoEl) verNoEl.textContent = VER;
@@ -215,6 +233,26 @@
}; };
} }
// 给 navigateBack 打补丁:旧版 bridge.share 用它「触发 postMessage 投递」,
// 副作用是把 web-view 页弹掉、转发被上一页接管(场景3 要复现的 bug)。
if (window.wx && window.wx.miniProgram && window.wx.miniProgram.navigateBack) {
var _back = window.wx.miniProgram.navigateBack.bind(window.wx.miniProgram);
window.wx.miniProgram.navigateBack = function (o) {
var opt = o || {};
dbg('navigateBack 调用 → delta=' + (opt.delta || 1), 'warn');
var _bfail = opt.fail, _bsucc = opt.success;
opt.fail = function (err) {
dbgErr('navigateBack fail: ' + ((err && err.errMsg) || '未知原因'));
if (_bfail) { try { _bfail(err); } catch (e) {} }
};
opt.success = function (res) {
dbg('navigateBack success(已退回上一页)', 'warn');
if (_bsucc) { try { _bsucc(res); } catch (e) {} }
};
try { _back(opt); } catch (ex) { dbgErr('navigateBack 抛异常: ' + (ex && ex.message ? ex.message : ex)); }
};
}
// 给 postMessage 打补丁:记录每一次调用 + 捕获异常 // 给 postMessage 打补丁:记录每一次调用 + 捕获异常
var origPost = (window.wx && window.wx.miniProgram && window.wx.miniProgram.postMessage) ? window.wx.miniProgram.postMessage.bind(window.wx.miniProgram) : null; var origPost = (window.wx && window.wx.miniProgram && window.wx.miniProgram.postMessage) ? window.wx.miniProgram.postMessage.bind(window.wx.miniProgram) : null;
if (window.wx && window.wx.miniProgram) { if (window.wx && window.wx.miniProgram) {
@@ -241,6 +279,51 @@
if (a.readAuth().avatar) { el('rAvatarWrap').innerHTML = '<img src="' + a.readAuth().avatar + '" alt="头像">'; } if (a.readAuth().avatar) { el('rAvatarWrap').innerHTML = '<img src="' + a.readAuth().avatar + '" alt="头像">'; }
} }
/* ============================================================
④-b 场景3:分享深链验证
------------------------------------------------------------
规则:web-view 里的 H5 **不能**自己拉起分享面板,卡片只能由宿主小程序页
的 onShareAppMessage 产出。正确做法是 H5 只发 postMessage(传标题/图),
宿主用 options.webViewUrl(当前 web-view 真实地址,含 hash/SPA 路由)拼深链。
❌ 旧做法:postMessage 后调 navigateBack「触发投递」——会把 web-view 页弹掉,
转发被上一页接管,接收者落到首页(本页可复现)。
============================================================ */
function shRefresh() {
if (!el('shCur')) return;
el('shCur').textContent = location.href;
var m = (location.hash || '').replace(/^#\/?/, '');
el('shMark').textContent = m || '(未设置)';
}
function shMakeMarker() {
var n = Date.now().toString(36);
location.hash = '#/share-probe/' + n; // 模拟 H5 应用的 hash 深链(webview 不丢 hash)
shRefresh();
dbg('已写入深链标记 → #/share-probe/' + n, 'ok');
wxAuth.toast('深链标记:' + n);
}
function shRequest(useNavigateBack) {
var mp = window.wx && window.wx.miniProgram;
if (!mp || !mp.postMessage) {
dbgErr('不在小程序 web-view(无 wx.miniProgram),无法分享');
wxAuth.toast('请在小程序 web-view 中打开本页');
return;
}
mp.postMessage({ data: {
action: 'share',
title: 'wx-h5-demo · 分享深链验证',
href: location.href,
summary: '接收者应打开本页(含 hash 标记),而不是小程序首页',
imageUrl: 'https://mp.yizhezhe.com/mama-box/assets/resources/report.png'
} });
if (useNavigateBack) {
dbg('❌ 复现旧 bug:postMessage 后调用 navigateBack({delta:1}),会退出本 web-view 页', 'warn');
mp.navigateBack({ delta: 1, fail: function (err) { dbgErr('navigateBack fail: ' + ((err && err.errMsg) || '未知原因')); } });
} else {
dbg('✅ 已请求分享(本页未跳)。请点右上角「···」→ 转发;卡片链接应为:' + location.href, 'ok');
wxAuth.toast('请点右上角「···」→ 转发给朋友');
}
}
/* ============================================================ /* ============================================================
④ 初始化:环境检测 + 读取授权 ④ 初始化:环境检测 + 读取授权
============================================================ */ ============================================================ */
@@ -257,6 +340,14 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
init(); init();
// 场景3:分享深链验证
shRefresh();
window.addEventListener('hashchange', shRefresh);
el('btnShMark').addEventListener('click', function () { dbg('🖱️ 点击「生成深链标记」'); shMakeMarker(); });
el('btnShOk').addEventListener('click', function () { dbg('🖱️ 点击「✅ 正确分享」'); shRequest(false); });
el('btnShBad').addEventListener('click', function () { dbg('🖱️ 点击「❌ 复现旧 bug」'); shRequest(true); });
el('btnAuth').addEventListener('click', function () { el('btnAuth').addEventListener('click', function () {
dbg('🖱️ 点击「授权登录」按钮'); dbg('🖱️ 点击「授权登录」按钮');
if (!(window.wx && window.wx.miniProgram)) { if (!(window.wx && window.wx.miniProgram)) {

View File

@@ -0,0 +1,22 @@
// 场景3-反面(❌ 复现线上 bug):写死分享首页
// 路径:/pages/demo/home.js
// ------------------------------------------------------------
// 用途:扮演「小程序首页」。H5 点「❌ 复现旧 bug」会 postMessage + navigateBack,
// 退回本页;此时用户点右上角「···」转发,触发的是本页的 onShareAppMessage。
// 本页故意写死 path: '/pages/demo/home' 且忽略 options.webViewUrl ——
// 接收者点开卡片就落到本页(= 线上「分享后跳首页」的现象)。
Page({
data: {},
goWebview() {
wx.navigateTo({ url: '/pages/demo/share-webview' });
},
// ❌ 反面:写死首页、不带 H5 地址(旧版 pages/index/index.vue 的写法)
onShareAppMessage() {
return {
title: '兜爸心选 · 首页(bug 复现)',
path: '/pages/demo/home'
};
}
});

View File

@@ -0,0 +1,27 @@
<!-- 场景3-反面(❌):「小程序首页」(复现 bug 用)
路径:/pages/demo/home
用法:把本页并入小程序并在 app.json 注册(路径 /pages/demo/home),
作为进入 demo 的起点(也可临时当成首页)。
验证:
本页「进入 H5 web-view」→ H5 点 ③「复现旧 bug」→ 立刻退回本页 →
点右上角「···」转发 → 接收者落到本页,而不是那个 H5 页面。
对照:把 H5 里改点 ②「正确分享」并重新走一遍,接收者就应落到 H5 页面。 -->
<view style="padding:48rpx 40rpx;text-align:center">
<view style="font-size:36rpx;font-weight:700;color:#2B2320">兜爸心选 · 首页</view>
<view style="font-size:26rpx;color:#6B615B;line-height:1.7;margin-top:16rpx">
本页是 bug 复现用的「首页」:<br/>
<text style="color:#C4553A">onShareAppMessage 写死 path: '/pages/demo/home'</text><br/>
并忽略回调里的 webViewUrl(旧版实现)。
</view>
<button style="margin-top:40rpx;background:#F4593C;color:#fff;border-radius:14rpx" bindtap="goWebview">
进入 H5 web-view(场景3)
</button>
<view style="font-size:24rpx;color:#A79C95;line-height:1.7;margin-top:32rpx;text-align:left">
操作步骤:<br/>
1. 点上面按钮进入 web-view(加载 wx-h5-demo);<br/>
2. H5 里点 ③「❌ 复现旧 bug:postMessage + navigateBack」;<br/>
3. 会立刻退回本页 —— 点右上角「···」转发;<br/>
4. 接收者打开卡片会落到本页(bug 现象)。<br/>
5. 改点 ②「✅ 正确分享」重走一遍,接收者应打开 H5 页面。
</view>
</view>

View File

@@ -0,0 +1,84 @@
// 场景3(✅ 正确实现):web-view 承载 H5 + 分享深链
// 路径:/pages/demo/share-webview.js
// ------------------------------------------------------------
// 关键点:web-view 里的 H5 不能自己拉起分享面板,卡片只能由本页的
// onShareAppMessage 产出。深链必须取微信回调参数
// options.webViewUrl(= 当前 web-view 的真实地址,含 H5 的 hash / SPA 路由),
// 再拼回本页 ?url=...,接收者打开的就是「发送者当时那一页」,而不是首页。
//
// ❌ 反面写法(复现 bug):scenario3-home-bug.js —— 写死 path、忽略 webViewUrl。
// 线上真实实现见 CRMEB:pages/annex/web_view/index.vue、pages/index/index.vue。
const H5_BASE = 'https://mp.yizhezhe.com/wx-h5-demo/';
// 剥掉分享链接里的身份/缓存参数:不能把发送者的 openid 带给接收者,
// 接收者身份由宿主页/容器按自己的 openid 重新注入。
const SHARE_DROP_KEYS = ['openid', 'unionid', 't', '_v', 'phone', 'nickname', 'avatar'];
function dropKeyValuePairs(str, keys) {
return String(str || '')
.split('&')
.filter(p => !!p && keys.indexOf(p.split('=')[0]) === -1)
.join('&');
}
function stripAuthParams(url) {
let out = String(url || '');
const hashIdx = out.indexOf('#');
let base = hashIdx >= 0 ? out.slice(0, hashIdx) : out;
let hash = hashIdx >= 0 ? out.slice(hashIdx + 1) : '';
const qIdx = base.indexOf('?');
let query = qIdx >= 0 ? base.slice(qIdx + 1) : '';
base = qIdx >= 0 ? base.slice(0, qIdx) : base;
query = dropKeyValuePairs(query, SHARE_DROP_KEYS);
hash = dropKeyValuePairs(hash, SHARE_DROP_KEYS);
return base + (query ? '?' + query : '') + (hash ? '#' + hash : '');
}
Page({
data: { url: '', shareTitle: '', shareImageUrl: '' },
onLoad(options) {
// 开启「转发给好友 / 分享到朋友圈」两项菜单
wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] });
// 被分享卡片打开时带 ?url=<encoded 当前 H5 地址>;否则加载默认 H5
let target = H5_BASE;
if (options && options.url) {
try { target = decodeURIComponent(options.url); } catch (e) { target = options.url; }
}
console.log('[scenario3] 加载 H5:', target);
this.setData({ url: target });
},
// H5 点「分享」→ postMessage(action:'share') → 这里只缓存标题/图;
// 链接以 options.webViewUrl 为准(postMessage 投递有延迟,不能当深链来源)。
onMessage(e) {
const data = (e.detail && e.detail.data) || [];
(Array.isArray(data) ? data : [data]).forEach((m) => {
if (!m || m.action !== 'share') return;
console.log('[scenario3] H5 请求分享:', JSON.stringify(m));
if (m.title) this.setData({ shareTitle: m.title });
if (m.imageUrl) this.setData({ shareImageUrl: m.imageUrl });
wx.showToast({ title: '点击右上角 ··· 分享给好友', icon: 'none', duration: 2500 });
});
},
// ✅ 关键:用 options.webViewUrl 拼深链
onShareAppMessage(options) {
const target = (options && options.webViewUrl) || this.data.url;
const path = '/pages/demo/share-webview?url=' + encodeURIComponent(stripAuthParams(target));
console.log('[scenario3] onShareAppMessage → path=' + path);
return {
title: this.data.shareTitle || 'wx-h5-demo · 分享深链验证',
path: path,
imageUrl: this.data.shareImageUrl || ''
};
},
// onShareTimeline 没有回调参数(拿不到 webViewUrl),也不能自定义 path;
// 只能把目标 H5 放进 query,由本页 onLoad 读 ?url= 落地。
onShareTimeline() {
return {
title: this.data.shareTitle || 'wx-h5-demo · 分享深链验证',
query: 'url=' + encodeURIComponent(stripAuthParams(this.data.url))
};
}
});

View File

@@ -0,0 +1,11 @@
<!-- 场景3(✅ 正确):web-view 承载 H5 + 分享深链
路径:/pages/demo/share-webview
用法:把本页并入小程序并在 app.json 注册(路径 /pages/demo/share-webview)。
验证:
小程序「首页」→ 进本页 → H5 点 ②「正确分享」→ 停留本页 →
点右上角「···」转发 → 接收者打开的是同一个 H5 地址(含 hash 标记),不是首页。
说明:深链在 scenario3-share-webview.js 的 onShareAppMessage(options) 里
用 options.webViewUrl 拼出;H5 只负责 postMessage 标题/图。 -->
<web-view wx:if="{{url}}" src="{{url}}" bindmessage="onMessage"></web-view>
<view wx:else style="padding:40rpx;text-align:center;color:#A79C95">正在加载 H5…</view>