Angular: membuat kode dapat dibaca untuk back-end. Bonus: menukar API dan caching permintaan

Sangat sering pada suatu proyek, kecepatan pengembangan frontend berada di depan kecepatan pengembangan backend. Dalam situasi ini, dua hal muncul:

  1. kemampuan untuk meluncurkan bagian depan tanpa backend, atau tanpa titik akhir yang terpisah;
  2. Jelaskan pada back-end apa endpoint yang dibutuhkan, format permintaan, respons, dll.

Saya ingin membagikan cara saya mengatur kode yang bertanggung jawab atas permintaan API, yang dengan sempurna menyelesaikan dua masalah ini, dan juga memungkinkan Anda untuk menyimpan permintaan.

Buat file konfigurasi api.config.json di root proyek:

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

Di sini kita mengatur URL dasar untuk API, jika useFakeApiByDefault = parameter benar, maka aplikasi kita akan menggunakan hanya bertopik bukan semua permintaan. Jika false, maka stubs akan digunakan hanya untuk permintaan dari array fakeEndPoints.

Untuk mengimpor JSON ke dalam kode, kami menambahkan dua baris ke bagian CompilerOptions dari file tsconfig.json:

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

Buat kelas dasar 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> - jenis permintaan dan contoh respons. Sebagai tanggapan, payload sudah ditarik.

Saya tidak akan memposting kelas ApiService dan ErrorService, agar tidak mengembang postingan, tidak ada yang istimewa di sana. ApiService mengirimkan permintaan http, ErrorService memungkinkan Anda untuk berlangganan kesalahan. Untuk menampilkan kesalahan, Anda harus berlangganan ErrorService di komponen tempat kami sebenarnya ingin menampilkan kesalahan (saya menandatangani tata letak utama di mana saya membuat modal atau tooltip).

Sihir dimulai ketika kita mewarisi dari kelas ini. Kelas

turunan akan terlihat seperti ini: /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;
}

Tidak lebih, dari isi file itu segera jelas URL mana, format permintaan apa (GetUserRequest), format respons apa.

Jika file tersebut berada di folder / api yang terpisah, masing-masing file sesuai dengan titik akhir sendiri, saya pikir Anda dapat menunjukkan folder ini ke back-end, dan secara teoritis, jika Anda terlalu malas untuk menulis dokumentasi pada api, Anda dapat melakukannya tanpa itu. Anda masih dapat menyebarkan file di dalam folder / api ke folder sesuai dengan pengendali.

Jika Anda menambahkan 'GetUsers' ke array "fakeEndPoints" pada konfigurasi, maka tidak akan ada permintaan, dan responsnya akan diganti dengan data rintisan dari sampleResponse.

Agar permintaan palsu dapat di-debug (di tab Network, tentu saja, kami tidak akan melihat apa-apa), saya menyediakan output dari dua baris di kelas dasar di kelas dasar:

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

Jika Anda menimpa cache properti kelas = true, maka permintaan akan di-cache (pertama kali permintaan ke API dibuat, maka hasil dari permintaan pertama selalu dikembalikan). Yang benar di sini adalah untuk menyelesaikan: Anda perlu membuat caching hanya berfungsi jika parameter permintaan (isi instance dari kelas UserRequest).

Kami mendefinisikan kembali metode payloadMap jika kami memerlukan transformasi data yang diterima dari server. Jika metode ini tidak didefinisikan ulang, maka data dari payload akan di Janji yang dikembalikan.

Sekarang kita mendapatkan data dari API di komponen:

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();
  }
}


Dalam implementasi seperti itu, adalah mungkin untuk menunjukkan kepada pelanggan hasil dari melengkapi Wishlist nya, bahkan jika backend belum selesai untuk Wishlist ini. Letakkan titik akhir yang diperlukan di rintisan - dan Anda sudah bisa "menyentuh" ​​fitur dan mendapatkan umpan balik.

Dalam komentar saya menantikan kritik konstruktif tentang masalah apa yang mungkin timbul ketika membangun fungsionalitas, yang bisa keluar perangkap lain.

Di masa depan saya ingin menulis ulang solusi ini dari janji untuk async / menunggu. Saya pikir kodenya akan lebih elegan.

Dalam stok ada beberapa solusi arsitektur yang lebih untuk Angular, dalam waktu dekat saya berencana untuk berbagi.

All Articles