在 .NET 8 Blazor Web App 中攔截所有內部導航並平滑捲動到頂端

| .NET | 2 Reads

在 .NET 8 的 Blazor Web App(Hybrid 模式)中,當你在文章內容裡大量使用原生 <a href="…"> 連結時,點擊後往往只會做「局部刷新」,導致頁面內容更新但瀏覽器捲軸位置依然停留在原地,使用者體驗大打折扣。本文將示範如何在宿主頁面(App.razor)中,注入一段小小的 JavaScript 補丁,攔截所有內部路由並統一「平滑捲動到頂端」。


1. 背景與需求

  • 局部刷新 vs 全頁刷新
    Blazor Server/WASM 的 SPA 路由會攔截同源連結,透過 History API 做導航並只重渲染 @Body 區域,元件雖然更新了,但捲軸位置不會歸零。

  • 大量原生 <a>
    博客文章裡常常直接寫:

    <a href="/category/技術分享">技術分享</a>
    

    不想改成 <NavLink>、也不想在每個連結上額外寫 JS。

  • 目標
    無論點擊 <a><NavLink>、呼叫 NavigateTo,或是瀏覽器前進後退,都能在新內容渲染完畢後,一次性「平滑捲動到頁首」。


2. .NET 8 Hybrid 模式下的宿主頁面

在 .NET 6/7 的 Blazor Server,我們常把起始設在 _Host.cshtml

<body>
  <component type="typeof(App)" render-mode="InteractiveServer" />
</body>

而在 .NET 8 的「Blazor Web App(Hybrid)」裡,App.razor 就相當於舊版的 _Host.cshtml,同時支援 Server 與 WASM 的啟動:

<!DOCTYPE html>
<html lang="en">
<head> … </head>
<body>
  <Routes />
  <script src="_framework/blazor.web.js"></script>
</body>
</html>

因此,要做「全局攔截」或「最早注入」的腳本,就應該放在這個檔案裡。


3. 原理:攔截 History API 並觸發自定義事件

瀏覽器的 SPA 導航主要靠 history.pushState(以及 popstate):

  1. Blazor 客戶端路由會呼叫 history.pushState(...)

  2. 使用者點擊瀏覽器「前進/後退」會觸發 popstate

我們可以「打補丁」(monkey-patch)它:

// 保留原始方法
const _pushState = history.pushState;
// 重寫 pushState
history.pushState = function () {
  _pushState.apply(this, arguments);
  window.dispatchEvent(new Event('spa-navigate'));
};
// 攔截前進/後退
window.addEventListener('popstate', () => {
  window.dispatchEvent(new Event('spa-navigate'));
});
// 統一平滑捲動
window.addEventListener('spa-navigate', () => {
  window.scrollTo({ top: 0, behavior: 'smooth' });
});

每當發生內部導航或使用者前進/後退,就會觸發自定義的 spa-navigate 事件,接著執行 scrollTo


4. 在 App.razor 中注入補丁

將上述腳本,加入到你的 App.razor(即 .NET 8 Hybrid 的宿主)中,示例如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <base href="/" />

  <link rel="stylesheet" href="v2knowBlazor.styles.css" />
  <HeadOutlet />
</head>
<body>
  <!-- Blazor 根組件佔位 -->
  <Routes />

  <!-- —— 平滑捲動補丁開始 —— -->
  <script>
    (function () {
      const _pushState = history.pushState;
      history.pushState = function () {
        _pushState.apply(this, arguments);
        window.dispatchEvent(new Event('spa-navigate'));
      };
      window.addEventListener('popstate', () => {
        window.dispatchEvent(new Event('spa-navigate'));
      });
      window.addEventListener('spa-navigate', () => {
        window.scrollTo({ top: 0, behavior: 'smooth' });
      });
    })();
  </script>
  <!-- —— 平滑捲動補丁結束 —— -->

  <script src="_framework/blazor.web.js"></script>
  <script src="js/MatrixBackground.js"></script>
  <script src="js/visitorDataInterop.js"></script>
  <script src="js/sidebarInterop.js"></script>
  <script src="js/articlePageHelper.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"
          onerror="this.onerror=null;this.src='js/highlight.min.js';"></script>
</body>
</html>

注意:

  • 補丁腳本必須放在 <Routes /> 之後、Blazor 框架腳本之前,確保能攔截最早的路由呼叫。

  • 不要把這段腳本放到任何 .razor 組件裡,否則插入時機都太晚,無法全局攔截。


5. 完整示例

假設你的專案結構如下:

/Pages
  └ App.razor         ← .NET 8 Hybrid 的宿主
/wwwroot
  /js
    └ articlePageHelper.js
  /css
    └ Global.css

那麼最終的 Pages/App.razor 內容就是上述範例。此後,無論你在文章裡使用多少個純 <a href="/category/xxx">…</a>,點擊都會觸發 spa-navigate,並在新內容渲染完畢後平滑捲動到頂端。


6. 小結

  • .NET 8 HybridApp.razor 就是宿主頁面,替代了過去的 _Host.cshtml/index.html

  • JavaScript 補丁:在宿主頁面打補丁攔截 history.pushState + popstate,再統一平滑捲動;不用修改任何組件或連結。

  • 兼容性佳:同時支援純 HTML 連結、<NavLink>NavigateTo()、前進/後退,讓使用者在每次路由切換後都能自動回到頁首。

透過這段「秒級」的腳本補丁,你的博客內部連結再也不用一個個改寫成 <NavLink>,用戶點完任何內部導航後,都能順暢地回到頂端,提升閱讀體驗。

This article was last edited at