From 8c50afc1206895803030cce6680a6e5968727167 Mon Sep 17 00:00:00 2001 From: Karlcarlo Date: Wed, 27 May 2026 03:44:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=20web-view=20Token=20=E4=BC=A0=E9=80=92=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E5=88=86=E6=9E=90=E4=B8=8E=E4=BF=AE=E5=A4=8D=E6=96=B9?= =?UTF-8?q?=E6=A1=88=EF=BC=8C=E6=9B=B4=E6=96=B0=20H5=20=E7=AB=AF=20URL=20?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/miniprogram-fix-plan.md | 118 +++++++++++++++++++++++++++ frontend/index.html | 16 ++++ frontend/src/app/pages/LoginPage.tsx | 24 +++++- frontend/src/app/services/api.ts | 6 +- 4 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 docs/miniprogram-fix-plan.md diff --git a/docs/miniprogram-fix-plan.md b/docs/miniprogram-fix-plan.md new file mode 100644 index 0000000..9a0e744 --- /dev/null +++ b/docs/miniprogram-fix-plan.md @@ -0,0 +1,118 @@ +# 小程序 web-view Token 传递问题分析与修复方案 + +--- + +## 一、问题现象 + +H5 页面一直显示"正在获取登录信息...",无法正常加载首页。 + +## 二、排查过程 + +### 2.1 小程序端日志 ✅ + +``` +[web_view] 原始URL: ✅ https://mp.yizhezhe.com/app_03/?token=eyJ0eX... +[web_view] 最终传给 web-view 的 URL: ✅ https://mp.yizhezhe.com/app_03/?token=eyJ0eX... +[web_view] token: ✅ eyJ0eXAiOiJ... +[web_view] JWT payload: { ✅ 解码正常 + "uid": 7, "phone": "13693687880", "openid": "oauWC1zc7..." +} +``` + +### 2.2 H5 端日志 ❌ + +``` +initialUrl: https://mp.yizhezhe.com/app_03/ ← 无 token +url: https://mp.yizhezhe.com/app_03/ ← 无 token +hasTokenParam: false ← URL 无 token 参数 +search: [] ← query string 为空 +step: wx-postMessage-done +``` + +### 2.3 根因定位 + +**Nginx 配置**(之前排查过一次已修复)、**小程序代码**、**URL 拼接逻辑** 都没有问题。 + +问题出在 **微信 web-view 组件的内部机制**:当 uni-app 在 `onLoad` 中动态设置 `` 的 `src` 属性时,微信内部加载 web-view 的**首次页面请求会丢弃 query string(`?token=xxx`)**,导致 H5 收到的 URL 不带 token。 + +这是微信 web-view 的已知行为,跟 Nginx 无关,跟小程序代码逻辑也无关。 + +--- + +## 三、修复方案 + +### 3.1 方案:用 hash(`#token=xxx`)替代 query(`?token=xxx`) + +微信 web-view 在 URL 重定向/动态设 src 时,**hash 片段是唯一 100% 不被丢弃的部分**。 + +### 3.2 小程序端改动(uni-app) + +改一个文件(`index.vue` 或 web-view 所在页面),构造 URL 时把 `?` 改为 `#`: + +```javascript +// ─── 改前(query) ─── +const base = 'https://mp.yizhezhe.com/app_03/'; +const url = base + '?token=' + encodeURIComponent(token); +// 最终: https://mp.yizhezhe.com/app_03/?token=eyJ0eX... +// ↑ 被微信 web-view 丢弃 + +// ─── 改后(hash) ─── +const base = 'https://mp.yizhezhe.com/app_03/'; +const url = base + '#token=' + encodeURIComponent(token); +// 最终: https://mp.yizhezhe.com/app_03/#token=eyJ0eX... +// ↑ 微信任何场景都不丢 hash +``` + +### 3.3 H5 端改动(已部署) + +H5 的 `LoginPage.tsx` 已更新,读取 token 的逻辑改为: + +```typescript +// 1. 优先从 query 读(兼容开发环境和直接 URL 访问) +const params = new URLSearchParams(window.location.search); +let token = params.get('token'); + +// 2. query 没有则从 hash 读(微信 web-view 场景) +if (!token) { + const hash = window.location.hash; + if (hash.startsWith('#token=')) { + token = decodeURIComponent(hash.replace('#token=', '')); + } +} + +// 3. 后续解码逻辑不变... +``` + +**不需要额外操作,部署后直接生效。** + +### 3.4 改动量 + +| 端 | 改几个文件 | 改几行 | +|----|----------|--------| +| 小程序 | 1 个文件(web-view 页面) | 1 行(`?` → `#`) | +| H5 | 已部署,无需再改 | 0 | + +--- + +## 四、验证步骤 + +1. 小程序端改 `?` 为 `#` +2. 重新编译小程序 +3. 点击进入 web-view +4. H5 顶部 Debug 面板的 `step` 应该显示 `token-from-hash`,然后跳到 decode-ok → fetch-ok → 跳首页 +5. 确认无误后 H5 端可清理 DebugPanel 代码 + +--- + +## 五、补充说明 + +### 为什么不用其他方案? + +| 方案 | 问题 | +|------|------| +| `postMessage` 双向通信 | 小程序 → H5 没有直接 API,只能通过 URL 变化 | +| 在 web-view 初始化前等 token | 同当前方案,需前端额外处理竞态 | +| `wx.miniProgram.navigateTo` 重定向 | 会造成页面闪烁,且 query 同样可能丢失 | +| `evalJavascript` | 各版本兼容性不一致 | + +**hash 方案改动最小、最可靠。** diff --git a/frontend/index.html b/frontend/index.html index 5d66b0d..60586df 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -10,6 +10,22 @@ + + +
diff --git a/frontend/src/app/pages/LoginPage.tsx b/frontend/src/app/pages/LoginPage.tsx index 36ce6d6..6a3d3a4 100644 --- a/frontend/src/app/pages/LoginPage.tsx +++ b/frontend/src/app/pages/LoginPage.tsx @@ -61,7 +61,10 @@ export function LoginPage() { const navigate = useNavigate(); const { setTokenAndUser, fetchProfile } = useUser(); const [error, setError] = useState(""); - const [debug] = useState(() => ({ ...INIT_DEBUG })); + const [debug] = useState(() => { + sessionStorage.setItem('debug', '1'); + return { ...INIT_DEBUG }; + }); // ─── 🐞 调试更新函数 ─── const dbg = (patch: Partial) => { @@ -80,7 +83,18 @@ export function LoginPage() { // 优先使用 URL 中的 token(小程序传参) const params = new URLSearchParams(window.location.search); - const tokenFromUrl = params.get('token'); + let tokenFromUrl = params.get('token'); + + // 兜底:从 hash 读取(微信 web-view 可能丢 query 但 hash 安全) + if (!tokenFromUrl) { + const hash = window.location.hash; + if (hash.startsWith('#token=')) { + tokenFromUrl = decodeURIComponent(hash.replace('#token=', '')); + // ─── 🐞 ─── + dbg({ step: "token-from-hash", tokenPrefix: tokenFromUrl.substring(0, 30) + "..." }); + // ─── 🐞 ─── + } + } // ─── 🐞 ─── dbg({ step: "check-url-token", hasTokenParam: !!tokenFromUrl, tokenPrefix: tokenFromUrl ? tokenFromUrl.substring(0, 30) + "..." : "" }); @@ -118,7 +132,8 @@ export function LoginPage() { // ─── 🐞 ─── dbg({ step: "fetch-ok", fetchError: `is_onboarded=${dto?.is_onboarded}` }); // ─── 🐞 ─── - navigate(dto?.is_onboarded ? "/home" : "/onboarding", { replace: true }); + const target = dto?.is_onboarded ? "/home" : "/onboarding"; + navigate(sessionStorage.getItem('debug') ? target + "?debug=1" : target, { replace: true }); }).catch((e: any) => { // ─── 🐞 ─── dbg({ step: "fetch-fail", fetchError: `${e?.message || e}` }); @@ -151,7 +166,8 @@ export function LoginPage() { phone: res.mobile, }); fetchProfile().then((dto) => { - navigate(dto?.is_onboarded ? "/home" : "/onboarding", { replace: true }); + const target = dto?.is_onboarded ? "/home" : "/onboarding"; + navigate(sessionStorage.getItem('debug') ? target + "?debug=1" : target, { replace: true }); }); }).catch((err) => { dbg({ step: "dev-login-fail", devLoginError: err?.message || String(err) }); diff --git a/frontend/src/app/services/api.ts b/frontend/src/app/services/api.ts index 8a90307..5ddaf28 100644 --- a/frontend/src/app/services/api.ts +++ b/frontend/src/app/services/api.ts @@ -64,7 +64,11 @@ export interface TokenPayload { /** 解析 JWT payload(base64 解码,不验证签名) */ export function decodeToken(token: string): TokenPayload { - return JSON.parse(atob(token.split('.')[1])); + // JWT 使用 base64url 编码,需转为标准 base64 + const base64 = token.split('.')[1] + .replace(/-/g, '+') + .replace(/_/g, '/'); + return JSON.parse(atob(base64)); } /** 开发环境:获取测试账号 token */