From ab6d693b1eab8119c66fb08a386da4ff5c55cde8 Mon Sep 17 00:00:00 2001 From: Karlcarlo Date: Sat, 10 Oct 2026 01:45:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E5=85=B1=E4=BA=AB=E5=BA=93):=20wx-aut?= =?UTF-8?q?h.js=20=E8=BF=81=E8=87=B3=20/common/wx-auth.js=EF=BC=88box=5Fap?= =?UTF-8?q?p=20=E5=94=AF=E4=B8=80=E6=9D=A5=E6=BA=90=EF=BC=8Cnginx=20?= =?UTF-8?q?=E6=97=A7=E8=B7=AF=E5=BE=84=E5=85=BC=E5=AE=B9=EF=BC=89=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E6=9C=AC=E4=BB=93=E5=88=A0=E5=89=AF=E6=9C=AC?= =?UTF-8?q?=EF=BC=8C=E9=98=B2=E4=B8=8E=20v2=20=E5=88=86=E5=8F=89=20(v26101?= =?UTF-8?q?001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 +- js/wx-auth.js | 340 -------------------------------------------------- 2 files changed, 1 insertion(+), 341 deletions(-) delete mode 100644 js/wx-auth.js diff --git a/index.html b/index.html index 79e1774..37109b1 100644 --- a/index.html +++ b/index.html @@ -14,7 +14,7 @@ - + diff --git a/js/wx-auth.js b/js/wx-auth.js deleted file mode 100644 index 3b17357..0000000 --- a/js/wx-auth.js +++ /dev/null @@ -1,340 +0,0 @@ -/* ============================================================ - wx-auth.js · H5 应用通用授权 + 跳商城 共享助手(各 H5 应用引用即可) - ------------------------------------------------------------ - 依赖:页面引入 - 用法:在 H5 页面里 - - 然后: - wxAuth.init(); // 检测环境 + 读取已授权资料(hash) + 回填 - wxAuth.isAuthorized(); // 是否已授权(手机号/昵称/头像任一非空) - wxAuth.requestProfile(); // 点「授权登录」→ postMessage 通知宿主页 - wxAuth.goMall(); // 点「进商城」→ 跳小程序商城 tabBar 页 - wxAuth.goHome(); // 点「返回首页」→ 回「后台配置的首页」(带身份;别裸写 location.href) - wxAuth.identity(); // 读 openid/unionid(URL 优先 → 同源缓存兜底) - wxAuth.watchLocation(); // 换页/路由切换自动上报当前位置 → 宿主刷新分享卡片(推荐) - wxAuth.reportLocation({title}); // 手动上报一次当前页(标题/图可选) - 分享卡片同步协议(location/share/setTitle/copyLink/requestProfile)见 - box_app/docs/h5-auth-standard.md 第四节 - 回传协议:宿主页授权成功后更新 web-view src 为 - ?t=#openid=..&phone=..&nickname=..&avatar=.. - (微信 webview 丢 query、不丢 hash,故授权结果放 hash) - ============================================================ */ -(function (g) { - 'use strict'; - function readHash(name) { - try { - var h = location.hash.slice(1) + '&' + location.search.slice(1); - var m = h.match(new RegExp('(?:^|[&#])' + name + '=([^&#]*)(?:$|[&#])')); - return m ? decodeURIComponent(m[1]) : ''; - } catch (e) { return ''; } - } - - /* ============================================================ - 同源身份缓存 —— openid / unionid 的兜底通道(2026-09-22) - - 为什么需要:H5 应用里「返回首页」这类跳转是**页面自己发起的同源跳转**, - 宿主的 属性不变,所以宿主那条 _applyIdentity() 注入**不会触发**, - 目标页(mama-box)拿到的 URL 里没有 openid → 只能显示「身份不完整」。 - ⚠️ 而且这一跳**永远不可能带上身份**:孕检日历那类应用会把 openid/phone - 从自己的 URL 上主动擦掉(防分享链接冒名顶替,理由见它的注释)—— - 所以对第三方应用来说,这条缓存是**不改它们代码的唯一出路**。 - - 做法:mp.yizhezhe.com 下所有应用与 mama-box 是**同源**(实测在架 12 个应用 - 全在该域名),共用一份 sessionStorage。宿主注入过的身份顺手存一份, - 读取时 **URL 优先 → 缓存兜底**。 - - 三条纪律(改这个文件前先读): - ① **URL 永远权威**:只要 URL 里带 openid 就无条件覆盖缓存 —— - 绝不做「有缓存就不读 URL」,否则切号/换人后会串号; - ② **只在本 origin 内有效**:App 表允许填外链,外链应用读不到这份缓存, - 所以「跳转要带身份」的规范不能删,缓存只是兜底; - ③ **只存 openid/unionid**(服务端身份键),不碰 token / 手机号 / 昵称头像。 - 存 sessionStorage 而不是 localStorage:报告的场景都在同一次会话内, - sessionStorage 够用,且随标签页结束就没了,暴露窗口小得多。 - ============================================================ */ - var ID_KEY = 'mmbox.identity'; - var ID_TTL = 12 * 3600 * 1000; // 12h:sessionStorage 本就随会话结束,这层防的是长驻旧值 - function rememberIdentity(oid, uid) { - if (!oid) return; - try { - sessionStorage.setItem(ID_KEY, JSON.stringify({ openid: oid, unionid: uid || '', at: Date.now() })); - } catch (e) { /* 隐私模式等:静默,不影响主流程 */ } - } - function cachedIdentity() { - try { - var c = JSON.parse(sessionStorage.getItem(ID_KEY) || 'null'); - if (!c || !c.openid) return null; - if (c.at && (Date.now() - c.at) > ID_TTL) return null; - return c; - } catch (e) { return null; } - } - /* 读身份:URL 优先 → 缓存兜底。命中缓存时打一条日志(口径见接入规范第 5 节: - 缓存会掩盖「宿主没注入身份」这个本来会显性提示的信号,所以留痕便于排查)。 */ - function identity() { - /* ⚠️ 与 mama-box 的 hashParam 同款护栏:历史 bug 会把缓存戳粘进 openid - (abc001?t=123),真 openid 不含 ?,剥掉以兼容被污染的旧链接。 */ - var oid = String(readHash('openid') || '').split('?')[0]; - var uid = String(readHash('unionid') || '').split('?')[0]; - if (oid) { rememberIdentity(oid, uid); return { openid: oid, unionid: uid, from: 'url' }; } - var c = cachedIdentity(); - if (c) { - try { - console.log('[wxAuth] URL 未带身份 → 用同源缓存兜底:openid=' + c.openid + - (c.unionid ? ' unionid=' + c.unionid : '') + '(写入于 ' + new Date(c.at).toLocaleTimeString() + ')'); - } catch (e) {} - return { openid: c.openid, unionid: c.unionid || '', from: 'cache' }; - } - return { openid: '', unionid: '', from: 'none' }; - } - /* 载入即采一次:**接入方不需要改任何代码** —— 只要页面引了本库, - 宿主注入过的身份就会被存下来(孕检日历那类第三方应用正是靠这一条自己存下身份的)。 */ - (function captureAtLoad() { try { identity(); } catch (e) {} })(); - - var wxAuth = { - MODE: 'browser', // 'webview' | 'wechat-browser' | 'browser' - inWebview: false, - onReady: null, - /* ① 环境检测 + 读回传资料(授权后宿主更新 src,本页重载读到 hash) */ - init: function (cb) { - this.onReady = cb || null; - var self = this; - var isWechat = /MicroMessenger/i.test(navigator.userAgent); - function setMode(m) { - self.MODE = m; self.inWebview = (m === 'webview'); - if (self.onReady) self.onReady(self); - } - if (window.wx && window.wx.miniProgram) { - var responded = false; - window.wx.miniProgram.getEnv(function (r) { - responded = true; - setMode(r && r.miniprogram ? 'webview' : (isWechat ? 'wechat-browser' : 'browser')); - }); - setTimeout(function () { if (!responded) setMode(isWechat ? 'wechat-browser' : 'browser'); }, 800); - } else { - setMode(isWechat ? 'wechat-browser' : 'browser'); - } - }, - /* ② 读取授权结果(hash 里的 phone/nickname/avatar) */ - readAuth: function () { - return { phone: readHash('phone'), nickname: readHash('nickname'), avatar: readHash('avatar') }; - }, - /* ③ 是否已授权 */ - isAuthorized: function () { - var a = this.readAuth(); - return !!(a.nickname || a.avatar || a.phone); - }, - /* ④ 请求授权:navigateTo → 小程序端独立授权页(默认 pages/annex/auth/index)。 - 改动说明:微信 web-view 的 postMessage 非实时送达,点击时端上收不到; - 改用 wx.miniProgram.navigateTo 实时跳转到授权页,避开该限制。 */ - /* ④ 请求授权:环境判定(与 goMall/mama-box 同一套): - ① UA 不含 MicroMessenger = 普通浏览器 → 直接提示(秒出,不等超时); - ② 微信内 → getEnv 确认是否 webview(纯浏览器加载了 jweixin 时 getEnv - 不回调,800ms 超时兜底)→ 是小程序才 navigateTo 授权页。 */ - requestProfile: function (authPage) { - var self = this; - function nav() { - /* 把「发起授权的当前 H5 地址」经 navigateTo query 带给授权页(backUrl)—— - 与 openid 同一条可靠通道(navigateTo onLoad 必达),不依赖 postMessage - (微信只在后退/销毁/分享时批量派发,时序不可靠:宫格 location.href 换应用后, - 宿主记的 shareHref 可能还是上一个应用的地址,授权结果被送回错的地方)。 - 授权页把 backUrl 写进 __AUTH_RESULT__,宿主 onShow 优先用它做回传基址: - 从哪个 H5 触发的授权,授权后就回到哪个 H5。 */ - var url = authPage || '/pages/annex/auth/index'; - var backUrl = ''; - try { backUrl = location.href; } catch (e) {} - if (backUrl) { - url += (url.indexOf('?') !== -1 ? '&' : '?') + 'backUrl=' + encodeURIComponent(backUrl); - } - window.wx.miniProgram.navigateTo({ - url: url, - /* 页面不存在 / 页面栈超限(>10层) 时 navigateTo 是静默失败 —— 必须自己兜住并提示, - 否则用户只看到「正在打开授权页…」后再无反应,无从排查。 */ - fail: function (err) { - var m = (err && err.errMsg) || '未知原因'; - self.toast('打开授权页失败:' + m); - try { console.warn('[wxAuth] navigateTo fail:', m); } catch (e) {} - } - }); - self.toast('正在打开授权页…'); - } - if (!/MicroMessenger/i.test(navigator.userAgent)) { - this.toast('请在小程序中完成授权登录'); - return; - } - if (window.wx && window.wx.miniProgram) { - var responded = false; - window.wx.miniProgram.getEnv(function (r) { - responded = true; - if (r && r.miniprogram) nav(); - else self.toast('请在小程序中完成授权登录'); - }); - setTimeout(function () { - if (!responded) self.toast('请在小程序中完成授权登录'); - }, 800); - } else { - this.toast('请在小程序中完成授权登录'); - } - }, - /* ⑤ 跳小程序商城(tabBar 页用 switchTab;非 tabBar 用 navigateTo) */ - /* ⑤ 跳商城:环境判定与 requestProfile 同一套 —— 仅小程序 webview 内 switchTab, - UA 非微信/微信内非 webview(或 getEnv 不回调)都提示「请在小程序中打开商城」。 - 默认落点 260918 起为「商城」tab(/pages/index/mall,原装装修首页恢复页), - 与 app.js 的 goMall() 保持一致;此前是分类页 /pages/goods_cate/goods_cate。 */ - goMall: function (url) { - var self = this; - function go() { - window.wx.miniProgram.switchTab({ - url: url || '/pages/index/mall', - fail: function (err) { - var m = (err && err.errMsg) || '未知原因'; - self.toast('打开商城失败:' + m); - try { console.warn('[wxAuth] switchTab fail:', m); } catch (e) {} - } - }); - } - if (!/MicroMessenger/i.test(navigator.userAgent)) { - this.toast('请在小程序中打开商城'); - return; - } - if (window.wx && window.wx.miniProgram) { - var responded = false; - window.wx.miniProgram.getEnv(function (r) { - responded = true; - if (r && r.miniprogram) go(); - else self.toast('请在小程序中打开商城'); - }); - setTimeout(function () { - if (!responded) self.toast('请在小程序中打开商城'); - }, 800); - } else { - this.toast('请在小程序中打开商城'); - } - }, - /* ⑥ 读当前身份(openid/unionid):URL 优先 → 同源缓存兜底。 - 返回 {openid, unionid, from:'url'|'cache'|'none'}。 - 用途:本页自己也需要 openid 时(请求后端、拼跳转链接)统一走它, - 顺带把身份写进缓存 —— 别在各应用里各写一份读参数的代码。 */ - identity: identity, - /* 手动把身份写进缓存(一般不用调:URL 带身份时 identity() 已自动写)。 - 场景:授权回传后身份只在 hash 里出现一次,若随后要清 URL(防分享泄露), - 可先调它存一份。 */ - rememberIdentity: rememberIdentity, - /* ⑦ 回「后台配置的首页」(H5 方式)。带身份:URL 优先,其次缓存。 - ⚠️ 「返回首页」这类跳转**不要**裸写 location.href='/mama-box/' —— - 宿主不会因此重新注入身份(web-view src 没变),目标页会显示「身份不完整」, - 而重试也救不回来。要么调本方法,要么保证链接带上 openid/unionid。 - (第三方应用改不了的话,靠 identity() 的缓存兜底也能让首页认出人来。) - 2026-10-09 起首页地址在 box_app 后台可定制:本方法先拉一次生效地址 - (GET /api/mp/public/home/config,同源无 CORS)再跳;超时/接口坏/值非法 - → 回退内置默认(旧行为),「回首页」绝不被网络卡死(§5.6 部署顺序与降级)。 - 自定义首页 H5 落地后的「清 last_app」义务见接入规范第七节(mama-box 自带)。 */ - goHome: function () { - var id = identity(); - var q = id.openid - ? 'openid=' + encodeURIComponent(id.openid) + (id.unionid ? '&unionid=' + encodeURIComponent(id.unionid) : '') - : ''; - var DEFAULT_HOME = 'https://mp.yizhezhe.com/mama-box/'; - /* 首页地址可能自带 query(后台配置的),身份用 & 续在后面,别拼出两个 ? */ - var jump = function (home) { - location.href = home + (q ? (home.indexOf('?') >= 0 ? '&' : '?') + q : ''); - }; - var okUrl = function (u) { - return typeof u === 'string' && /^https:\/\//i.test(u) && !/\s/.test(u); - }; - try { - var done = false; - var finish = function (u) { - if (done) return; - done = true; - jump(okUrl(u) ? u : DEFAULT_HOME); - }; - var timer = setTimeout(function () { finish(''); }, 1500); - var xhr = new XMLHttpRequest(); - xhr.open('GET', 'https://mp.yizhezhe.com/api/mp/public/home/config', true); - try { xhr.timeout = 1500; } catch (e) {} - xhr.onload = function () { - clearTimeout(timer); - try { - var b = JSON.parse(xhr.responseText || '{}'); - /* ⚠️ 这个后端不存在路由时 HTTP 仍是 200,错误码在 body.code —— 必须查 body */ - finish(b && b.code === 200 && b.result ? b.result.url : ''); - } catch (e) { finish(''); } - }; - xhr.onerror = xhr.onabort = xhr.ontimeout = function () { clearTimeout(timer); finish(''); }; - xhr.send(); - } catch (e) { jump(DEFAULT_HOME); } - }, - /* ⑧ 展示授权结果到指定元素(可选便捷方法) */ - fillAuth: function (nameEl, avatarEl) { - var a = this.readAuth(); - if (nameEl && a.nickname) nameEl.textContent = a.nickname; - if (avatarEl && a.avatar) avatarEl.src = a.avatar; - }, - /* ⑨ 上报"当前在哪一页"(分享状态同步,不弹 toast、可频繁调用)。 - 为什么需要:web-view 里的 H5 不能自己拉起分享面板,转发卡片由小程序宿主页产出; - 宿主只有 onShareAppMessage(options).webViewUrl 能拿到当前地址,而 - onShareTimeline(朋友圈)**没有回调参数**;首页壳又不会随 H5 换页重新 onLoad。 - 所以应用在「页面/路由切换」时上报一次 location,宿主即可刷新分享图/标题/深链。 - 协议:{action:'location', href, title?, summary?, imageUrl?} → 详见 docs/h5-auth-standard.md 第四节 - opts.imageUrl 给了就以 H5 为准(仅当前应用);不给则用 box_app 后台配置的分享图。 */ - reportLocation: function (opts) { - opts = opts || {}; - if (!(window.wx && window.wx.miniProgram && window.wx.miniProgram.postMessage)) return false; - try { - window.wx.miniProgram.postMessage({ - data: { - action: 'location', - href: opts.href || location.href, // 可传 href:用于"即将跳转到的地址"上报 - title: opts.title || '', - summary: opts.summary || '', - imageUrl: opts.imageUrl || '' - } - }); - return true; - } catch (e) { - try { console.warn('[wxAuth] reportLocation fail:', e); } catch (e2) {} - return false; - } - }, - /* ⑧ 自动监听页面切换并上报(首屏一次 + hashchange/popstate + pushState/replaceState)。 - 用法(推荐在 init 回调里调用): - wxAuth.watchLocation(function () { - return { title: document.title }; // 可选:本页分享标题/图 - }); - 同一 href 只会发一次(内部去重),SPA 与多页应用都适用。 */ - watchLocation: function (getShare) { - var self = this; - var last = ''; - function pull() { - if (location.href === last) return; - last = location.href; - var s = (typeof getShare === 'function') ? (getShare() || {}) : {}; - self.reportLocation(s); - } - pull(); // 首屏 - if (g.addEventListener) { - g.addEventListener('hashchange', pull); - g.addEventListener('popstate', pull); - } - /* history.pushState/replaceState 不触发上面两个事件,需要包一层 */ - ['pushState', 'replaceState'].forEach(function (fn) { - var orig = g.history && g.history[fn]; - if (typeof orig !== 'function') return; - g.history[fn] = function () { - var r = orig.apply(this, arguments); - setTimeout(pull, 0); - return r; - }; - }); - return self; - }, - - toast: function (m) { - var t = document.getElementById('toast'); - if (!t) return; - t.textContent = m; t.classList.add('show'); - clearTimeout(t._tt); t._tt = setTimeout(function () { t.classList.remove('show'); }, 1800); - } - }; - g.wxAuth = wxAuth; -})(window);