Skip to content

Commit f3852d1

Browse files
committed
docs(rank): 补中文注释说明埋点组件与 tab 壳子意图
- DocsPageViewTracker 说明埋点目的、sessionStorage 去重策略与静默失败 - RankTabs 解释为什么状态走 URL query 而不是 component state
1 parent 6fc5ec7 commit f3852d1

2 files changed

Lines changed: 29 additions & 0 deletions

File tree

app/components/DocsPageViewTracker.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,38 @@
33
import { usePathname } from "next/navigation";
44
import { useEffect } from "react";
55

6+
/**
7+
* 文档页面访问埋点组件。
8+
*
9+
* 放在 app/docs/layout.tsx 下,pathname 变化时向自家 /api/analytics 上报一次 page_view,
10+
* 供将来基于 AnalyticsEvent 表做文档热度分析(当前 A-2 功能的热榜是用 GA4 数据,此处并行积累自家数据)。
11+
*
12+
* 去重策略:同一浏览器会话内同一 path 只报一次(sessionStorage key = "pv_reported:<path>")。
13+
* 为什么用 sessionStorage 不用 localStorage:关闭标签页后应当算新会话,否则长期复访的用户会被严重低估。
14+
*
15+
* 无返回 UI(return null),仅作副作用组件使用。
16+
*/
617
export function DocsPageViewTracker() {
718
const pathname = usePathname();
819

920
useEffect(() => {
1021
if (!pathname) return;
1122

23+
// 同会话同 path 已上报则跳过,避免刷新/快速切换重复计数
1224
const key = `pv_reported:${pathname}`;
1325
if (sessionStorage.getItem(key)) return;
1426

1527
sessionStorage.setItem(key, "1");
1628

29+
// 如果用户登录了,带上 Sa-Token 让后端能把事件关联到 userId;匿名用户后端会写入 userId=null
1730
const token =
1831
typeof window !== "undefined" ? localStorage.getItem("satoken") : null;
1932
const headers: Record<string, string> = {
2033
"Content-Type": "application/json",
2134
};
2235
if (token) headers["x-satoken"] = token;
2336

37+
// 埋点失败静默吞掉:不能因为分析接口挂了影响文档页的正常阅读体验
2438
fetch("/api/analytics", {
2539
method: "POST",
2640
headers,

app/components/rank/RankTabs.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,25 @@ type Tab = "contributors" | "hot";
77
type Window = "7d" | "30d" | "all";
88

99
interface RankTabsProps {
10+
/** Contributors tab 的静态内容,由 /rank/page.tsx SSR 渲染后以 children 传入 */
1011
children: React.ReactNode;
12+
/** SSR 决定的初始 tab,来自 URL query ?tab=;客户端挂载后以 searchParams 为准 */
1113
initialTab: Tab;
14+
/** SSR 决定的初始窗口,Hot Docs tab 用 */
1215
initialWindow: Window;
1316
}
1417

18+
/**
19+
* /rank 页的 Tab 壳子:Contributors(贡献者榜,静态 JSON)/ Hot Docs(热门文档榜,后端 API)。
20+
*
21+
* Tab 和窗口状态都写进 URL query(?tab=&window=),而不是组件内 state,这样:
22+
* 1. 分享链接能直接定位到具体视图
23+
* 2. 浏览器前进/后退正常切换
24+
* 3. 刷新不丢状态
25+
*
26+
* 用 router.push 而非 replaceState 是为了让返回键能回到上一个 tab;窗口切换在 HotDocsTab 内部用
27+
* replaceState,避免每切一次就污染历史栈。
28+
*/
1529
export function RankTabs({
1630
children,
1731
initialTab,
@@ -25,6 +39,7 @@ export function RankTabs({
2539
const switchTab = (tab: Tab) => {
2640
const params = new URLSearchParams(searchParams.toString());
2741
params.set("tab", tab);
42+
// 首次切到 Hot Docs 还没选过窗口时默认 30d,避免 HotDocsTab 拿到 undefined
2843
if (tab === "hot" && !params.get("window")) {
2944
params.set("window", "30d");
3045
}

0 commit comments

Comments
 (0)