@xingwangzhe/stalux 1.41.4 → 1.41.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.41.4",
3
+ "version": "1.41.5",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -274,6 +274,57 @@ function trackVercountPage(): void {
274
274
  document.head.appendChild(script);
275
275
  }
276
276
 
277
+ /** 文档是否已解析完、DOM 可查询(`loading` 之外的两种状态都算)。 */
278
+ function documentIsParsed(): boolean {
279
+ const readyState = document.readyState;
280
+ return readyState === "interactive" || readyState === "complete";
281
+ }
282
+
283
+ let pendingEarlyView = 0;
284
+ let earlyViewPending = false;
285
+ let viewedEarly = false;
286
+
287
+ function dispatchPageView(): void {
288
+ initializeTrackers();
289
+ trackGooglePage();
290
+ trackUmamiPage();
291
+ trackVercountPage();
292
+ state.previousPage = window.location.href;
293
+ }
294
+
295
+ /**
296
+ * 首访在文档解析完成后就上报一次,不等 window.load。
297
+ * `astro:page-load` 首访时由 window.load 派发,要等完所有图片、字体和第三方资源,
298
+ * 慢资源或卡死的请求会让页脚统计一直停在 Loading...。上报必须按页去重,
299
+ * 否则同一次访问会被重复计入、Vercount 也会重复追加脚本。
300
+ */
301
+ function dispatchEarlyView(): void {
302
+ const token = ++pendingEarlyView;
303
+ earlyViewPending = true;
304
+ queueMicrotask(() => {
305
+ if (token !== pendingEarlyView || !earlyViewPending) return;
306
+ earlyViewPending = false;
307
+ viewedEarly = true;
308
+ dispatchPageView();
309
+ });
310
+ }
311
+
312
+ function handlePageLoad(): void {
313
+ if (earlyViewPending) {
314
+ // page-load 属于当前页,由它立即上报,作废排队中的早派发。
315
+ pendingEarlyView++;
316
+ earlyViewPending = false;
317
+ viewedEarly = true;
318
+ dispatchPageView();
319
+ return;
320
+ }
321
+ if (viewedEarly) {
322
+ viewedEarly = false;
323
+ return;
324
+ }
325
+ dispatchPageView();
326
+ }
327
+
277
328
  if (state.installed && (!state.native || state.revision !== 4)) {
278
329
  // Revision 4 replaces synchronous Google page views and the motion policy.
279
330
  // An open tab can cross a deployment boundary. Its old anonymous worker
@@ -284,6 +335,9 @@ if (state.installed && (!state.native || state.revision !== 4)) {
284
335
  state.revision = 4;
285
336
  state.installed = true;
286
337
  document.addEventListener("astro:before-swap", (event) => {
338
+ pendingEarlyView++;
339
+ earlyViewPending = false;
340
+ viewedEarly = false;
287
341
  transitionReady = prepareRouteMotion(event as Event & RouteMotionEvent).ready;
288
342
  // Invalidate an older page's frames, timer and posted task before this
289
343
  // incoming snapshot starts. Its captured visit remains in the FIFO.
@@ -294,11 +348,7 @@ if (state.installed && (!state.native || state.revision !== 4)) {
294
348
  document.addEventListener("visibilitychange", () => {
295
349
  if (document.hidden) flushGooglePages();
296
350
  });
297
- document.addEventListener("astro:page-load", () => {
298
- initializeTrackers();
299
- trackGooglePage();
300
- trackUmamiPage();
301
- trackVercountPage();
302
- state.previousPage = window.location.href;
303
- });
351
+ document.addEventListener("astro:page-load", handlePageLoad);
352
+ if (documentIsParsed()) dispatchEarlyView();
353
+ else document.addEventListener("DOMContentLoaded", dispatchEarlyView, { once: true });
304
354
  }
@@ -92,9 +92,15 @@ function prepareBackground(): Promise<number | undefined> {
92
92
  );
93
93
  }
94
94
 
95
+ let firstCommitStarted = false;
96
+
95
97
  function initializeBackground(): void {
96
98
  const active = getLayerEl(getActiveLayer());
97
99
  if (!prepared && active?.style.backgroundImage) return;
100
+ // 首访的早提交已经发起,但解码还没落地:此时 window.load 上的 page-load
101
+ // 不是新页面,不能再解码并淡入第二张。
102
+ if (!prepared && firstCommitStarted) return;
103
+ if (!prepared) firstCommitStarted = true;
98
104
  const current = generation;
99
105
  const ready = prepared ?? prepareBackground();
100
106
  prepared = undefined;
@@ -119,4 +125,9 @@ if (!(window as Window & { __STALUX_BG_LISTENER__?: boolean }).__STALUX_BG_LISTE
119
125
  transitionFinished = prepareRouteMotion(event as RouteMotionEvent).finished;
120
126
  });
121
127
  document.addEventListener("astro:page-load", initializeBackground);
128
+ // 首访的 page-load 由 window.load 派发,任何慢图片或卡死的第三方资源都会把首屏背景
129
+ // 拖成一片纯色;文档解析完就提交首张背景,软导航仍由 page-load 负责。
130
+ const readyState = document.readyState;
131
+ if (readyState === "interactive" || readyState === "complete") initializeBackground();
132
+ else document.addEventListener("DOMContentLoaded", initializeBackground, { once: true });
122
133
  }
@@ -31,10 +31,21 @@ function getBrowserEnvironment(): PageRuntimeEnvironment | undefined {
31
31
  };
32
32
  }
33
33
 
34
+ /** 文档是否已解析完、DOM 可查询(`loading` 之外的两种状态都算)。 */
35
+ function isDocumentParsed(readyState: string | undefined): boolean {
36
+ return readyState === "interactive" || readyState === "complete";
37
+ }
38
+
34
39
  /**
35
40
  * Mount one page-scoped behavior for the current DOM and dispose it before Astro swaps the body.
36
41
  * The key is global for the browser session, so a bundled module can be encountered repeatedly
37
42
  * without accumulating document-level listeners.
43
+ *
44
+ * 首访不能只等 `astro:page-load`:Astro 在首次加载时从 `window.load` 派发该事件,而
45
+ * `window.load` 要等完所有图片、字体与第三方 iframe。首访一旦有子资源变慢或卡死
46
+ * (国内访问外部资源时的常态),事件就迟迟不来甚至永不到达,评论与统计整页空白,
47
+ * 此时 `document.readyState` 停在 `interactive`,只判 `complete` 的兜底也救不回来。
48
+ * 因此文档解析完成即接管本页;`astro:page-load` 只负责软导航后的新页面与重复信号。
38
49
  */
39
50
  export function registerPageLifecycle(
40
51
  key: string,
@@ -55,7 +66,13 @@ export function registerPageLifecycle(
55
66
  logger.debug("lifecycle registered");
56
67
 
57
68
  let active = true;
58
- let pageGeneration = 0;
69
+ // 已安排但尚未执行的早挂载。自增即作废:早挂载一旦被 page-load 或换页接管,
70
+ // 排队中的任务不能再挂到已被替换的 DOM 上。
71
+ let pendingEarlyMount = 0;
72
+ let earlyMountPending = false;
73
+ // 早挂载已经覆盖当前页。此时 window.load 上的 page-load 只是同一页的重复信号,
74
+ // 再挂一次会造出第二份观察器或第二个评论实例。
75
+ let mountedEarly = false;
59
76
  const registration: RuntimeRegistration = {
60
77
  unregister: () => undefined,
61
78
  };
@@ -73,7 +90,6 @@ export function registerPageLifecycle(
73
90
  };
74
91
  const mountPage = () => {
75
92
  if (!active) return;
76
- pageGeneration++;
77
93
  disposePage();
78
94
  const started = performance.now();
79
95
  logger.debug("mount started");
@@ -87,11 +103,41 @@ export function registerPageLifecycle(
87
103
  throw error;
88
104
  }
89
105
  };
90
- const handlePageLoad = () => mountPage();
106
+ const handlePageLoad = () => {
107
+ if (earlyMountPending) {
108
+ // page-load 属于当前页,由它立即挂载,作废排队中的早挂载。
109
+ pendingEarlyMount++;
110
+ earlyMountPending = false;
111
+ mountedEarly = true;
112
+ mountPage();
113
+ return;
114
+ }
115
+ if (mountedEarly) {
116
+ mountedEarly = false;
117
+ logger.debug("page-load after early mount; keeping the live instance");
118
+ return;
119
+ }
120
+ mountPage();
121
+ };
91
122
  const handleBeforeSwap = () => {
92
- pageGeneration++;
123
+ pendingEarlyMount++;
124
+ earlyMountPending = false;
125
+ mountedEarly = false;
93
126
  disposePage();
94
127
  };
128
+ const mountEarlyPage = () => {
129
+ const token = ++pendingEarlyMount;
130
+ earlyMountPending = true;
131
+ // 微任务:模块脚本执行时 DOM 已完整,但要等模块自身求值结束再调用 mount()
132
+ // (模块顶层的后置绑定可能仍在 TDZ)。
133
+ queueTask(() => {
134
+ if (!active || token !== pendingEarlyMount || !earlyMountPending) return;
135
+ earlyMountPending = false;
136
+ mountedEarly = true;
137
+ mountPage();
138
+ });
139
+ };
140
+ const handleDomReady = () => mountEarlyPage();
95
141
 
96
142
  target.addEventListener("astro:page-load", handlePageLoad);
97
143
  target.addEventListener("astro:before-swap", handleBeforeSwap);
@@ -104,19 +150,14 @@ export function registerPageLifecycle(
104
150
  } finally {
105
151
  target.removeEventListener("astro:page-load", handlePageLoad);
106
152
  target.removeEventListener("astro:before-swap", handleBeforeSwap);
153
+ target.removeEventListener("DOMContentLoaded", handleDomReady);
107
154
  registry.delete(key);
108
155
  }
109
156
  };
110
157
  registry.set(key, registration);
111
158
 
112
- // Late-loaded modules can miss Astro's initial page-load event.
113
- if (target.readyState === "complete") {
114
- const scheduledGeneration = pageGeneration;
115
- queueTask(() => {
116
- // A real page-load or swap supersedes this late-module fallback.
117
- if (pageGeneration === scheduledGeneration) mountPage();
118
- });
119
- }
159
+ if (isDocumentParsed(target.readyState)) mountEarlyPage();
160
+ else target.addEventListener("DOMContentLoaded", handleDomReady, { once: true });
120
161
 
121
162
  return registration.unregister;
122
163
  }