fix(router): 深链接全被塞进「今日」,点底部标签页显示的是别人的内容
打开 …/sleep/ 之后,底部点「今日」出来的是睡眠,点「健康」出来的健康从来
没去过地址栏说的地方。
原因:browserHistory 只开在主视图(今日)上,而 Framework7 在启动时也会用
它去消费地址栏——它只认识这一个视图。于是不管哪个标签页真正拥有那个路径,
睡眠都被压进了**今日**的栈,还顺手伪造了一份两条记录的 history。
- View 加 browserHistoryOnLoad={false}。注意不是 browserHistoryInitialMatch
——那个只决定两条记录里渲染哪一条,伪造的栈照样留着(读 router-class.js
的 getInitialUrl 才看明白)。之后的导航追踪不受影响。
- routes.ts 加 tabForPath():冷启动的 URL 归哪个标签页。只有深链接查这张表,
应用内点击照旧——从今日卡片点进睡眠,仍然留在今日。
- App.tsx 的 useDeepLink 负责派发初始地址,并把目标页压在该标签页自己的根
之上,这样返回箭头回到的是健康,而不是空栈。
派发要延后一帧再做、并在 unmount 时取消:StrictMode 会把 <View> 挂两次,
第一次挂载的 F7 视图会被销毁重建,在它上面导航的结果全丢——症状是一个睡眠
页孤零零留在 DOM 里,router 却坚称自己在健康。另外比较路径要先归一化斜杠,
每个页面都注册了带斜杠和不带斜杠两种写法,直接比会把趋势压在趋势上面。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { App as F7App, View, Views, Toolbar, Link, f7ready } from 'framework7-re
|
|||||||
import Framework7 from 'framework7/lite-bundle';
|
import Framework7 from 'framework7/lite-bundle';
|
||||||
import Framework7React from 'framework7-react';
|
import Framework7React from 'framework7-react';
|
||||||
|
|
||||||
import routes from './routes';
|
import routes, { tabForPath } from './routes';
|
||||||
import Copilot from './components/Copilot';
|
import Copilot from './components/Copilot';
|
||||||
import { FEATURES } from './features';
|
import { FEATURES } from './features';
|
||||||
import { apiClient, AUTH_EVENT } from './services/api';
|
import { apiClient, AUTH_EVENT } from './services/api';
|
||||||
@@ -220,6 +220,67 @@ function useRouterWatchdog() {
|
|||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a cold URL in the tab that owns it.
|
||||||
|
*
|
||||||
|
* The main view has `browserHistory`, which is what gives 今日 shareable
|
||||||
|
* URLs — but Framework7 also applies it on boot, and it only knows about
|
||||||
|
* that one view. So loading `…/sleep/` pushed 睡眠 onto **今日's** stack:
|
||||||
|
* the tab bar highlighted 今日, tapping 今日 showed 睡眠, and tapping 健康
|
||||||
|
* showed a 健康 that had never been where the address bar said it was.
|
||||||
|
*
|
||||||
|
* `browserHistoryInitialMatch={false}` on that view stops F7 from doing it,
|
||||||
|
* and the initial address is dispatched here instead — to the tab that owns
|
||||||
|
* the path, with that tab activated. In-app navigation is untouched: a detail
|
||||||
|
* screen still opens inside whichever tab you were already in.
|
||||||
|
*/
|
||||||
|
/* Module scope, not a ref: this must happen once per page load, and a ref is
|
||||||
|
reset by the remount it needs to survive. */
|
||||||
|
let deepLinkDispatched = false;
|
||||||
|
|
||||||
|
function useDeepLink(authed: boolean) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authed || deepLinkDispatched) return;
|
||||||
|
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const tab = tabForPath(path);
|
||||||
|
if (!tab) return;
|
||||||
|
|
||||||
|
/* Deferred, with the timer cleared on unmount, because StrictMode mounts
|
||||||
|
<View> twice: the first mount's Framework7 view is destroyed and rebuilt
|
||||||
|
from its `url` prop, so anything navigated on it is discarded — leaving
|
||||||
|
a 睡眠 page orphaned in the DOM while the router insisted it was on
|
||||||
|
健康. Cancelling on unmount means only the surviving mount dispatches. */
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
f7ready((app) => {
|
||||||
|
const view = app.views.get(`#view-${tab}`);
|
||||||
|
if (!view) return;
|
||||||
|
deepLinkDispatched = true;
|
||||||
|
app.tab.show(`#view-${tab}`);
|
||||||
|
// The tab's own root is already loaded, so this pushes the deep-linked
|
||||||
|
// screen on top of it — which is what makes the back chevron return to
|
||||||
|
// 健康 rather than to an empty stack. Skipped when the path *is* that
|
||||||
|
// root, or the tab would stack a duplicate of what it already shows.
|
||||||
|
//
|
||||||
|
// Compared with trailing slashes normalised, because every screen is
|
||||||
|
// registered under both spellings (see routes.ts): `…/trends` and the
|
||||||
|
// view's own `/trends/` are the same screen, and comparing them raw
|
||||||
|
// stacked 趋势 on top of 趋势.
|
||||||
|
const slashed = (url: string) => (url.endsWith('/') ? url : `${url}/`);
|
||||||
|
const current = view.router.history[view.router.history.length - 1] || '';
|
||||||
|
if (slashed(current) !== slashed(path)) {
|
||||||
|
view.router.navigate(slashed(path), { animate: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timer);
|
||||||
|
// Runs once per page load: re-running it on a later render would yank the
|
||||||
|
// user back to the address they arrived at.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [authed]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether there is a session, kept live for this tab.
|
* Whether there is a session, kept live for this tab.
|
||||||
*
|
*
|
||||||
@@ -251,6 +312,7 @@ function App() {
|
|||||||
const authed = useSession();
|
const authed = useSession();
|
||||||
useClearBootFlag();
|
useClearBootFlag();
|
||||||
useRouterWatchdog();
|
useRouterWatchdog();
|
||||||
|
useDeepLink(authed);
|
||||||
const navigating = useNavProgress();
|
const navigating = useNavProgress();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -298,6 +360,14 @@ function App() {
|
|||||||
// every tab each one reads the browser URL instead of its own
|
// every tab each one reads the browser URL instead of its own
|
||||||
// `url`, so all five loaded the root page.
|
// `url`, so all five loaded the root page.
|
||||||
browserHistory={tab.id === 'today'}
|
browserHistory={tab.id === 'today'}
|
||||||
|
/* Stops F7 from consuming the address bar on boot: it would load
|
||||||
|
that path into THIS view whichever tab actually owns it, and
|
||||||
|
fabricate a two-entry history to match. `…InitialMatch={false}`
|
||||||
|
is not enough — it only changes which of the two entries gets
|
||||||
|
rendered, leaving the bogus stack in place. Tracking for later
|
||||||
|
navigation is unaffected; the initial address is dispatched by
|
||||||
|
useDeepLink instead. */
|
||||||
|
browserHistoryOnLoad={false}
|
||||||
browserHistorySeparator=""
|
browserHistorySeparator=""
|
||||||
/* Framework7's page transition is animation-driven: the router
|
/* Framework7's page transition is animation-driven: the router
|
||||||
sets allowPageChange = false on push and restores it when the
|
sets allowPageChange = false on push and restores it when the
|
||||||
|
|||||||
@@ -67,4 +67,33 @@ const routes: Router.RouteParameters[] = [
|
|||||||
{ path: '(.*)', component: NotFoundPage },
|
{ path: '(.*)', component: NotFoundPage },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which tab a **cold URL** belongs to.
|
||||||
|
*
|
||||||
|
* Only deep links consult this. Navigating in the app keeps pushing detail
|
||||||
|
* screens onto whichever tab you are already in — 睡眠 opened from a 今日 card
|
||||||
|
* stays in 今日 — and that stays true; this table answers the different
|
||||||
|
* question of where a screen should appear when it is the first thing loaded.
|
||||||
|
*
|
||||||
|
* Order matters: the first pattern that matches wins, so `/body-age/` has to
|
||||||
|
* be tested before `/body/`.
|
||||||
|
*/
|
||||||
|
const TAB_OWNERS: Array<[RegExp, string]> = [
|
||||||
|
[/^\/(health|sleep|body-age|body)\/?$/, 'health'],
|
||||||
|
[/^\/(trends|daily)\/?$/, 'trends'],
|
||||||
|
[/^\/metric\//, 'trends'],
|
||||||
|
[/^\/(exercise|race|challenges)\/?$/, 'exercise'],
|
||||||
|
[/^\/activity\//, 'exercise'],
|
||||||
|
[/^\/(settings|sync|devices|rating-basis)\/?$/, 'settings'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/** The tab a cold URL should open in, or null for the root and unknown paths. */
|
||||||
|
export function tabForPath(path: string): string | null {
|
||||||
|
if (!path || path === '/' || path === '') return null;
|
||||||
|
for (const [pattern, tab] of TAB_OWNERS) {
|
||||||
|
if (pattern.test(path)) return tab;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|||||||
Reference in New Issue
Block a user