角度:使代码对后端可读。奖励:交换API和查询缓存

通常,在项目中,前端开发速度要领先于后端开发速度。在这种情况下,会发生两件事:

  1. 在没有后端或没有单独端点的情况下启动前端的能力;
  2. 向后端描述需要哪些端点,请求,响应的格式等。

我想分享组织API请求代码的方式,这可以完美解决这两个问题,还可以缓存请求。

在项目根目录中创建api.config.json配置文件:

{
  "apiUrl": "https://api.example.com",
  "useFakeApiByDefault": false,
  "fakeEndPoints": ["Sample"]
}

在这里,我们设置API的基本URL,如果useFakeApiByDefault = true参数,则我们的应用程序将仅使用存根而不是所有请求。如果为false,则存根将仅用于来自fakeEndPoints数组的请求。

为了将JSON导入代码,我们在tsconfig.json文件的CompilerOptions部分添加了两行:

    "resolveJsonModule": true,
    "esModuleInterop": true,

创建基类BaseEndpoint。

/src/app/api/_base.endpoint.ts:

import {ApiMethod, ApiService} from '../services/api/api.service';
import * as ApiConfig from '../../../api.config.json';
import {ErrorService} from '../services/error/error.service';
import {NgModule} from '@angular/core';

@NgModule({providers: [ApiService, ErrorService]})

export abstract class BaseEndpoint<Request, ResponseModel> {
  protected url: string;
  protected name: string;
  protected method: ApiMethod = ApiMethod.Post;
  protected sampleResponse: ResponseModel;
  protected cache: boolean = false;
  protected responseCache: ResponseModel = null;

  constructor(private api: ApiService, private error: ErrorService) {
  }

  public execute(request: Request): Promise<ResponseModel> {
    if (this.cache && this.responseCache !== null) {
      return new Promise<ResponseModel>((resolve, reject) => {
        resolve(this.responseCache);
      });
    } else {
      if (ApiConfig.useFakeApiByDefault || ApiConfig.fakeEndPoints.includes(this.name)) {
        console.log('Fake Api Request:: ', this.name, request);
        console.log('Fake Api Response:: ', this.sampleResponse);
        return new Promise<ResponseModel>((resolve) => resolve(this.sampleResponse));
      } else {
        return new Promise<ResponseModel>((resolve, reject) => {
          this.api.execute(this.url, this.method, request).subscribe(
            (response: Response<ResponseModel>) => {
              if (response.status === 200) {
                if (this.cache) { this.responseCache = response.payload; }
                resolve(this.payloadMap(response.payload));
              } else {
                this.error.emit(response.error);
                reject(response.error);
              }
            }, response => {
              this.error.emit('    ')
              reject('    '));
           }
        });
      }
    }
  }

  protected payloadMap(payload: ResponseModel): ResponseModel { return payload; }
}

abstract class Response<T> {
  public status: number;
  public error: string;
  public payload: T;
}

<Request,ResponseModel>-请求和响应实例的类型。作为响应,有效载荷已经被拉出。

我不会发布ApiService和ErrorService类,以免使该帖子膨胀,那里没有什么特别的。 ApiService发送http请求,ErrorService允许您订阅错误。要显示错误,您需要在我们实际想要显示错误的组件中订阅ErrorService(我在其中制作模态或工具提示的主布局中签名)。

当我们从此类继承时,魔术就开始了。

派生类如下所示:/src/app/api/get-users.endpoint.ts

import {BaseEndpoint} from './_base.endpoint';
import {Injectable} from '@angular/core';
import {UserModel} from '../models/user.model';

@Injectable()
export class GetUsersEndpoint extends BaseEndpoint<GetUsersRequest, UserModel[]> {
  protected name = 'GetUsers';
  protected url= '/getUsers';
  protected sampleResponse = [{
      id: 0,
      name: 'Ivan',
      age: 18
  },
  {
      id: 1,
      name: 'Igor',
      age: 25
   }

  protected payloadMap(payload) {
    return payload.map(user => new UserModel(user));
  }
}

export class GetUsersRequest {
   page: number;
   limit: number;
}

仅此而已,从文件的内容中可以立即清除哪种URL,哪种请求格式(GetUserRequest),哪种响应格式。

如果此类文件位于单独的文件夹/ api中,则每个文件都对应于其自己的终结点,我认为您可以将此文件夹显示到后端,并且从理论上讲,如果您懒于在api上编写文档,则可以不使用它。您仍然可以根据控制器将文件夹/ api中的文件分散到文件夹中。

如果将“ GetUsers”添加到“ fakeEndPoints”配置数组,则将没有请求,并且响应将被sampleResponse中的存根数据替换。

为了调试伪造的请求(当然,在“网络”选项卡中,我们什么都看不到),我提供了控制台的输出,以向控制台类输出两行:

console.log('Fake Api Request:: ', this.name, request);
console.log('Fake Api Response:: ', this.sampleResponse);

如果您覆盖类属性cache = true,则将缓存该请求(第一次向API发出请求时,始终返回第一个请求的结果)。事实是最终确定:仅当请求参数(UserRequest类的实例的内容)时,才需要使缓存起作用。

如果需要对从服务器接收到的数据进行任何转换,我们将重新定义有效负载映射方法。如果未重新定义方法,则有效负载中的数据将位于返回的Promise中。

现在,我们从组件中的API获取数据:

import {Component, OnInit, ViewChild} from '@angular/core';
import {UserModel} from '../../models/user.model';
import {GetUsersEndpoint, GetUsersRequest} from '../../api/get_users.endpoint';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html',
  styleUrls: ['./users.component.css'],
  providers: [GetUsersEndpoint]
})
export class UsersComponent implements OnInit {

  public users: UserModel[];
  public request: GetUsersRequest = {page: 1, limit: 20};

  constructor(private getUsersEndpoint: GetUsersEndpoint) {    
  }

  private load() {
    this.getUsersEndpoint.execute(this.request).then(data => {
      this.users = data;
    });
  }

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


在这种实施方式中,即使尚未完成这些愿望清单的后端,也可以向客户显示完成其愿望清单的结果。将必要的端点放在存根上-您已经可以“触摸”功能并获得反馈。

在这些评论中,我期待对构建功能时可能出现的问题进行建设性的批评,以及其他陷阱。

将来,我想将此解决方案从Promise重写为async / await。我认为代码将更加优雅。

在库存中,还有更多针对Angular的架构解决方案,我计划在不久的将来分享。

All Articles