AngularHTTP客户端与API调用

Angular提供了强大的HttpClient模块来处理HTTP请求,支持RESTful API调用、请求拦截、响应处理等功能。

前置要求:确保已在AppModule中导入HttpClientModule

1. 基础配置

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http'; // 导入HttpClientModule

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    HttpClientModule // 添加到imports数组
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

2. 创建数据服务

最佳实践是将HTTP请求封装在服务中:

// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, map } from 'rxjs/operators';

export interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

@Injectable({
  providedIn: 'root' // 根注入,单例模式
})
export class UserService {
  private apiUrl = 'https://api.example.com/users';

  constructor(private http: HttpClient) { }

  // GET请求 - 获取所有用户
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }

  // GET请求 - 获取单个用户
  getUser(id: number): Observable<User> {
    const url = `${this.apiUrl}/${id}`;
    return this.http.get<User>(url);
  }

  // POST请求 - 创建用户
  createUser(user: User): Observable<User> {
    return this.http.post<User>(this.apiUrl, user, {
      headers: new HttpHeaders({
        'Content-Type': 'application/json'
      })
    });
  }

  // PUT请求 - 更新用户
  updateUser(id: number, user: User): Observable<User> {
    const url = `${this.apiUrl}/${id}`;
    return this.http.put<User>(url, user);
  }

  // DELETE请求 - 删除用户
  deleteUser(id: number): Observable<void> {
    const url = `${this.apiUrl}/${id}`;
    return this.http.delete<void>(url);
  }

  // 带查询参数的GET请求
  searchUsers(term: string): Observable<User[]> {
    const params = new HttpParams()
      .set('q', term)
      .set('_limit', '10');

    return this.http.get<User[]>(this.apiUrl, { params });
  }
}

3. 在组件中使用服务

// user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { UserService, User } from './user.service';

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html'
})
export class UserListComponent implements OnInit {
  users: User[] = [];
  loading = false;
  error = '';

  constructor(private userService: UserService) { }

  ngOnInit(): void {
    this.loadUsers();
  }

  loadUsers(): void {
    this.loading = true;
    this.userService.getUsers().subscribe({
      next: (data) => {
        this.users = data;
        this.loading = false;
      },
      error: (err) => {
        this.error = '加载用户数据失败: ' + err.message;
        this.loading = false;
      },
      complete: () => {
        console.log('用户数据加载完成');
      }
    });
  }

  createUser(): void {
    const newUser: User = {
      id: 0, // 服务器端会生成ID
      name: '新用户',
      email: 'newuser@example.com',
      createdAt: new Date()
    };

    this.userService.createUser(newUser).subscribe({
      next: (createdUser) => {
        this.users.push(createdUser);
        alert('用户创建成功');
      },
      error: (err) => {
        console.error('创建用户失败:', err);
        alert('创建用户失败');
      }
    });
  }

  deleteUser(id: number): void {
    if (confirm('确定要删除这个用户吗?')) {
      this.userService.deleteUser(id).subscribe({
        next: () => {
          this.users = this.users.filter(user => user.id !== id);
        },
        error: (err) => {
          console.error('删除用户失败:', err);
        }
      });
    }
  }
}
<!-- user-list.component.html -->
<div class="user-list">
  <h2>用户列表</h2>

  <div *ngIf="loading" class="loading">
    <i class="fas fa-spinner fa-spin"></i> 加载中...
  </div>

  <div *ngIf="error" class="alert alert-danger">
    {{error}}
  </div>

  <button (click)="createUser()" class="btn btn-primary mb-3">
    创建新用户
  </button>

  <table class="table table-striped" *ngIf="users.length > 0">
    <thead>
      <tr>
        <th>ID</th>
        <th>姓名</th>
        <th>邮箱</th>
        <th>创建时间</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let user of users">
        <td>{{user.id}}</td>
        <td>{{user.name}}</td>
        <td>{{user.email}}</td>
        <td>{{user.createdAt | date}}</td>
        <td>
          <button (click)="deleteUser(user.id)" class="btn btn-danger btn-sm">
            删除
          </button>
        </td>
      </tr>
    </tbody>
  </table>

  <div *ngIf="users.length === 0 && !loading" class="alert alert-info">
    暂无用户数据
  </div>
</div>

4. 错误处理

使用RxJS操作符进行错误处理:

// 在服务中添加错误处理
export class UserService {
  private handleError(error: any): Observable<never> {
    let errorMessage = '';

    if (error.error instanceof ErrorEvent) {
      // 客户端错误
      errorMessage = `客户端错误: ${error.error.message}`;
    } else {
      // 服务器端错误
      errorMessage = `服务器错误: ${error.status} - ${error.statusText || ''} ${error.error || ''}`;
    }

    console.error(errorMessage);
    return throwError(() => new Error(errorMessage));
  }

  // 带错误处理的GET请求
  getUsersSafe(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl).pipe(
      retry(2), // 失败时重试2次
      catchError(this.handleError)
    );
  }

  // 处理特定状态码
  getUserWithStatusHandling(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`, {
      observe: 'response' // 获取完整响应对象
    }).pipe(
      map(response => {
        if (response.status === 200) {
          return response.body!;
        } else if (response.status === 404) {
          throw new Error('用户不存在');
        } else {
          throw new Error(`请求失败,状态码: ${response.status}`);
        }
      }),
      catchError(this.handleError)
    );
  }
}

5. HTTP拦截器

拦截器用于在请求发出前或响应返回前进行统一处理:

// auth.interceptor.ts - 认证拦截器
import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Router } from '@angular/router';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private router: Router) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // 获取token
    const token = localStorage.getItem('auth_token');

    // 克隆请求并添加认证头
    let authRequest = request;
    if (token) {
      authRequest = request.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
    }

    // 添加公共请求头
    authRequest = authRequest.clone({
      setHeaders: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      }
    });

    return next.handle(authRequest).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          // 未授权,跳转到登录页
          this.router.navigate(['/login']);
        } else if (error.status === 403) {
          // 禁止访问
          alert('权限不足,无法访问该资源');
        } else if (error.status === 0) {
          // 网络错误
          console.error('网络连接错误,请检查网络设置');
        }
        return throwError(() => error);
      })
    );
  }
}
// logging.interceptor.ts - 日志拦截器
import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpResponse
} from '@angular/common/http';
import { Observable, tap } from 'rxjs';
import { environment } from '../environments/environment';

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const startTime = Date.now();

    // 只在开发环境记录日志
    if (!environment.production) {
      console.log(`请求开始: ${request.method} ${request.url}`);
    }

    return next.handle(request).pipe(
      tap(
        event => {
          if (event instanceof HttpResponse) {
            const elapsed = Date.now() - startTime;
            if (!environment.production) {
              console.log(`请求完成: ${request.method} ${request.url} (${elapsed}ms)`);
            }
          }
        },
        error => {
          const elapsed = Date.now() - startTime;
          console.error(`请求失败: ${request.method} ${request.url} (${elapsed}ms)`, error);
        }
      )
    );
  }
}
// 注册拦截器
// app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './interceptors/auth.interceptor';
import { LoggingInterceptor } from './interceptors/logging.interceptor';

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: LoggingInterceptor,
      multi: true
    }
  ]
})
export class AppModule { }

6. 环境配置

// environments/environment.ts - 开发环境
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/api',
  apiKey: 'dev-key-123',
  version: '1.0.0'
};

// environments/environment.prod.ts - 生产环境
export const environment = {
  production: true,
  apiUrl: 'https://api.production.com/api',
  apiKey: 'prod-key-456',
  version: '1.0.0'
};

// 在服务中使用环境变量
import { environment } from '../environments/environment';

export class UserService {
  private apiUrl = `${environment.apiUrl}/users`;

  constructor(private http: HttpClient) { }
}

7. 完整的API服务示例

// api.service.ts - 通用API服务
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map, timeout, retry } from 'rxjs/operators';
import { environment } from '../environments/environment';

export interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: Date;
}

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private readonly baseUrl = environment.apiUrl;
  private readonly defaultTimeout = 10000; // 10秒超时

  constructor(private http: HttpClient) { }

  // 通用GET方法
  get<T>(endpoint: string, params?: any): Observable<ApiResponse<T>> {
    let httpParams = new HttpParams();

    if (params) {
      Object.keys(params).forEach(key => {
        if (params[key] !== null && params[key] !== undefined) {
          httpParams = httpParams.set(key, params[key].toString());
        }
      });
    }

    return this.http.get<ApiResponse<T>>(`${this.baseUrl}/${endpoint}`, {
      params: httpParams
    }).pipe(
      timeout(this.defaultTimeout),
      retry(1),
      catchError(this.handleError)
    );
  }

  // 通用POST方法
  post<T>(endpoint: string, body: any): Observable<ApiResponse<T>> {
    return this.http.post<ApiResponse<T>>(
      `${this.baseUrl}/${endpoint}`,
      body,
      {
        headers: new HttpHeaders({
          'Content-Type': 'application/json'
        })
      }
    ).pipe(
      timeout(this.defaultTimeout),
      catchError(this.handleError)
    );
  }

  // 通用PUT方法
  put<T>(endpoint: string, body: any): Observable<ApiResponse<T>> {
    return this.http.put<ApiResponse<T>>(
      `${this.baseUrl}/${endpoint}`,
      body
    ).pipe(
      timeout(this.defaultTimeout),
      catchError(this.handleError)
    );
  }

  // 通用DELETE方法
  delete<T>(endpoint: string): Observable<ApiResponse<T>> {
    return this.http.delete<ApiResponse<T>>(
      `${this.baseUrl}/${endpoint}`
    ).pipe(
      timeout(this.defaultTimeout),
      catchError(this.handleError)
    );
  }

  // 文件上传
  uploadFile<T>(endpoint: string, file: File): Observable<ApiResponse<T>> {
    const formData = new FormData();
    formData.append('file', file);

    return this.http.post<ApiResponse<T>>(
      `${this.baseUrl}/${endpoint}`,
      formData
    ).pipe(
      catchError(this.handleError)
    );
  }

  // 错误处理
  private handleError(error: any): Observable<never> {
    console.error('API调用错误:', error);

    let userMessage = '请求失败,请稍后重试';

    if (error.status === 0) {
      userMessage = '网络连接失败,请检查网络设置';
    } else if (error.status === 401) {
      userMessage = '登录已过期,请重新登录';
    } else if (error.status === 404) {
      userMessage = '请求的资源不存在';
    } else if (error.status >= 500) {
      userMessage = '服务器错误,请稍后重试';
    }

    return throwError(() => ({
      message: userMessage,
      originalError: error
    }));
  }
}

8. 使用示例

GET /api/users
POST /api/users
PUT /api/users/:id
DELETE /api/users/:id

9. 最佳实践

推荐做法:
  • 将API URL配置在环境文件中
  • 为不同类型的API创建专门的服务
  • 使用拦截器处理公共逻辑(认证、日志等)
  • 实现适当的错误处理和用户反馈
  • 为长时间请求添加加载状态
  • 使用TypeScript接口定义响应数据结构
  • 在生产环境禁用详细错误信息
  • 实现请求取消机制避免内存泄漏
注意事项:
  • 避免在组件中直接使用HttpClient,应封装在服务中
  • 处理订阅的内存泄漏,使用async pipe或手动取消订阅
  • 注意CORS(跨域资源共享)配置
  • 实现请求重试机制时注意幂等性
  • 对敏感数据使用HTTPS加密传输
  • 限制并发请求数量,避免服务器过载
  • 为上传/下载大文件实现进度提示

10. 常见问题解决

CORS问题

如果遇到跨域问题,需要在服务器端配置CORS头,或在开发环境中配置代理:

// proxy.conf.json
{
  "/api": {
    "target": "http://localhost:3000",
    "secure": false,
    "changeOrigin": true,
    "logLevel": "debug"
  }
}

angular.json中配置代理:

"serve": {
  "builder": "@angular-devkit/build-angular:dev-server",
  "options": {
    "proxyConfig": "proxy.conf.json"
  }
}