路由導航與 Navbar 顯示控制

常見的需求是根據不同的路由頁面來控制 Navbar 的顯示與隱藏。

例如,登入和註冊頁面通常不需要顯示 Navbar,而其他頁面則需要。

問題描述

在實現「登入/註冊頁面不顯示 Navbar,其他頁面顯示」功能時,常見的做法是在 AppComponent 中根據當前路由來控制 Navbar 的顯示狀態。然而遇到這樣的問題:首次加載應用時,即使是登入頁面也會短暫顯示 Navbar,而重新整理後才正常隱藏

常見但有問題的實現

以下是一個常見但可能有問題的實現:

import { Component, signal } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import { NavbarComponent } from './shared/navbar/navbar.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, NavbarComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {
  showNavBar = signal(false);

  constructor(router: Router) {
    // 直接在構造函數中檢查路由
    router.url.includes('login') || router.url.includes('register')
      ? this.showNavBar.set(false)
      : this.showNavBar.set(true);
  }
}
<!-- app.component.html -->
@if (showNavBar()){
<header>
  <app-navbar />
</header>
}
<main [class.content-container]="showNavBar()">
  <router-outlet />
</main>

為什麼這種方法有問題?

在 Angular 應用啟動時,路由器的初始化是非同步的,所以在 AppComponent 的構造函數中,router.url 可能還未被正確設置,導致條件判斷不準確。這就是為什麼首次載入時可能會顯示 Navbar,但重新整理後(此時路由已完全初始化)就正常了。

改進方法一:使用路由事件

一個改進方法是使用路由事件監聽器來偵測路由變化:

constructor(router: Router) {
  router.events.subscribe((event) => {
    if (event instanceof NavigationEnd) {
      this.showNavBar.set(
        event.url !== '/login' && event.url !== '/register'
      );
    }
  });
}

雖然這種方法改進了前一種方法,但仍然可能在應用首次加載時出現問題,因為它沒有處理初始路由狀態。

最佳實現方法

最佳的解決方案是結合多種技術來確保路由完全初始化後再進行檢查:

import { Component, signal, OnInit, inject } from '@angular/core';
import { Router, RouterOutlet, NavigationEnd, Event as RouterEvent } from '@angular/router';
import { filter } from 'rxjs/operators';
import { NavbarComponent } from './shared/navbar/navbar.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, NavbarComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent implements OnInit {
  private router = inject(Router);
  showNavBar = signal(false);

  ngOnInit() {
    // 設置短暫的延遲來確保路由已完全初始化
    setTimeout(() => {
      // 首先檢查當前路由
      this.updateNavbarVisibility(this.router.url);

      // 監聽後續的路由變化
      this.router.events
        .pipe(
          filter(
            (event: RouterEvent): event is NavigationEnd =>
              event instanceof NavigationEnd
          )
        )
        .subscribe((event: NavigationEnd) => {
          this.updateNavbarVisibility(event.url);
        });
    }, 0);
  }

  private updateNavbarVisibility(url: string): void {
    this.showNavBar.set(!(url.includes('login') || url.includes('register')));
  }
}

這個解決方案的關鍵點:

  1. 使用 setTimeout 延遲初始化:即使是 0 毫秒的延遲,也能確保代碼在當前執行周期之後執行,這時路由應該已經完全初始化。

  2. 單獨的更新方法:將更新邏輯抽離到單獨的方法中,提高代碼的可讀性和可維護性。

  3. 使用 ngOnInit 而非構造函數:在 ngOnInit 生命週期鉤子中處理初始化邏輯,這時組件已經完全初始化。

  4. RxJS 過濾器:使用 filter 操作符來只處理 NavigationEnd 事件,避免處理其他類型的路由事件。

  5. TypeScript 類型安全:使用類型斷言確保事件是 NavigationEnd 類型,提供更好的類型安全性。

解決 TypeScript 類型問題

如果在使用此方法時遇到類型問題,特別是關於 Event 的類型錯誤,這通常是因為 TypeScript 混淆了 DOM 的 Event 和 Angular Router 的 Event。解決方法是明確導入並重命名 Router 的 Event:

import { Event as RouterEvent } from '@angular/router';

然後在 filter 操作符中使用正確的類型:

filter((event: RouterEvent): event is NavigationEnd => event instanceof NavigationEnd)

其他可能的解決方案

除了上述方法外,還有其他幾種方式可以處理這個問題:

  1. 使用不同的布局組件:為需要 Navbar 的頁面和不需要 Navbar 的頁面創建不同的布局組件,然後在路由配置中使用不同的布局。

  2. 使用路由數據:在路由配置中添加數據屬性,例如 {path: 'login', component: LoginComponent, data: {showNavbar: false}},然後在 AppComponent 中根據當前路由的數據來決定是否顯示 Navbar。

  3. 使用路由守衛:創建一個路由守衛來預先處理路由導航,並設置全局狀態來控制 Navbar 的顯示。

結論

在 Angular 應用中控制 Navbar 的顯示是一個常見需求,但初始化過程中的異步特性可能導致問題。通過正確使用路由事件監聽、setTimeout 延遲和類型安全的代碼,可以實現穩定可靠的 Navbar 控制邏輯。

這種方法不僅解決了首次加載和導航過程中的問題,還提供了良好的代碼結構和類型安全性,是處理此類需求的推薦方式。

Last updated