Chapter 05

HTTP 与数据请求

HttpClient、RxJS 操作符链、拦截器认证 Token,构建企业级 API 通信层

1. 配置 HttpClient

Angular 的 HttpClient 是内置的 HTTP 通信模块,基于 RxJS Observable。在 Standalone 应用中,通过 provideHttpClient() 注册。

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(
      withInterceptors([authInterceptor])  // 注册拦截器
    ),
  ]
};

2. HttpClient CRUD 操作

import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, catchError, retry } from 'rxjs/operators';
import { environment } from '../environments/environment';

export interface Post {
  id: number;
  title: string;
  content: string;
  authorId: number;
  createdAt: string;
}

@Injectable({ providedIn: 'root' })
export class PostService {
  private http = inject(HttpClient);
  private baseUrl = `${environment.apiUrl}/posts`;

  // GET 列表(带查询参数)
  getPosts(page = 1, pageSize = 10): Observable<Post[]> {
    const params = new HttpParams()
      .set('page', page)
      .set('pageSize', pageSize);

    return this.http.get<Post[]>(this.baseUrl, { params }).pipe(
      retry(2),               // 失败时自动重试 2 次
      catchError(this.handleError)
    );
  }

  // GET 单个
  getPost(id: number): Observable<Post> {
    return this.http.get<Post>(`${this.baseUrl}/${id}`);
  }

  // POST 创建
  createPost(data: Omit<Post, 'id' | 'createdAt'>): Observable<Post> {
    return this.http.post<Post>(this.baseUrl, data);
  }

  // PUT 更新(全量替换)
  updatePost(id: number, data: Partial<Post>): Observable<Post> {
    return this.http.put<Post>(`${this.baseUrl}/${id}`, data);
  }

  // PATCH 局部更新
  patchPost(id: number, data: Partial<Post>): Observable<Post> {
    return this.http.patch<Post>(`${this.baseUrl}/${id}`, data);
  }

  // DELETE
  deletePost(id: number): Observable<void> {
    return this.http.delete<void>(`${this.baseUrl}/${id}`);
  }

  private handleError(error: any): Observable<never> {
    console.error('API 错误:', error);
    throw error;
  }
}

3. RxJS 常用操作符

map — 转换数据

import { map } from 'rxjs/operators';

this.http.get<ApiResponse<User[]>>('/api/users').pipe(
  // 从 { data: User[], total: number } 中提取 data
  map(response => response.data)
).subscribe(users => this.users = users);

catchError — 错误处理

import { catchError, of, EMPTY } from 'rxjs';

this.postService.getPost(id).pipe(
  catchError(err => {
    if (err.status === 404) {
      this.router.navigate(['/not-found']);
      return EMPTY;  // 不发出任何值
    }
    return of(null);  // 返回默认值
  })
).subscribe(post => this.post = post);

switchMap — 切换内层 Observable

import { switchMap } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';

// 路由参数变化时,取消前一次请求,发起新请求
this.route.params.pipe(
  switchMap(params => this.postService.getPost(+params['id']))
).subscribe(post => this.post = post);

debounceTime — 搜索防抖

import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { Subject } from 'rxjs';

export class SearchComponent {
  private searchSubject = new Subject<string>();

  results$ = this.searchSubject.pipe(
    debounceTime(300),              // 停止输入 300ms 后才发起请求
    distinctUntilChanged(),          // 值未变化则不重复请求
    switchMap(query =>               // 切换到最新请求
      this.searchService.search(query)
    )
  );

  onSearch(event: Event) {
    this.searchSubject.next((event.target as HTMLInputElement).value);
  }
}

4. HTTP 拦截器

拦截器是 HTTP 请求/响应管道的中间件,可在请求发出前(添加 token、日志)或响应到达后(处理错误、转换数据)统一处理。

// auth.interceptor.ts — 函数式拦截器(Angular 15+)
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { AuthService } from '../services/auth.service';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthService);
  const token = localStorage.getItem('token');

  // 如果有 token,克隆请求并添加 Authorization 头
  const authReq = token
    ? req.clone({
        headers: req.headers.set('Authorization', `Bearer ${token}`)
      })
    : req;

  return next(authReq).pipe(
    catchError(err => {
      if (err.status === 401) {
        // Token 过期,清除登录状态
        auth.logout();
      }
      return throwError(() => err);
    })
  );
};

// loading.interceptor.ts — 全局 loading 状态
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
  const loading = inject(LoadingService);
  loading.show();
  return next(req).pipe(
    finalize(() => loading.hide())
  );
};

5. 环境变量配置

// src/environments/environment.ts(开发环境)
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/api',
  wsUrl: 'ws://localhost:3000',
};

// src/environments/environment.prod.ts(生产环境)
export const environment = {
  production: true,
  apiUrl: 'https://api.myapp.com/api',
  wsUrl: 'wss://api.myapp.com',
};

// 在 angular.json 的 fileReplacements 中配置替换规则
// ng build --configuration production 自动替换

6. 实战:数据列表 CRUD 组件

@Component({
  selector: 'app-post-list',
  standalone: true,
  imports: [AsyncPipe, DatePipe],
  template: `
    <div class="toolbar">
      <button (click)="loadPosts()">刷新</button>
      <button (click)="openCreateDialog()">新建</button>
    </div>

    @if (loading()) {
      <app-spinner />
    } @else if (error()) {
      <div class="error">{{ error() }}</div>
    } @else {
      <table>
        @for (post of posts(); track post.id) {
          <tr>
            <td>{{ post.title }}</td>
            <td>{{ post.createdAt | date:'yyyy-MM-dd' }}</td>
            <td>
              <button (click)="editPost(post)">编辑</button>
              <button (click)="deletePost(post.id)">删除</button>
            </td>
          </tr>
        }
      </table>
    }
  `
})
export class PostListComponent implements OnInit {
  private postService = inject(PostService);

  posts = signal<Post[]>([]);
  loading = signal(false);
  error = signal<string | null>(null);

  ngOnInit() { this.loadPosts(); }

  loadPosts() {
    this.loading.set(true);
    this.error.set(null);
    this.postService.getPosts().subscribe({
      next: posts => this.posts.set(posts),
      error: err => this.error.set(err.message),
      complete: () => this.loading.set(false)
    });
  }

  deletePost(id: number) {
    if (!confirm('确定删除?')) return;
    this.postService.deletePost(id).subscribe(() => {
      this.posts.update(posts => posts.filter(p => p.id !== id));
    });
  }
}

本章小结:Angular HttpClient 提供类型安全的 HTTP 通信。拦截器统一处理认证和错误,RxJS 操作符链使数据转换和异步控制更优雅。下一章深入 Angular 表单系统。