如何在其他地方使用 LanguageService 的 language$

概述

LanguageService.language$ 是一個 ReplaySubject<LangChangeEvent>(1),用於發佈語言變化事件。本文說明如何在組件、服務和其他地方正確使用它。

1. 在組件中使用

基本訂閱模式

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { LanguageService } from '../../base/services/language.service';

@Component({
  selector: 'app-header',
  template: `
    <div>當前語言: {{ currentLang }}</div>
    <button (click)="changeLanguage('zh-tw')">中文</button>
    <button (click)="changeLanguage('en-us')">English</button>
  `
})
export class HeaderComponent implements OnInit, OnDestroy {

  currentLang = 'en-us';
  private subscription?: Subscription;

  constructor(private languageService: LanguageService) {}

  ngOnInit() {
    // ✅ 正確:訂閱語言變化
    this.subscription = this.languageService.language$.subscribe(event => {
      console.log('語言改變:', event.lang);
      this.currentLang = event.lang;

      // 可以在這裡執行其他邏輯
      this.updateUI(event.lang);
    });
  }

  ngOnDestroy() {
    // ✅ 重要:取消訂閱防止記憶體洩漏
    this.subscription?.unsubscribe();
  }

  changeLanguage(lang: string) {
    this.languageService.setLang(lang);
  }

  private updateUI(lang: string) {
    // 更新組件狀態或執行其他邏輯
    console.log('UI 更新為語言:', lang);
  }
}

使用 Async Pipe (推薦)

import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { LanguageService } from '../../base/services/language.service';

@Component({
  selector: 'app-language-indicator',
  template: `
    <!-- Async pipe 自動處理訂閱和取消訂閱 -->
    <div>當前語言: {{ currentLang$ | async }}</div>
    <div>語言事件: {{ languageEvent$ | async | json }}</div>
  `
})
export class LanguageIndicatorComponent {

  // 映射為語言代碼
  currentLang$ = this.languageService.language$.pipe(
    map(event => event.lang)
  );

  // 直接使用完整事件
  languageEvent$ = this.languageService.language$;

  constructor(private languageService: LanguageService) {}

  // 不需要手動取消訂閱,Async pipe 會處理
}

2. 在服務中使用

服務間通信

import { Injectable } from '@angular/core';
import { LanguageService } from './language.service';

@Injectable({
  providedIn: 'root'
})
export class TranslationService {

  constructor(private languageService: LanguageService) {
    // 在服務中訂閱語言變化
    this.languageService.language$.subscribe(event => {
      console.log('重新載入翻譯文件:', event.lang);
      this.loadTranslations(event.lang);
    });
  }

  private loadTranslations(lang: string) {
    // 載入對應語言的翻譯文件
  }
}

組合多個 Observable

import { Injectable } from '@angular/core';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { LanguageService } from './language.service';
import { UserService } from './user.service';

@Injectable({
  providedIn: 'root'
})
export class LocalizationService {

  // 組合語言和用戶偏好設定
  localizedSettings$ = combineLatest([
    this.languageService.language$,
    this.userService.userPreferences$
  ]).pipe(
    map(([langEvent, preferences]) => ({
      language: langEvent.lang,
      locale: this.getLocale(langEvent.lang, preferences.region)
    }))
  );

  constructor(
    private languageService: LanguageService,
    private userService: UserService
  ) {}

  private getLocale(lang: string, region: string): string {
    // 返回完整的 locale 字符串,如 'zh-TW', 'en-US'
    return `${lang}-${region}`;
  }
}

3. 在指令中使用

import { Directive, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { LanguageService } from '../services/language.service';

@Directive({
  selector: '[appLanguageAware]'
})
export class LanguageAwareDirective implements OnInit, OnDestroy {

  private subscription?: Subscription;

  constructor(private languageService: LanguageService) {}

  ngOnInit() {
    this.subscription = this.languageService.language$.subscribe(event => {
      // 根據語言變化更新指令行為
      this.updateDirectiveBehavior(event.lang);
    });
  }

  ngOnDestroy() {
    this.subscription?.unsubscribe();
  }

  private updateDirectiveBehavior(lang: string) {
    // 更新指令邏輯
  }
}

4. 在 Guard 中使用

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Observable, map } from 'rxjs';
import { LanguageService } from '../services/language.service';

@Injectable({
  providedIn: 'root'
})
export class LanguageGuard implements CanActivate {

  constructor(private languageService: LanguageService) {}

  canActivate(): Observable<boolean> {
    // 確保語言已初始化
    return this.languageService.language$.pipe(
      map(event => {
        const supportedLanguages = ['en-us', 'zh-tw', 'zh-cn'];
        return supportedLanguages.includes(event.lang);
      })
    );
  }
}

5. 最佳實踐

記憶體管理

// ✅ 正確:使用 takeUntil 模式
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

export class MyComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    this.languageService.language$
      .pipe(takeUntil(this.destroy$))
      .subscribe(event => {
        // 處理邏輯
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

錯誤處理

// ✅ 正確:處理錯誤
this.languageService.language$.subscribe({
  next: (event) => {
    console.log('語言改變:', event.lang);
  },
  error: (error) => {
    console.error('語言服務錯誤:', error);
  },
  complete: () => {
    console.log('語言監聽完成');
  }
});

共享訂閱

// ✅ 正確:使用 shareReplay 避免重複訂閱
import { shareReplay } from 'rxjs/operators';

// 在服務中
export class SharedDataService {
  sharedLanguage$ = this.languageService.language$.pipe(
    shareReplay(1)  // 共享最新的值給所有訂閱者
  );

  constructor(private languageService: LanguageService) {}
}

6. 常見錯誤和解決方案

錯誤 1: 忘記取消訂閱

// ❌ 錯誤:造成記憶體洩漏
export class BadComponent {
  ngOnInit() {
    this.languageService.language$.subscribe(event => {
      // 組件銷毀後訂閱仍然存在
    });
  }
}

// ✅ 正確:正確取消訂閱
export class GoodComponent implements OnDestroy {
  private subscription?: Subscription;

  ngOnInit() {
    this.subscription = this.languageService.language$.subscribe(event => {
      // 處理邏輯
    });
  }

  ngOnDestroy() {
    this.subscription?.unsubscribe();
  }
}

錯誤 2: 在訂閱中修改狀態

// ❌ 錯誤:在訂閱回調中修改外部狀態
export class BadComponent {
  currentLang = 'en-us';

  ngOnInit() {
    this.languageService.language$.subscribe(event => {
      this.currentLang = event.lang;
      // 這裡不應該有複雜邏輯
    });
  }
}

// ✅ 正確:保持訂閱回調簡單
export class GoodComponent {
  currentLang$ = this.languageService.language$.pipe(
    map(event => event.lang)
  );

  constructor(private languageService: LanguageService) {}
}

7. 測試中的使用

// 在測試中使用
describe('MyComponent', () => {
  let languageServiceSpy: jasmine.SpyObj<LanguageService>;

  beforeEach(() => {
    const spy = jasmine.createSpyObj('LanguageService', [], {
      language$: of({ lang: 'en-us' } as LangChangeEvent)
    });
    languageServiceSpy = spy;
  });

  it('應該訂閱語言變化', () => {
    // 測試訂閱邏輯
  });
});

總結

使用原則

  1. 總是取消訂閱 - 防止記憶體洩漏
  2. 使用 Async Pipe - 在模板中自動管理訂閱
  3. 保持訂閱簡單 - 複雜邏輯放在 pipe 操作符中
  4. 處理錯誤 - 總是處理可能的錯誤情況
  5. 共享熱流 - 使用 shareReplay 優化性能

選擇使用方式

  • 組件: Async Pipe 或 takeUntil 模式
  • 服務: 直接訂閱(服務通常是單例)
  • 指令/Guard: takeUntil 模式
  • 測試: 使用測試替身 (spy) d:\wpg\shipment-notice-platform-web\using-language-observable.md