fix(身份): 宿主页「拿到 openid 才加载 H5」+ 复用 App.vue 并行登录(不重复换 code)
背景(2026-09-20 线上排查):删掉小程序缓存重进时 H5 以访客态加载(孕周 `--`、档案空、
跳应用不带 openid,实测跳授权示例是裸 URL)。服务端证据:该设备只打了 `/api/mp/apps`,
没有任何带 openid 的 profile/pregnancy 调用 —— 宿主那次没注入身份,不是数据丢了。
根因:
1) App.vue 静默授权只处理 token 分支:v2/wechat/silence_auth 对新用户 / 强制绑定手机号返回
{auth_login:1, key, openid, unionid}(无 token),openid 被丢掉;且 res.data.userInfo 缺失时
取 .uid 会抛异常把后续 commit 一起带走。
2) 两个宿主页只从 store.openid / 缓存 OPENID / JWT 读身份,读不到就匿名加载(首页壳不等不重试,
容器页 _finalizeUrl / _fetchOpenidAndLoad 兜底)。
改法:
- App.vue:补「无 token 但有 openid」分支(SET_OPENID + 缓存,unionid 同理);userInfo 判空;
**并把这次静默登录的 promise 挂到 globalData._silenceAuthPromise**(供宿主页复用,见下)。
- mixins/webviewHost.js:新增
· _silenceAuthOpenid()(wx.login → v2/wechat/silence_auth,兼容 auth_login/userInfo/JWT 三种
openid 来源,拿到即落 store + 缓存,带 8s 超时保险丝);
· _awaitHostLogin(timeout)(等 App.vue 那次登录结束,**不重复换 code**)。
- 两个宿主页统一 fail-closed「拿到 openid 才加载」,并且**第 1 次先 await App.vue 的并行登录**、
命中缓存就直接加载;之后才自己取。**网络尝试上限 3 → 2,退避 600×n → 固定 500ms**
(实测:code2session 约 200–300ms,重复换一次就是白花这个钱;最坏等待从 ~3.5s 降到 ~1–2.5s)。
- 首页壳:读不到身份就不设 webUrl(停在小程序自己的加载屏,文案分「取身份中/失败/页面加载失败」三态);
2 次仍失败 → 「未获取到微信身份」+「重新获取身份」。
- 容器页:三条失败分支(有 token 无 openid 的重试失败、无 token 无 openid、onShow 仍无 openid)
全部改走 _ensureIdentityThenLoad();删除本页重复实现的 _fetchOpenidAndLoad;新增兜底屏
(以前 url 为空是白屏)。非可信域(商城自身域名等)不注入身份、照常直接加载。
- utils/util.js:注释里的方法名同步(_fetchOpenidAndLoad → _ensureIdentityThenLoad)。
⚠️ 提交同时包含这两处**此前已存在的未提交 WIP**(同文件、改动交织):pages/annex/web_view/index.vue
的 mixin 重构(handleMessage→handleHostMessage 等 4 处 handler 改名)与 pages/index/index.vue
的两行 tabBar 调整。
⚠️ 刻意 fail-closed:CRMEB silence_auth 整体不可用时会停在失败屏而不是降级加载。
⚠️ 未动 manifest.json 版本号(见另一次改动:已统一为 0.3.6 / 36)。
This commit is contained in:
@@ -21,14 +21,17 @@
|
||||
<view class="mamabox-home">
|
||||
<web-view v-if="webUrl" :src="webUrl" @message="handleHostMessage" @error="onWebviewError"></web-view>
|
||||
|
||||
<!-- 加载中 / 加载失败兜底(web-view 未就绪或加载出错时才渲染) -->
|
||||
<!-- 加载中 / 加载失败兜底(web-view 未就绪或加载出错时才渲染)
|
||||
⚠️ 2026-09-20 起增加「正在获取微信身份」这一态:首页壳**必须拿到 openid 才加载 H5**,
|
||||
拿不到就重试、并停在这一屏 —— 不再匿名加载(匿名加载会让 H5 进访客态:
|
||||
孕周 `--`、档案空,用户看到的是「能用但都是空的」,比多等几秒更糟)。 -->
|
||||
<view v-else class="mamabox-fallback">
|
||||
<view class="mamabox-emoji">{{ loadError ? '🥲' : '🍼' }}</view>
|
||||
<view class="mamabox-emoji">{{ fallbackEmoji }}</view>
|
||||
<view class="mamabox-tip">
|
||||
<view class="mamabox-title">{{ loadError ? '页面加载失败' : '正在加载' }}</view>
|
||||
<view class="mamabox-desc">{{ loadError ? '请检查网络后重试' : '正在为你打开「兜爸心选」' }}</view>
|
||||
<view class="mamabox-title">{{ fallbackTitle }}</view>
|
||||
<view class="mamabox-desc">{{ fallbackDesc }}</view>
|
||||
</view>
|
||||
<button v-if="loadError" class="mamabox-retry" hover-class="mamabox-retry-hover" @tap="retryLoad">重新加载</button>
|
||||
<button v-if="loadError || identityFail" class="mamabox-retry" hover-class="mamabox-retry-hover" @tap="retryLoad">{{ loadError ? '重新加载' : '重新获取身份' }}</button>
|
||||
</view>
|
||||
|
||||
<!-- 小程序原生授权面板(H5 postMessage 请求后显示):手机号/昵称/头像 -->
|
||||
@@ -103,14 +106,34 @@
|
||||
name: 'Index',
|
||||
mixins: [shareCard, webviewHost],
|
||||
computed: {
|
||||
...mapGetters(['isLogin', 'uid', 'cartNum'])
|
||||
...mapGetters(['isLogin', 'uid', 'cartNum']),
|
||||
/* 兜底屏文案:三种状态(取身份中 / 取身份失败 / 页面加载失败)分开说,
|
||||
别让用户对着「正在加载」干等却不知道在等什么。 */
|
||||
fallbackTitle() {
|
||||
if (this.loadError) return '页面加载失败';
|
||||
if (this.identityFail) return '未获取到微信身份';
|
||||
if (this.identityPending) return '正在获取微信身份';
|
||||
return '正在加载';
|
||||
},
|
||||
fallbackDesc() {
|
||||
if (this.loadError) return '请检查网络后重试';
|
||||
if (this.identityFail) return '请检查网络后点下方重试,或退出小程序重新进入';
|
||||
if (this.identityPending) return '马上就好,请稍候(不会加载空数据的页面)';
|
||||
return '正在为你打开「兜爸心选」';
|
||||
},
|
||||
fallbackEmoji() {
|
||||
return (this.loadError || this.identityFail) ? '🥲' : '🍼';
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
webUrl: '', // web-view 地址(硬编码 + #openid/#unionid)
|
||||
requesting: false, // 防重入
|
||||
_shownOnce: false, // 首次展示标记(避免首次 onShow 重复加载)
|
||||
_idAttempts: 0, // 「取到 openid 才加载」的自动重试计数(见 fetchIdentityThenLoad)
|
||||
loadError: false, // web-view 加载失败(显示「重新加载」)
|
||||
identityPending: false, // 正在获取 openid(此期间**不加载 H5**,见 fetchIdentityThenLoad)
|
||||
identityFail: false, // 多次仍未拿到 openid → 停在兜底屏,给「重新获取身份」入口
|
||||
shareInfo: {}, // CRMEB 后台「分享设置」(标题/图,无 App 匹配时兜底)
|
||||
// 授权面板字段由 mixins/webviewHost.js 提供:
|
||||
// showAuth / authNickname / authAvatar / _authCode
|
||||
@@ -157,15 +180,22 @@
|
||||
});
|
||||
return;
|
||||
}
|
||||
// ✅ 实测结论(微信开发者工具):首页的 <web-view> 是原生全屏组件,
|
||||
// 会**盖住**原生 tabBar,所以首页只显示 H5 内部导航(无双条);
|
||||
// 切到商城原生页(分类/购物车/我的)时 tabBar 正常显示,点「首页」tab 可回 H5。
|
||||
// ⇒ 无需 uni.hideTabBar()(隐藏了反而让商城 tab 根页变死胡同),保持现状即可。
|
||||
// ⚠️ 首页必须主动隐藏原生 tabBar(2026-09-18 定论,推翻下面旧结论):
|
||||
// 真机上 web-view **盖不住**原生 tabBar —— 旧结论是在 custom:true、
|
||||
// 原生条根本不渲染的时期测的。H5 自带底部导航 + 原生 tabBar = 双条。
|
||||
// hideTabBar 是全局状态:商城各 tab 页都挂着 pageFooter,由它按后台
|
||||
// 「装修→底部导航」配置自管显隐(启用=hideTabBar+渲染 DIY 底栏,
|
||||
// 禁用=showTabBar 露出原生条兜底),所以这里隐藏不会让商城页变死胡同。
|
||||
// ⚠️ 但严禁在商城页主动 showTabBar 来「配对」——上一轮翻车根因
|
||||
// (showTabBar + DIY 底栏同显 = 商品列表双 tabBar,见 onShow 旧警告)。
|
||||
uni.hideTabBar();
|
||||
this.loadMamaBox();
|
||||
},
|
||||
onShow() {
|
||||
// 通知自定义 tabBar 按当前路由刷新(首页=web-view,不显示 tabBar)
|
||||
uni.$emit('tabbar-update');
|
||||
// 每次回到首页都再压一次 tabBar:hideTabBar 是全局状态,可能被别处
|
||||
// showTabBar 抬回来(如后台停用 DIY 底部导航时商城页会露出原生条兜底)。
|
||||
// (原这行的 uni.$emit('tabbar-update') 随 custom-tab-bar 死代码一并移除)
|
||||
uni.hideTabBar();
|
||||
// 0) 独立授权页(pages/annex/auth/index)返回:读取 __AUTH_RESULT__,
|
||||
// 把手机号/昵称/头像写回 H5 URL 的 hash(授权结果只走 hash)。
|
||||
// ⚠️ 必须在下面「重载 webview」之前处理,否则结果会被 ?t= 重载冲掉。
|
||||
@@ -188,14 +218,22 @@
|
||||
// 以 shareHref(当前 H5)为基址重载,停留在原应用首页。
|
||||
this.loadError = false;
|
||||
const reloadBase = (this.shareHref || MAMA_HOME).split('#')[0];
|
||||
this.webUrl = this.buildWebUrl(reloadBase + '?t=' + Date.now());
|
||||
if (this._hostOpenid()) {
|
||||
this.webUrl = this.buildWebUrl(reloadBase + '?t=' + Date.now());
|
||||
} else {
|
||||
/* ⚠️ 仍无 openid:走同一条「取到才加载」的路(自动重试 3 次 → 兜底屏),
|
||||
不再匿名重载(原因见 fetchIdentityThenLoad 的注释)。 */
|
||||
console.log('[auth-flow] 🔄 onShow: 仍无 openid,走「取到才加载」');
|
||||
this._idAttempts = 0;
|
||||
this.fetchIdentityThenLoad(reloadBase);
|
||||
}
|
||||
} else {
|
||||
this._shownOnce = true;
|
||||
}
|
||||
// ⚠️ 实测:home/商城 之间 programmatic 切换 tabBar(hide/show) 会引发
|
||||
// 「进入商城跳一下 + 商品列表双 tabBar」两个副作用。已回退该方案。
|
||||
// 首页的底部导航由 web-view 自身决定(多数版本会盖住原生 tabBar,
|
||||
// 首页只显示 H5 内部导航),不再手动 hideTabBar。
|
||||
// 历史(勿照旧回退):曾试过「home hide + 商城页 show」双向 programmatic
|
||||
// 切换,因商城页 showTabBar 与 DIY 底栏同显 →「商品列表双 tabBar」+跳一下,
|
||||
// 当时整体回退。2026-09-18 起改为单向:只在首页 hideTabBar,商城页显隐
|
||||
// 完全交给 pageFooter(后台 DIY 配置驱动),不再有任何 showTabBar 配对逻辑。
|
||||
// 购物车角标(tabBar 索引 3 = 购物车),沿用原首页逻辑
|
||||
if (this.cartNum > 0) {
|
||||
uni.setTabBarBadge({
|
||||
@@ -275,9 +313,73 @@
|
||||
if (this.requesting) return;
|
||||
this.requesting = true;
|
||||
this.loadError = false;
|
||||
this.webUrl = this.buildWebUrl(MAMA_HOME);
|
||||
/* ⚠️ 身份兜底(2026-09-20 新增):store.openid / 缓存 OPENID / JWT 三处都读不到时,
|
||||
**先主动取一次**(wx.login → silence_auth,见 mixin 的 _silenceAuthOpenid),
|
||||
拿到再拼 hash 加载 —— 别匿名加载。
|
||||
以前这里直接匿名加载,H5 于是进访客态(孕周 `--`、档案查不到、跳应用不带身份),
|
||||
而且没人补:根因是新用户/未绑手机号分支的 openid 被 App.vue 丢掉 + 首页壳没有
|
||||
容器页那样的 openid 兜底(两条都在 2026-09-20 修,现统一叫 _ensureIdentityThenLoad)。
|
||||
取不到 openid 仍继续加载(保持可用),H5 侧会显示「未获取到微信身份」提示条。 */
|
||||
this.identityFail = false;
|
||||
if (this._hostOpenid()) {
|
||||
this.identityPending = false;
|
||||
this._idAttempts = 0;
|
||||
this.webUrl = this.buildWebUrl(MAMA_HOME);
|
||||
this.requesting = false;
|
||||
return;
|
||||
}
|
||||
/* ⚠️⚠️ 拿不到 openid **就不加载**(2026-09-20 用户定的行为):
|
||||
宁可让用户在这一屏多等几秒(自动重试 3 次),也不匿名加载 H5 ——
|
||||
匿名加载会让 H5 进访客态(孕周 `--`、档案查不到),
|
||||
用户看到「页面能用但全是空」比等一会儿更糟,且事后很难判断是身份问题。 */
|
||||
this._idAttempts = 0;
|
||||
this.fetchIdentityThenLoad(MAMA_HOME);
|
||||
this.requesting = false;
|
||||
},
|
||||
/**
|
||||
* 取到 openid 才加载 H5;自动重试 3 次仍失败 → 停在兜底屏给「重新获取身份」入口。
|
||||
* ⚠️ 每次尝试前都再读一次 _hostOpenid():App.vue 的 onLaunch 静默授权是并行的,
|
||||
* 它先落地的话这里直接命中缓存,不必自己再请求一次。
|
||||
* @param {string} base - 要加载的 H5 基址(首页壳是 MAMA_HOME;onShow 重载时是当前 H5)
|
||||
*/
|
||||
fetchIdentityThenLoad(base) {
|
||||
/* 1) 缓存命中(含 App.vue 并行登录刚落地的)→ 0 等待加载 */
|
||||
if (this._loadWithCachedIdentity(base)) return;
|
||||
this.identityPending = true;
|
||||
this.identityFail = false;
|
||||
const seq = this._idAttempts; // 0 = 还没取过
|
||||
this._idAttempts = seq + 1;
|
||||
const MAX = 2; // ⚠️ 网络尝试上限 2 次(原 3 次,见下方退避说明)
|
||||
console.log('[auth-flow] 🔑 首页壳取 openid(第 ' + this._idAttempts + '/' + MAX + ' 次)');
|
||||
/* 2) 第 1 次**不急着自己请求**:先等 App.vue onLaunch 那次静默登录(同一次 exchange)。
|
||||
它先落地就直接加载 —— 避免两边各换一次 code 白花 200–300ms
|
||||
(微信 code 一次性;实测 code2session 约 200–300ms)。
|
||||
之后的重试才自己 wx.login + 换 openid(_silenceAuthOpenid)。 */
|
||||
const task = (seq === 0) ? this._awaitHostLogin(1500) : this._silenceAuthOpenid();
|
||||
task.then(() => {
|
||||
if (this._loadWithCachedIdentity(base)) return;
|
||||
if (this._idAttempts < MAX) {
|
||||
/* 退避固定 500ms(原来 600×n,最坏 3 次 + 1.8s 退避 ≈ 3.5s 才出失败屏;
|
||||
现在 2 次 + 0.5s ≈ 1.5s 量级) */
|
||||
setTimeout(() => this.fetchIdentityThenLoad(base), 500);
|
||||
return;
|
||||
}
|
||||
this.identityPending = false;
|
||||
this.identityFail = true;
|
||||
console.warn('[auth-flow] ⚠️ ' + MAX + ' 次仍未拿到 openid:停在兜底屏等待重试,**不匿名加载**');
|
||||
});
|
||||
},
|
||||
/* 有缓存身份就直接发起加载;返回 true 表示已经发起(调用方别再取) */
|
||||
_loadWithCachedIdentity(base) {
|
||||
if (!this._hostOpenid()) return false;
|
||||
this.identityPending = false;
|
||||
this.identityFail = false;
|
||||
this._idAttempts = 0;
|
||||
this._shownOnce = true;
|
||||
this.webUrl = this.buildWebUrl(base + (base.indexOf('?') >= 0 ? '&' : '?') + 't=' + Date.now());
|
||||
console.log('[auth-flow] ✅ 首页壳拿到 openid,开始加载 H5');
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* 给 H5 拼 hash 参数(openid 单一身份键 + unionid 可选)。
|
||||
* ⚠️ 只能拼 # 不能拼 ?:微信 web-view 首次加载丢 query(app_03 实测)。
|
||||
@@ -324,7 +426,14 @@
|
||||
this.loadError = false;
|
||||
this.requesting = false;
|
||||
this._shownOnce = true;
|
||||
this.webUrl = this.buildWebUrl(MAMA_HOME + '?t=' + Date.now());
|
||||
this.identityFail = false;
|
||||
/* 有身份直接重载;没有就再走一遍「取到才加载」(重置尝试次数,自动重试 3 次) */
|
||||
if (this._hostOpenid()) {
|
||||
this.webUrl = this.buildWebUrl(MAMA_HOME + '?t=' + Date.now());
|
||||
return;
|
||||
}
|
||||
this._idAttempts = 0;
|
||||
this.fetchIdentityThenLoad(MAMA_HOME);
|
||||
},
|
||||
|
||||
/* 授权完善资料(postMessage → 原生面板 → 共享授权接口 → hash 回传)与
|
||||
|
||||
Reference in New Issue
Block a user