You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 04:15:04