Angular通用资源服务如何支持返回分页及其他自定义元数据
前后端对齐响应结构规范
首先确认后端接口返回格式调整为外层包装结构,不再直接返回数组,外层结构至少包含业务数据字段、分页元数据字段。
定义通用类型接口
使用泛型定义响应结构可以保证TypeScript的类型推导能力,避免出现any类型丢失类型校验的问题
// 分页元数据通用接口,可根据业务需求扩展字段 export interface PaginationMeta { currentPage: number; pageSize: number; totalItems: number; totalPages: number; hasNext: boolean; hasPrev: boolean; } // 通用API响应结构,T为业务数据的类型 export interface ApiResponse<T> { data: T[]; meta: PaginationMeta; // 可扩展其他全局元字段,比如响应状态码、提示信息等 code?: number; message?: string; }
改造通用HTTP服务
将原来直接返回数组类型的请求方法,调整为返回上述定义的通用响应结构:
import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { ApiResponse, PaginationMeta } from './interfaces/api.interface'; @Injectable({ providedIn: 'root' }) export class CommonHttpService { private apiBaseUrl = '/api'; // 通用列表请求方法 getList<T>( endpoint: string, pageParams?: { page: number; size: number }, otherParams?: Record<string, string | number> ): Observable<ApiResponse<T>> { let params = new HttpParams(); // 拼接分页参数 if (pageParams) { params = params.set('page', pageParams.page.toString()) .set('size', pageParams.size.toString()); } // 拼接其他查询参数 if (otherParams) { Object.keys(otherParams).forEach(key => { params = params.set(key, otherParams[key].toString()); }) } return this.http.get<ApiResponse<T>>(`${this.apiBaseUrl}/${endpoint}`, { params }); } } // 业务服务继承通用服务示例,比如用户服务 @Injectable({ providedIn: 'root' }) export class UserService extends CommonHttpService { // 获取用户列表,指定业务数据类型为User getUsers(page: number, size: number, filters?: { keyword?: string }): Observable<ApiResponse<User>> { return this.getList<User>('users', { page, size }, filters); } } // 业务数据类型示例 interface User { id: number; name: string; email: string; }
组件侧调用逻辑
组件中直接从响应结构中拆分获取业务数据和分页元数据即可:
import { Component, OnInit } from '@angular/core'; import { UserService } from './user.service'; import { PaginationMeta } from './interfaces/api.interface'; import { User } from './user.service'; @Component({ selector: 'app-user-list', template: '' }) export class UserListComponent implements OnInit { userList: User[] = []; pagination: PaginationMeta = { currentPage: 1, pageSize: 10, totalItems: 0, totalPages: 0, hasNext: false, hasPrev: false }; constructor(private userService: UserService) {} ngOnInit(): void { this.loadUserList(); } loadUserList(): void { this.userService.getUsers(this.pagination.currentPage, this.pagination.pageSize) .subscribe({ next: (res) => { this.userList = res.data; this.pagination = res.meta; }, error: (err) => { console.error('获取用户列表失败', err); } }) } // 分页切换触发方法 onPageChange(newPage: number): void { this.pagination.currentPage = newPage; this.loadUserList(); } }
兼容旧接口可选方案
如果部分历史接口无法调整后端返回格式,可通过Angular响应拦截器统一将数组类型的返回值包装为通用响应结构,分页元数据可从响应头中获取:
import { Injectable } from '@angular/core'; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ApiResponse } from './interfaces/api.interface'; @Injectable() export class ResponseWrapInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(req).pipe( map((event: HttpEvent<any>) => { // 仅处理返回数组类型的响应 if (event instanceof HttpResponse && Array.isArray(event.body)) { const wrappedBody: ApiResponse<any> = { data: event.body, meta: { currentPage: Number(event.headers.get('X-Current-Page')) || 1, pageSize: Number(event.headers.get('X-Page-Size')) || event.body.length, totalItems: Number(event.headers.get('X-Total-Count')) || event.body.length, totalPages: Number(event.headers.get('X-Total-Pages')) || 1, hasNext: event.headers.get('X-Has-Next') === 'true', hasPrev: event.headers.get('X-Has-Prev') === 'true' } }; return event.clone({ body: wrappedBody }); } return event; }) ); } }
使用拦截器需要在AppModule的providers中注册:
providers: [ { provide: HTTP_INTERCEPTORS, useClass: ResponseWrapInterceptor, multi: true } ]
内容的提问来源于stack exchange,提问作者niskah
相关产品推荐
相关产品推荐

