前言
在现代的前端开发中,与后端服务器进行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装饰器的作用,让我们来看一个示例,分别比较有装饰器和无装饰器的情况。
有装饰器的示例
ts
@Injectable({ providedIn: 'root' })
class HttpTestService {
@GET('users')
getUser(): Observable<User[]> {
return null as Observable<User[]>;
}
}
无装饰器的示例
ts
@Injectable({ providedIn: 'root' })
class HttpTestService {
getUser(): Observable<User[]> {
// 实际实现...
}
}
从上面示例来看,当遇到相应数据后处理装饰器的方式就很难优雅进行后处理了,然而,在实际开发中,从服务器返回的原始数据通常并不总是能够直接满足我们的需求。这时候,需要考虑如何在获取到HTTP响应后,对数据进行进一步的处理和转换,以满足特定的业务需求
添加后处理支持
在前一节中,我们已经了解了HTTP装饰器的优势。但通常,从服务器返回的原始数据仍然需要在客户端进行后续处理,以适应我们的具体需求。这时候,就可以尝试实现HTTP装饰器的后处理功能,对HTTP响应数据进行进一步的加工和转换。
接下来,探讨如何使用后处理功能,以及如何在HTTP请求中实现后处理,以满足更复杂的业务逻辑和数据处理需求。我们将通过具体的示例来演示后处理的功能和灵活性。
示例:
让我们首先看一个具体的示例,演示了如何在HTTP请求中优雅添加后处理支持。我们将使用一个简单的HTTP请求示例,该请求获取用户数据,并在后处理中过滤出年龄大于18岁的用户。
ts
@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请求的响应类型相匹配是一个关键问题。
一种常见但可能不够优雅的方法是将数据作为方法的参数传递,如下所示:
ts
@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
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
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
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请求装饰器,如GET
和POST
,以及它们的配置参数。这些装饰器用于声明HTTP请求的配置,并通过httpRequest
函数来执行请求,同时支持后处理功能。
具体使用
- 装饰返回类型为
Observable
的方法
ts
@Injectable({ providedIn: 'root' })
class HttpTestService {
@GET('users')
async getUser(): Observable<User[]> {
const { response } = useHttpContext<User[]>();
return data.filter(({age}) => age > 18);
}
}
- 装饰返回类型为支持Promise的方法
ts
@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事件
observe
为events
- 例如上传:
ts
@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响应数据,以满足复杂的业务需求。它不仅提高了代码的可读性和可维护性,还为开发人员提供了更大的灵活性和效率