iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
自我挑戰組

Angular 開發二三事系列 第 9 篇

Day9: 全域 Loading

  • 分享至 

  • xImage
  •  

加班到快斷賽,還無法發文(打第二次了)/images/emoticon/emoticon04.gif


步驟一: 建立狀態管理 Service

import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class LoadingService {
  private readonly activeRequests = signal<number>(0);

  readonly isLoading = signal<boolean>(false);

  show(): void {
    this.activeRequests.update(count => count + 1);
    this.isLoading.set(true);
  }

  hide(): void {
    this.activeRequests.update(count => Math.max(0, count - 1));
    if (this.activeRequests() === 0) {
      this.isLoading.set(false);
    }
  }
}

步驟二: 建立攔截器

import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { finalize } from 'rxjs';
import { LoadingService } from '../services/loading.service';

export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
  const loadingService = inject(LoadingService);

  loadingService.show();

  return next(req).pipe(
    finalize(() => loadingService.hide())
  );
};

步驟三: 建立 Loading 元件

import { Component, inject } from '@angular/core';
import { LoadingService } from '@app/core/services/loading.service';

@Component({
  selector: 'app-global-loading',
  standalone: true,
  template: `
    @if (loadingService.isLoading()) {
      <div class="loading-overlay">
        <div class="spinner"></div>
      </div>
    }
  `,
  styles: [`
    .loading-overlay {
      position: fixed;
      top: 0; left: 0; width: 100vw; height: 100vh;
      background: rgba(0, 0, 0, 0.4);
      display: flex; justify-content: center; align-items: center;
      z-index: 9999;
    }
    .spinner {
      width: 48px; height: 48px;
      border: 5px solid #FFF;
      border-bottom-color: transparent;
      border-radius: 50%;
      animation: rotation 1s linear infinite;
    }
    @keyframes rotation {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
  `]
})
export class GlobalLoading {
  protected readonly loadingService = inject(LoadingService);
}

步驟四: 掛載到 AppComponent 與 app.config.ts

在最外層的 App 元件匯入 Loading 元件。

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { GlobalLoading } from '@shared-components';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, GlobalLoading],
  template: `
    <router-outlet />
    <app-global-loading />
  `
})
export class AppComponent {}

app.config.ts 設定攔截器,每當呼叫 API 便會執行 Loading 遮罩。

import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { loadingInterceptor } from '@app/core/interceptors/loading.interceptor';
import { authInterceptor } from '@app/core/interceptors/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([
        loadingInterceptor, // 全域 Loading 觸發
        authInterceptor,    // Token 與 Error 處理
      ])
    ),
    // ...
  ]
};

今天就這樣啦!


上一篇
Day8: 寫個 404 頁面
下一篇
Day10: 寫一個瀏覽器儲存空間封裝工具
系列文
Angular 開發二三事 共 10 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言