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 Framework7React from 'framework7-react';
|
||||
|
||||
import routes from './routes';
|
||||
import routes, { tabForPath } from './routes';
|
||||
import Copilot from './components/Copilot';
|
||||
import { FEATURES } from './features';
|
||||
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.
|
||||
*
|
||||
@@ -251,6 +312,7 @@ function App() {
|
||||
const authed = useSession();
|
||||
useClearBootFlag();
|
||||
useRouterWatchdog();
|
||||
useDeepLink(authed);
|
||||
const navigating = useNavProgress();
|
||||
|
||||
return (
|
||||
@@ -298,6 +360,14 @@ function App() {
|
||||
// every tab each one reads the browser URL instead of its own
|
||||
// `url`, so all five loaded the root page.
|
||||
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=""
|
||||
/* Framework7's page transition is animation-driven: the router
|
||||
sets allowPageChange = false on push and restores it when the
|
||||
|
||||
@@ -67,4 +67,33 @@ const routes: Router.RouteParameters[] = [
|
||||
{ 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;
|
||||
|
||||
Reference in New Issue
Block a user