服务基础 #

一、什么是服务 #

服务是一个广义的概念,用于封装业务逻辑、数据访问、工具函数等。服务可以被多个组件共享,实现代码复用和关注点分离。

text
┌─────────────────────────────────────────────────────┐
│                    Angular 应用                      │
├─────────────────────────────────────────────────────┤
│  ┌─────────┐  ┌─────────┐  ┌─────────┐             │
│  │ 组件A   │  │ 组件B   │  │ 组件C   │             │
│  └────┬────┘  └────┬────┘  └────┬────┘             │
│       │            │            │                   │
│       └────────────┼────────────┘                   │
│                    ↓                                │
│            ┌──────────────┐                         │
│            │    服务      │                         │
│            │  共享逻辑    │                         │
│            └──────────────┘                         │
└─────────────────────────────────────────────────────┘

二、创建服务 #

2.1 使用CLI创建 #

bash
ng generate service services/user
# 简写
ng g s services/user

2.2 服务基本结构 #

typescript
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  constructor() { }
  
  getUsers(): User[] {
    return [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ];
  }
}

2.3 @Injectable装饰器 #

typescript
@Injectable({
  providedIn: 'root'  // 在根模块中提供,全局单例
})
export class UserService { }

@Injectable({
  providedIn: 'any'   // 每个懒加载模块单独实例
})
export class FeatureService { }

@Injectable({
  providedIn: UserModule  // 在指定模块中提供
})
export class ModuleService { }

三、服务注册方式 #

3.1 providedIn: ‘root’(推荐) #

typescript
@Injectable({
  providedIn: 'root'
})
export class UserService {
  // 全局单例,应用启动时创建
}

3.2 模块级别注册 #

typescript
@NgModule({
  providers: [UserService]
})
export class UserModule { }

3.3 组件级别注册 #

typescript
@Component({
  selector: 'app-user',
  template: '...',
  providers: [UserService]  // 每个组件实例独立的服务实例
})
export class UserComponent { }

3.4 注册方式对比 #

方式 作用域 实例数量 适用场景
providedIn: 'root' 全局 单例 全局共享服务
模块级别 模块内 单例 模块内共享
组件级别 组件内 每组件一个 组件独立状态

四、使用服务 #

4.1 在组件中使用 #

typescript
import { Component, OnInit } from '@angular/core';
import { UserService } from '../services/user.service';

@Component({
  selector: 'app-user-list',
  template: `
    <ul>
      <li *ngFor="let user of users">{{ user.name }}</li>
    </ul>
  `
})
export class UserListComponent implements OnInit {
  users: User[] = [];
  
  constructor(private userService: UserService) {}
  
  ngOnInit() {
    this.users = this.userService.getUsers();
  }
}

4.2 服务间依赖 #

typescript
@Injectable({
  providedIn: 'root'
})
export class ApiService {
  constructor(private http: HttpClient) {}
  
  get<T>(url: string) {
    return this.http.get<T>(url);
  }
}

@Injectable({
  providedIn: 'root'
})
export class UserService {
  constructor(private apiService: ApiService) {}
  
  getUsers() {
    return this.apiService.get<User[]>('/api/users');
  }
}

五、服务类型示例 #

5.1 数据服务 #

typescript
@Injectable({
  providedIn: 'root'
})
export class DataService {
  private dataSubject = new BehaviorSubject<any[]>([]);
  data$ = this.dataSubject.asObservable();
  
  private loadingSubject = new BehaviorSubject<boolean>(false);
  loading$ = this.loadingSubject.asObservable();
  
  constructor(private http: HttpClient) {}
  
  loadData() {
    this.loadingSubject.next(true);
    this.http.get<any[]>('/api/data').subscribe({
      next: (data) => {
        this.dataSubject.next(data);
        this.loadingSubject.next(false);
      },
      error: (error) => {
        this.loadingSubject.next(false);
        console.error(error);
      }
    });
  }
  
  addItem(item: any) {
    const currentData = this.dataSubject.getValue();
    this.dataSubject.next([...currentData, item]);
  }
  
  removeItem(id: number) {
    const currentData = this.dataSubject.getValue();
    this.dataSubject.next(currentData.filter(item => item.id !== id));
  }
}

5.2 配置服务 #

typescript
@Injectable({
  providedIn: 'root'
})
export class ConfigService {
  private config: AppConfig;
  
  constructor() {
    this.config = {
      apiUrl: 'https://api.example.com',
      timeout: 30000,
      pageSize: 10
    };
  }
  
  get apiUrl(): string {
    return this.config.apiUrl;
  }
  
  get timeout(): number {
    return this.config.timeout;
  }
  
  get pageSize(): number {
    return this.config.pageSize;
  }
  
  updateConfig(newConfig: Partial<AppConfig>) {
    this.config = { ...this.config, ...newConfig };
  }
}

5.3 日志服务 #

typescript
@Injectable({
  providedIn: 'root'
})
export class LogService {
  private logs: LogEntry[] = [];
  
  log(message: string, level: 'info' | 'warn' | 'error' = 'info') {
    const entry: LogEntry = {
      timestamp: new Date(),
      message,
      level
    };
    
    this.logs.push(entry);
    
    switch (level) {
      case 'info':
        console.log(`[INFO] ${message}`);
        break;
      case 'warn':
        console.warn(`[WARN] ${message}`);
        break;
      case 'error':
        console.error(`[ERROR] ${message}`);
        break;
    }
  }
  
  getLogs(): LogEntry[] {
    return [...this.logs];
  }
  
  clearLogs() {
    this.logs = [];
  }
}

5.4 认证服务 #

typescript
@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private currentUserSubject = new BehaviorSubject<User | null>(null);
  currentUser$ = this.currentUserSubject.asObservable();
  
  private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
  isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
  
  constructor(
    private http: HttpClient,
    private router: Router
  ) {
    this.checkAuth();
  }
  
  private checkAuth() {
    const token = localStorage.getItem('token');
    if (token) {
      this.validateToken(token);
    }
  }
  
  login(credentials: LoginCredentials): Observable<User> {
    return this.http.post<AuthResponse>('/api/auth/login', credentials).pipe(
      tap(response => {
        localStorage.setItem('token', response.token);
        this.currentUserSubject.next(response.user);
        this.isAuthenticatedSubject.next(true);
      }),
      map(response => response.user)
    );
  }
  
  logout() {
    localStorage.removeItem('token');
    this.currentUserSubject.next(null);
    this.isAuthenticatedSubject.next(false);
    this.router.navigate(['/login']);
  }
  
  get currentUser(): User | null {
    return this.currentUserSubject.getValue();
  }
  
  get isAuthenticated(): boolean {
    return this.isAuthenticatedSubject.getValue();
  }
  
  private validateToken(token: string) {
    this.http.get<User>('/api/auth/me').subscribe({
      next: (user) => {
        this.currentUserSubject.next(user);
        this.isAuthenticatedSubject.next(true);
      },
      error: () => {
        this.logout();
      }
    });
  }
}

5.5 缓存服务 #

typescript
@Injectable({
  providedIn: 'root'
})
export class CacheService {
  private cache = new Map<string, { data: any; expiry: number }>();
  private defaultTTL = 5 * 60 * 1000; // 5分钟
  
  get<T>(key: string): T | null {
    const cached = this.cache.get(key);
    
    if (!cached) {
      return null;
    }
    
    if (Date.now() > cached.expiry) {
      this.cache.delete(key);
      return null;
    }
    
    return cached.data as T;
  }
  
  set<T>(key: string, data: T, ttl: number = this.defaultTTL) {
    this.cache.set(key, {
      data,
      expiry: Date.now() + ttl
    });
  }
  
  has(key: string): boolean {
    return this.get(key) !== null;
  }
  
  delete(key: string) {
    this.cache.delete(key);
  }
  
  clear() {
    this.cache.clear();
  }
  
  getOrSet<T>(key: string, factory: () => Observable<T>, ttl?: number): Observable<T> {
    const cached = this.get<T>(key);
    
    if (cached !== null) {
      return of(cached);
    }
    
    return factory().pipe(
      tap(data => this.set(key, data, ttl))
    );
  }
}

六、服务与RxJS #

6.1 使用Observable #

typescript
@Injectable({
  providedIn: 'root'
})
export class ProductService {
  private productsSubject = new BehaviorSubject<Product[]>([]);
  products$ = this.productsSubject.asObservable();
  
  constructor(private http: HttpClient) {}
  
  loadProducts() {
    this.http.get<Product[]>('/api/products').subscribe(
      products => this.productsSubject.next(products)
    );
  }
  
  getProduct(id: number): Observable<Product | undefined> {
    return this.products$.pipe(
      map(products => products.find(p => p.id === id))
    );
  }
  
  addProduct(product: Product) {
    const current = this.productsSubject.getValue();
    this.productsSubject.next([...current, product]);
  }
}

6.2 使用Signal(Angular 17+) #

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

@Injectable({
  providedIn: 'root'
})
export class CounterService {
  private countSignal = signal(0);
  
  readonly count = this.countSignal.asReadonly();
  
  readonly doubleCount = computed(() => this.countSignal() * 2);
  
  readonly isEven = computed(() => this.countSignal() % 2 === 0);
  
  increment() {
    this.countSignal.update(v => v + 1);
  }
  
  decrement() {
    this.countSignal.update(v => v - 1);
  }
  
  reset() {
    this.countSignal.set(0);
  }
}

七、服务最佳实践 #

7.1 单一职责 #

typescript
// 好的做法:职责单一
@Injectable({ providedIn: 'root' })
export class UserService {
  // 只处理用户相关逻辑
}

@Injectable({ providedIn: 'root' })
export class AuthService {
  // 只处理认证相关逻辑
}

// 不好的做法:职责过多
@Injectable({ providedIn: 'root' })
export class AppService {
  // 处理用户、认证、产品、订单...
}

7.2 使用接口定义类型 #

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

interface UserFilter {
  name?: string;
  status?: 'active' | 'inactive';
}

@Injectable({ providedIn: 'root' })
export class UserService {
  getUsers(filter?: UserFilter): Observable<User[]> {
    let params = new HttpParams();
    
    if (filter?.name) {
      params = params.set('name', filter.name);
    }
    if (filter?.status) {
      params = params.set('status', filter.status);
    }
    
    return this.http.get<User[]>('/api/users', { params });
  }
}

7.3 错误处理 #

typescript
@Injectable({ providedIn: 'root' })
export class ApiService {
  constructor(private http: HttpClient) {}
  
  get<T>(url: string): Observable<T> {
    return this.http.get<T>(url).pipe(
      retry(3),
      catchError(this.handleError)
    );
  }
  
  private handleError(error: HttpErrorResponse) {
    let errorMessage = '发生未知错误';
    
    if (error.error instanceof ErrorEvent) {
      errorMessage = `客户端错误: ${error.error.message}`;
    } else {
      errorMessage = `服务器错误: ${error.status} - ${error.message}`;
    }
    
    return throwError(() => new Error(errorMessage));
  }
}

7.4 使用InjectionToken #

typescript
import { InjectionToken } from '@angular/core';

export const API_URL = new InjectionToken<string>('API_URL');
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');

// 在模块中提供
@NgModule({
  providers: [
    { provide: API_URL, useValue: 'https://api.example.com' },
    { provide: APP_CONFIG, useValue: { theme: 'light', lang: 'zh-CN' } }
  ]
})
export class AppModule { }

// 在服务中使用
@Injectable({ providedIn: 'root' })
export class ApiService {
  constructor(
    @Inject(API_URL) private apiUrl: string,
    @Inject(APP_CONFIG) private config: AppConfig
  ) {}
}

八、总结 #

概念 说明
@Injectable 标记类为可注入服务
providedIn 指定服务提供范围
单例服务 全局唯一实例
服务依赖 服务间可相互注入
RxJS 服务中处理异步数据

下一步:依赖注入

最后更新:2026-03-26