likes
comments
collection
share

Angular HTTP装饰器优雅后处理

作者站长头像
站长
· 阅读数 25

前言

在现代的前端开发中,与后端服务器进行HTTP通信是一项不可或缺的任务。虽然HTTP装饰器并不是Angular框架本身引入的功能,而是一种自行封装的工具,但它为Angular应用提供了一种强大而灵活的方式来处理HTTP请求。

HTTP装饰器允许我们以声明式的方式创建接口,而不需要编写大量冗余的代码。这种声明式的方法提高了代码的可读性和可维护性,同时降低了出错的可能性。不仅如此,HTTP装饰器还能够提供类型安全性,确保我们的代码与后端API保持一致,减少潜在的错误和调试工作。

然而,在实际开发中,从服务器返回的原始数据通常并不总是能够直接满足我们的需求。这时候,我们需要考虑如何在获取到HTTP响应后,对数据进行进一步的处理和转换,以满足特定的业务需求。本文将深入探讨在Angular应用中使用HTTP装饰器后处理的重要性和必要性。我们将通过具体的示例来演示,如何有效地利用后处理来处理HTTP响应数据。

什么是HTTP装饰器?

HTTP装饰器是基于Angualr Http模块封装的ts装饰器,它提供了一种声明式的方式来创建接口,以定义HTTP请求和响应的结构。通过使用HTTP装饰器,我们可以更加清晰、类型安全和高效地描述我们的HTTP通信需求。让我们深入了解HTTP装饰器的概念以及它的优势。

1. 声明式接口定义

HTTP装饰器允许我们在类中以装饰器的形式定义HTTP请求和响应的接口。这种声明式的方法使得接口更加清晰和易于理解。相比于传统的手动配置,它提供了更直观的方式来描述数据的结构。

2. 类型安全 (ts 5.0之后的版本)

由于HTTP装饰器使用了TypeScript的类型系统,因此它可以在编译时捕获潜在的类型错误。这意味着如果您尝试使用不正确的数据类型,编译器将会发出警告或错误,有助于提高代码的可靠性和可维护性。

3. 减少重复性代码

HTTP装饰器允许我们在多个地方重用HTTP请求和响应的结构。这意味着我们可以避免编写大量重复的代码,提高了代码的可维护性,并减少了错误的可能性。当API发生变化时,只需更新一处定义,而不是在整个代码库中查找和修改多处引用。

4. 更好的可维护性

通过将HTTP请求和响应的结构集中在一个地方定义,我们可以更容易地进行维护和更新。这种集中的方式使得对代码的修改更加一致,减少了错误的风险,并提高了代码的可维护性。

5. 增强可读性

HTTP装饰器提供了一种更加直观的方式来理解每个HTTP请求的用途和期望的响应结构。这有助于团队成员更快地理解代码的意图,并减少了学习和维护成本。

示例

为了更好地理解HTTP装饰器的作用,让我们来看一个示例,分别比较有装饰器和无装饰器的情况。

有装饰器的示例

@Injectable({ providedIn: 'root' })
class HttpTestService {
  @GET('users')
  getUser(): Observable<User[]> {
    return null as Observable<User[]>;
  }
}

无装饰器的示例

@Injectable({ providedIn: 'root' })
class HttpTestService {
  getUser(): Observable<User[]> {
    // 实际实现... 
  }
}

从上面示例来看,当遇到相应数据后处理装饰器的方式就很难优雅进行后处理了,然而,在实际开发中,从服务器返回的原始数据通常并不总是能够直接满足我们的需求。这时候,需要考虑如何在获取到HTTP响应后,对数据进行进一步的处理和转换,以满足特定的业务需求

添加后处理支持

在前一节中,我们已经了解了HTTP装饰器的优势。但通常,从服务器返回的原始数据仍然需要在客户端进行后续处理,以适应我们的具体需求。这时候,就可以尝试实现HTTP装饰器的后处理功能,对HTTP响应数据进行进一步的加工和转换。

接下来,探讨如何使用后处理功能,以及如何在HTTP请求中实现后处理,以满足更复杂的业务逻辑和数据处理需求。我们将通过具体的示例来演示后处理的功能和灵活性。

示例:

让我们首先看一个具体的示例,演示了如何在HTTP请求中优雅添加后处理支持。我们将使用一个简单的HTTP请求示例,该请求获取用户数据,并在后处理中过滤出年龄大于18岁的用户。

@Injectable({ providedIn: 'root' })
class HttpTestService {
  @GET('users')
  getUser(): Observable<User[]> {
    const { data } = useHttpContext<User[], 'response', 'json'>();
    return of(data).pipe(filter(({ age }) => age > 18));
  }
}

在上面的示例中,@GET装饰器用于声明HTTP请求的配置,然后在getUser方法内部,我们使用后处理技术来过滤出符合条件的用户。这个示例展示了如何使用后处理功能来处理HTTP响应数据,以满足特定的需求。

实现的难点

实现HTTP装饰器的后处理功能时,有一些考虑和难点需要注意。例如,如何在不破坏类型检查的情况下,将后处理结果与HTTP请求的响应类型相匹配是一个关键问题。

一种常见但可能不够优雅的方法是将数据作为方法的参数传递,如下所示:

@Injectable({ providedIn: 'root' })
class HttpTestService {
  @GET('users')
  getUser(@Query query: QueryModel, @Result data: User[]): Observable<User[]> {
    return of(data).pipe(filter(({ age }) => age > 18));
  }
}

尽管这种方法可以工作,但它可能会显得怪异,因为data参数实际上不应该是getUser方法的入参。此外,这种方式还会导致在调用getUser方法时出现类型错误,除非将data参数设置为可选参数。因此,需要仔细考虑如何设计接口,以便实现后处理功能,并在类型安全的前提下使用它们。

具体实现

开始我也是将放回的数据作为入参去实现的,但我我有的强迫症,这种方案我比较难以接受,后面想到js是单线程就灵光一现,利用js的单线程的特点优雅的解决这个问题。

  • httpContext.ts
export type PathParams = TypeObject<string | number>;
export type QueryParams = TypeObject<string | number | boolean | ReadonlyArray<string | number | boolean>>;

// 请求参数 这个不是重点 可以根据实际情况补充
class RequestParams {
  queryParams: QueryParams = {};
  pathParams: PathParams = {};
  body: SafaAny | null = null;
}

// 不考虑T为'body'的情况
class HttpContext<U = never, T = 'response', R = 'json'> {
  request = new RequestParams();
  response!: T extends 'events'
    ? R extends 'text'
      ? U extends never
        ? HttpEvent<string>
        : HttpEvent<U>
      : R extends 'json'
      ? U extends never
        ? HttpEvent<Object>
        : HttpEvent<U>
      : R extends 'blob'
      ? U extends never
        ? HttpEvent<Blob>
        : HttpEvent<U>
      : R extends 'arraybuffer'
      ? U extends never
        ? HttpEvent<ArrayBuffer>
        : HttpEvent<U>
      : never
    : T extends 'response'
    ? R extends 'text'
      ? U extends never
        ? HttpResponse<string>
        : HttpResponse<U>
      : R extends 'json'
      ? U extends never
        ? HttpResponse<Object>
        : HttpResponse<U>
      : R extends 'blob'
      ? U extends never
        ? HttpResponse<Blob>
        : HttpResponse<U>
      : R extends 'arraybuffer'
      ? U extends never
        ? HttpResponse<ArrayBuffer>
        : HttpResponse<U>
      : never
    : never;
}

首先,创建一个名为httpContext.ts的文件,其中包含了一些用于请求参数和HTTP上下文的定义。这些定义有助于我们更好地使用和处理HTTP请求和响应。

  • httpRequest.ts
let _context: HttpResponseContext<SafaAny, SafaAny, SafaAny>;

export function useHttpContext<
  U = never,
  T extends 'events' | 'response' = 'response',
  R extends 'text' | 'json' | 'blob' | 'arraybuffer' = 'json'
>(): HttpResponseContext<U, T, R> {
  return _context;
}

interface HttpClientOptions {
  body?: SafaAny;
  headers?:
    | HttpHeaders
    | {
        [header: string]: string | string[];
      };
  context?: HttpContext;
  params?:
    | HttpParams
    | {
        [param: string]: string | number | boolean | ReadonlyArray<string | number | boolean>;
      };
  observe?: 'response' | 'events';
  reportProgress?: boolean;
  responseType?: 'arraybuffer' | 'blob' | 'json' | 'text';
  withCredentials?: boolean;
}

interface HttpOptions extends HttpClientOptions {
  url: string;
}

function httpRequest<T, This, Args extends never[]>(decorate: string, method: string, config: HttpOptions) {
  return function (
    target: (this: This, ...args: Args) => Observable<T> | Promise<T>,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Observable<T>>
  ): (this: This, ...args: Args) => Observable<T> {
    const methodName = String(context.name);

    if (context.private) {
      throw new Error(`'${decorate}' cannot decorate private properties like ${methodName as string}.`);
    }

    let http: HttpClient;

    context.addInitializer(function () {
      http = inject(HttpClient);
      this[methodName] = this[methodName].bind(this);
    });

    if (!config.headers || !(config.headers instanceof HttpHeaders)) {
      config.headers = new HttpHeaders(config.headers || {});
    }

    if (!config.params || !(config.params instanceof HttpParams)) {
      config.params = new HttpParams({ fromObject: config.params || {} });
    }

    config.observe = config.observe ?? 'response';
    config.responseType = config.responseType ?? 'json';

    const observe = config.observe;

    function newMethod(this: This, ...args: Args): Observable<T> {
      if (observe === 'response') {
        return http.request(method, config.url, config).pipe(
          switchMap(value => {
            _context = new HttpResponseContext();
            _context.response = value;
            return target.call(this, ...args);
          })
        );
      }

      const req = new HttpRequest(method, config.url, config);

      return http.request(req).pipe(
        switchMap(value => {
          _context = new HttpResponseContext();
          _context.response = value;
          return target.call(this, ...args);
        })
      ) as Observable<T>;
    }

    return newMethod;
  };
}

接下来,我们在httpRequest.ts文件中实现了HTTP请求的具体处理逻辑,包括如何使用HTTP装饰器来声明和执行HTTP请求。这里值得注意的是,我们引入了useHttpContext函数,它帮助我们获取HTTP上下文以支持后处理功能。

  • http.ts
interface GetOptions extends HttpClientOptions {}
interface PostOptions extends HttpClientOptions {
}

interface Target<T, This, Args extends never[]> {
  (
    target: (this: This, ...args: Args) => Observable<T> | Promise<T>,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Observable<T>>
  ): (this: This, ...args: Args) => Observable<T>;
}
function createRequestDecorateFromFunction<F extends Function, U extends Record<string, unknown>>(
  fn: F,
  extraApi: U
): F & U {
  return Object.assign(fn, extraApi);
}

export const PUT = createRequestDecorateFromFunction(
  function (url: string, config?: PostOptions): Target<SafaAny, SafaAny, SafaAny> {
    config = config ?? {};

    return httpRequest('PUT', 'PUT', { ...config, url });
  },
  {
    Event: function (url: string, config?: PostOptions): Target<SafaAny, SafaAny, SafaAny> {
      config = config ?? {};

      return httpRequest('PUT', 'PUT', { ...config, url, observe: 'events' });
    }
  }
);

export const GET = createRequestDecorateFromFunction(
  function (url: string, config?: GetOptions): Target<SafaAny, SafaAny, SafaAny> {
    config = config ?? {};

    return httpRequest('GET', 'GET', { ...config, url });
  },
  {
    Event: function (url: string, config?: GetOptions): Target<SafaAny, SafaAny, SafaAny> {
      config = config ?? {};

      return httpRequest('GET', 'GET', { ...config, url, observe: 'events' });
    }
  }
);

export const POST = createRequestDecorateFromFunction(
  function (url: string, config?: PostOptions): Target<SafaAny, SafaAny, SafaAny> {
    config = config ?? {};
    return httpRequest('POST', 'POST', { ...config, url });
  },
  {
    Event: function (url: string, config?: PostOptions): Target<SafaAny, SafaAny, SafaAny> {
      config = config ?? {};

      return httpRequest('POST', 'POST', { ...config, url, observe: 'events' });
    }
  }
);
// 其他请求类似

最后,我们在http.ts文件中定义了一些HTTP请求装饰器,如GETPOST,以及它们的配置参数。这些装饰器用于声明HTTP请求的配置,并通过httpRequest函数来执行请求,同时支持后处理功能。

具体使用

  • 装饰返回类型为Observable的方法
@Injectable({ providedIn: 'root' })
class HttpTestService {
  @GET('users')
  async getUser(): Observable<User[]> {
    const { response } = useHttpContext<User[]>();
    return data.filter(({age}) => age > 18);
  }
}
  • 装饰返回类型为支持Promise的方法
@Injectable({ providedIn: 'root' })
class HttpTestService {
  @GET('users')
  async getUser(): Promise<User[]> {
    const { response } = useHttpContext<User[]>();
    // await do something
    return data.filter(({age}) => age > 18);
  }
}
  • 监听Http事件 observeevents - 例如上传:
@Injectable({ providedIn: 'root' })
class HttpTestService {
  private getEventMessage(event: HttpEvent<any>, file: File) {
      switch (event.type) {
        case HttpEventType.Sent:
          return `Uploading file "${file.name}" of size ${file.size}.`;

        case HttpEventType.UploadProgress:
          const percentDone = event.total ? Math.round(100 * event.loaded / event.total) : 0;
          return `File "${file.name}" is ${percentDone}% uploaded.`;

        case HttpEventType.Response:
          return `File "${file.name}" was completely uploaded!`;

        default:
          return `File "${file.name}" surprising upload event: ${event.type}.`;
      }
  }
  
  @POST.Events('upload/file', { reportProgress: true })
  upload(@Body file: File): Observable<HttpEvent<any>> {
    const { response } = useHttpContext<HttpEvent<any>, 'events', 'blob'>();
    this.getEventMessage(response, file);
    return of(response);
  }
}

总结

HTTP装饰器后处理功能为Angular开发提供了一种强大的工具,使我们能够更好地处理HTTP响应数据,以满足复杂的业务需求。它不仅提高了代码的可读性和可维护性,还为开发人员提供了更大的灵活性和效率