feat(宿主恢复): 换应用上报 last_app + 首屏恢复上次 H5 应用 (v26092201)
需求:小程序里进入某个 H5 应用后退出小程序,再次冷启动要恢复那个应用。 - jumpApp / goArchiveH5 跳转前 reportLastApp():navigator.sendBeacon 上报**净地址** (实时、由浏览器保证投递,不依赖微信 postMessage 的延迟批量派发) - init() 首屏 resumeLastApp():sessionStorage 一次性闸门内 GET /api/mp/public/last-app, 命中就带当前 openid/unionid location.replace 过去 —— 冷启动才恢复, 同一会话切回首页 tab / 宿主 ?t= 重载都不会再跳;replace 不留 history、back 不会来回弹 - 宫格 / 工具卡 / 宝宝来信 / 孕检日历入口补 identifier,便于服务端在架校验 - 抽出 withIdentity(),jumpApp 行为不变(身份仍走 query,不追加 hash) - 版本 v26092009 → v26092201
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>兜爸心选 · 孕育陪伴</title>
|
||||
<!-- V:26092009 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
|
||||
<!-- V:26092201 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
|
||||
<link rel="stylesheet" href="mama-box.css">
|
||||
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
|
||||
<!-- 共享助手:授权/跳商城/位置上报(分享卡片同步,见 docs/h5-auth-standard.md 第四节) -->
|
||||
|
||||
101
js/app.js
101
js/app.js
@@ -3,7 +3,7 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
|
||||
var VER = '26092009';
|
||||
var VER = '26092201';
|
||||
const $ = (s, c) => (c || document).querySelector(s);
|
||||
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
|
||||
|
||||
@@ -150,6 +150,65 @@
|
||||
if (window.wxAuth && window.wxAuth.reportLocation) window.wxAuth.reportLocation({ href: url });
|
||||
}
|
||||
|
||||
/* ================= H5 宿主恢复:冷启动回到上次进入的 H5 应用(2026-09-22) =================
|
||||
需求:在小程序里进入某个 H5 应用后退出小程序,再次进入要恢复那个应用。
|
||||
数据源:box_app GET /api/mp/public/last-app?openid=(Redis,per-openid,7 天 TTL)
|
||||
· 写入 last_app:jumpApp / goArchiveH5 换应用前 reportLastApp()(sendBeacon,实时可靠)
|
||||
· 读取恢复:resumeLastApp() 在 init() 首屏跑一次,命中就 location.replace 过去
|
||||
闸门:sessionStorage 一次 —— 冷启动(web-view 上下文新建)才恢复;
|
||||
同一会话内宿主 onShow 的 ?t= 重载 / 用户切回首页 tab 都不会再跳。
|
||||
⚠️ 用 replace 不用 href:不留 history 条目,back 不会来回弹。
|
||||
⚠️ 只上报/恢复「净地址」(不带 openid/unionid):身份必须按**当前**用户重新注入,
|
||||
绝不能把记录那一刻的身份带过去(服务端也会再 sanitize 一遍兜底)。
|
||||
⚠️ 服务端有 App 表在架校验,所以恢复不到已下架的应用;Redis 挂掉时读到空 → 留在首页。 */
|
||||
const LAST_APP_API = '/api/mp/public/last-app';
|
||||
const RESUME_DONE_KEY = 'dsh_resume_done';
|
||||
|
||||
function urlBase(u) {
|
||||
return String(u || '').split('#')[0].split('?')[0].replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
/* 换应用前上报「即将进入的 H5 应用」。
|
||||
⚠️ sendBeacon:这一步后面紧跟 location.href 整页跳转,普通 fetch 会被取消;
|
||||
sendBeacon 不阻塞导航且由浏览器保证投递(同源 POST,参数走 query,
|
||||
因为 sendBeacon 发的是 text/plain,带 JSON body 会被 FastAPI 拒)。 */
|
||||
function reportLastApp(url, identifier) {
|
||||
try {
|
||||
if (!url) return;
|
||||
const oid = hashParam('openid');
|
||||
if (!oid) return; // 没有身份键,服务端无法落到 per-openid 记录:跳过(不影响跳转)
|
||||
const qs = ['openid=' + encodeURIComponent(oid), 'url=' + encodeURIComponent(url)];
|
||||
if (identifier) qs.push('identifier=' + encodeURIComponent(identifier));
|
||||
const target = LAST_APP_API + '?' + qs.join('&');
|
||||
if (navigator.sendBeacon && navigator.sendBeacon(target)) return;
|
||||
if (window.fetch) { fetch(target, { method: 'POST', keepalive: true }).catch(function () {}); return; }
|
||||
} catch (e) { try { console.warn('[resume] reportLastApp fail:', e); } catch (e2) {} }
|
||||
}
|
||||
|
||||
/* 首屏恢复:读一次 last_app,命中就带着当前身份 replace 过去。 */
|
||||
function resumeLastApp() {
|
||||
try {
|
||||
if (sessionStorage.getItem(RESUME_DONE_KEY)) return; // 同一 web-view 会话只尝试一次
|
||||
const oid = hashParam('openid');
|
||||
if (!oid) return; // 无身份不跳(宿主拿不到 openid 时也不会加载本页)
|
||||
/* 先置闸门再请求:GET 失败/超时也不在本次会话里重试 ——
|
||||
宁可少恢复一次,也不要「用户切回首页 tab 时被突然跳走」。 */
|
||||
sessionStorage.setItem(RESUME_DONE_KEY, '1');
|
||||
fetch(LAST_APP_API + '?openid=' + encodeURIComponent(oid))
|
||||
.then(r => (r.ok ? r.json() : null))
|
||||
.then(d => {
|
||||
const rec = d && d.result;
|
||||
if (!rec || !rec.url) return;
|
||||
if (urlBase(rec.url) === urlBase(location.href)) return; // 防呆:记录指向本页自身就不跳
|
||||
const target = withIdentity(rec.url); // 用当前 openid/unionid 重新注入
|
||||
console.log('[resume] 恢复上次 H5 应用:', rec.identifier || rec.url);
|
||||
reportHostLocation(target); // 让宿主分享卡片同步到目标地址
|
||||
location.replace(target);
|
||||
})
|
||||
.catch(function () {});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function goArchiveH5() {
|
||||
const oid = hashParam('openid');
|
||||
const uid = hashParam('unionid');
|
||||
@@ -158,6 +217,7 @@
|
||||
if (oid) q.push('openid=' + encodeURIComponent(oid));
|
||||
if (uid) q.push('unionid=' + encodeURIComponent(uid));
|
||||
if (q.length) u += '?' + q.join('&');
|
||||
reportLastApp('https://mp.yizhezhe.com/chanjian-rili/', 'chanjianrili');
|
||||
reportHostLocation(u);
|
||||
location.href = u;
|
||||
}
|
||||
@@ -181,12 +241,11 @@
|
||||
for (let i = 0; i < (name || '').length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
|
||||
return PALETTE[h % PALETTE.length];
|
||||
}
|
||||
/* 跳转到其它 H5 应用:身份只走 query(openid/unionid,原 URL 有 query 用 & 续接)。
|
||||
/* 给目标 H5 拼上当前身份(openid/unionid,query 形态)。
|
||||
⚠️ 不再追加 #openid —— 部分应用自带 hash 路由(如 /baobao-yingji),追加 hash 会被当
|
||||
非法路由 → 页面未找到。宿主每次加载/onShow 会重新注入身份兜底;
|
||||
hash 仅保留作为「授权回传结果」(#phone/nickname/avatar)的通道。 */
|
||||
function jumpApp(url) {
|
||||
haptic();
|
||||
function withIdentity(url) {
|
||||
const oid = hashParam('openid');
|
||||
const uid = hashParam('unionid');
|
||||
const q = [];
|
||||
@@ -198,9 +257,16 @@
|
||||
· nginx/后端日志里 __no_identity=1 一眼可查(排查 openid 为空时很快)。
|
||||
排查背景见 docs/mama-box-change-list-20260919.md 与 showIdNotice() 的注释。 */
|
||||
if (!oid && !uid) q.push('__no_identity=1');
|
||||
if (q.length) url += (url.indexOf('?') >= 0 ? '&' : '?') + q.join('&');
|
||||
reportHostLocation(url);
|
||||
location.href = url;
|
||||
return q.length ? url + (url.indexOf('?') >= 0 ? '&' : '?') + q.join('&') : url;
|
||||
}
|
||||
/* 跳转到其它 H5 应用。identifier 可选(来自 App 表),用于 last_app 上报的服务端在架校验;
|
||||
缺省时服务端按 URL 前缀反查,效果一致。 */
|
||||
function jumpApp(url, identifier) {
|
||||
haptic();
|
||||
const target = withIdentity(url);
|
||||
reportLastApp(url, identifier); // 净地址:恢复时身份按当前用户重新注入
|
||||
reportHostLocation(target);
|
||||
location.href = target;
|
||||
}
|
||||
function renderAppGrid(apps) {
|
||||
const grid = $('#appsGrid');
|
||||
@@ -212,12 +278,12 @@
|
||||
⚠️ colorTheme 目前全是 'blue' 颜色名 —— 有图时别拿它当底色,否则清一色蓝。 */
|
||||
const bg = ico ? 'background:#fff' : 'background:' + (a.colorTheme || appTileColor(name));
|
||||
const img = ico ? '<img class="a-ico" src="' + ico + '" alt="" loading="lazy">' : '<span class="a-ico a-ico-letter" style="background:' + bg + '">' + name.slice(0, 1) + '</span>';
|
||||
return '<button class="app ripple" data-url="' + encodeURIComponent(a.url) + '">' + img + name + '</button>';
|
||||
return '<button class="app ripple" data-url="' + encodeURIComponent(a.url) + '" data-id="' + encodeURIComponent(a.identifier || '') + '">' + img + name + '</button>';
|
||||
}).join('');
|
||||
/* 磁贴点击(事件委托,innerHTML 重建后不用重绑) */
|
||||
grid.onclick = function (e) {
|
||||
const t = e.target.closest('.app');
|
||||
if (t && t.dataset.url) jumpApp(decodeURIComponent(t.dataset.url));
|
||||
if (t && t.dataset.url) jumpApp(decodeURIComponent(t.dataset.url), decodeURIComponent(t.dataset.id || ''));
|
||||
};
|
||||
/* 角标「共 N 个」同步真实数量 */
|
||||
const cnt = $('.apps-head.for-all .sec-more');
|
||||
@@ -504,7 +570,7 @@
|
||||
$$('[data-mini]').forEach(b => b.addEventListener('click', () => {
|
||||
const map = { kick: '数胎动', weight: '孕期体重', cal: '孕检日历', curve: '数宫缩', fweight: '胎儿估重' };
|
||||
const app = APP_BY_ID[MINI_APP_MAP[b.dataset.mini]];
|
||||
if (app && app.url) { jumpApp(app.url); return; }
|
||||
if (app && app.url) { jumpApp(app.url, app.identifier); return; }
|
||||
toast('「' + map[b.dataset.mini] + '」即将上线');
|
||||
}));
|
||||
|
||||
@@ -2425,6 +2491,9 @@
|
||||
|
||||
/* ================= 初始化 ================= */
|
||||
function init() {
|
||||
/* 冷启动恢复:读一次 last_app,命中就跳到上次进入的 H5 应用(会话内只尝试一次,
|
||||
见 resumeLastApp)。放在最前面,尽量少闪一下首页。 */
|
||||
resumeLastApp();
|
||||
/* 首屏向小程序宿主上报当前位置:宿主据此解析分享卡片(见 reportHostLocation 说明)。 */
|
||||
if (window.wxAuth && window.wxAuth.reportLocation) window.wxAuth.reportLocation();
|
||||
/* 默认预产期是「今天 + 3 个月」现算出来的,标记里那些
|
||||
@@ -2549,6 +2618,14 @@
|
||||
return;
|
||||
}
|
||||
const base = '/api/mp/public/mama-box/';
|
||||
/* ⚠️ profile 拉取要带上 unionid(2026-09-21 排查「首页个人信息为空」):
|
||||
服务端「先取用户中心」需要 unionid —— 只有 openid 时走本地 openid→unionid
|
||||
映射(Member),映射缺失的老用户(资料全靠 CRMEB 同步来的)会先取失败,
|
||||
预产期/孕周永远预填不上,而第三方应用因为 URL 自带 unionid 一直有数据。
|
||||
与 footprint/today 同款:hash 里有就带上,服务端成功后会回写映射。 */
|
||||
const _pfUnionid = hashParam('unionid');
|
||||
const _pfQs = 'openid=' + encodeURIComponent(openid) +
|
||||
(_pfUnionid ? '&unionid=' + encodeURIComponent(_pfUnionid) : '');
|
||||
/* 并行拉 个人档案 + 孕期状态:
|
||||
- profile:babyName/userName/babySex/dueDate(身份 + 建档字段)
|
||||
- pregnancy:week/day/due 以服务端权威派生为准(避免客户端重复算、口径统一)
|
||||
@@ -2556,7 +2633,7 @@
|
||||
/* 走到这里说明这次加载是带身份的(openid 非空)→ 把上一次的提示收掉 */
|
||||
hideIdNotice();
|
||||
Promise.all([
|
||||
fetch(base + 'profile?openid=' + encodeURIComponent(openid)).then(r => (r.ok ? r.json() : null)),
|
||||
fetch(base + 'profile?' + _pfQs).then(r => (r.ok ? r.json() : null)),
|
||||
fetch(base + 'pregnancy?openid=' + encodeURIComponent(openid)).then(r => (r.ok ? r.json() : null))
|
||||
]).then(function (rs) {
|
||||
const pj = rs[0], pg = rs[1];
|
||||
@@ -2809,7 +2886,7 @@
|
||||
const app = APP_BY_ID['laixin'];
|
||||
const url = (app && app.url) ? app.url : LAIXIN_FALLBACK_URL;
|
||||
if (!url) { toast('「宝宝来信」即将上线'); return; }
|
||||
jumpApp(url);
|
||||
jumpApp(url, (app && app.identifier) || 'laixin');
|
||||
}
|
||||
$('#tabLetter').addEventListener('click', openLetterApp);
|
||||
/* 首页那张「宝宝来信」卡与底 tab 是**同一个入口**(2026-09-20):
|
||||
|
||||
Reference in New Issue
Block a user