Arbeiten mit Daten in Angular

Hallo allerseits, mein Name ist Sergey und ich bin ein Webentwickler. Verzeihen Sie mir Dmitry Karlovsky für die geliehene Einführung, aber es waren seine Veröffentlichungen , die mich dazu inspirierten, diesen Artikel zu schreiben.


Heute möchte ich über die Arbeit mit Daten in Angular- Anwendungen im Allgemeinen und Domänenmodellen im Besonderen sprechen.


Angenommen, wir haben eine Liste der Benutzer, die wir vom Server im Formular erhalten


[ { "id": 1, "first_name": "James", "last_name": "Hetfield", "position": "Web developer" }, { "id": 2, "first_name": "Elvis", "last_name": "", "position": "Project manager" }, { "id": 3, "first_name": "Steve", "last_name": "Vai", "position": "QA engineer" } ] 

und Sie müssen es wie auf dem Bild anzeigen


Liste der Benutzer


Es sieht einfach aus - lass es uns versuchen. Um diese Liste zu erhalten, haben wir UserService einen UserService Dienst UserService folgenden UserService . Bitte beachten Sie, dass der Link zum Avatar des Benutzers nicht sofort in der Antwort angezeigt wird, sondern auf der Grundlage der Benutzer- id .


 // UserService import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {Observable} from 'rxjs'; import {UserServerResponse} from './user-server-response.interface'; @Injectable() export class UserService { constructor(private http: HttpClient) { } getUsers(): Observable<UserServerResponse[]> { return this.http.get<UserServerResponse[]>('/users'); } getUserAvatar(userId: number): string { return `/users/${userId}/avatar`; } } 

Die UserListComponent Komponente ist für die Anzeige der Benutzerliste verantwortlich.


 // UserListComponent import {Component} from '@angular/core'; import {UserService} from '../services/user.service'; @Component({ selector: 'app-user-list', template: ` <div *ngFor="let user of users | async"> <img [src]="userService.getUserAvatar(user.id)"> <p><b>{{user.first_name}} {{user.last_name}}</b>, {{user.position}}</p> </div> ` }) export class UserListComponent { users = this.userService.getUsers(); constructor(public userService: UserService) { } } 

Und hier hatten wir schon ein definitives Problem . Achten Sie auf die Serverantwort. Das Feld last_name ist möglicherweise leer. Wenn wir die Komponente in dieser Form last_name , erhalten wir unerwünschte Leerzeichen vor dem Komma. Was sind die Lösungsmöglichkeiten?


  1. Sie können die Anzeigevorlage leicht korrigieren


     <p> <b>{{[user.first_name, user.last_name].filter(el => !!el).join(' ')}}</b>, {{user.position}} </p> 

    Auf diese Weise überladen wir die Vorlage jedoch mit Logik und sie wird selbst für eine so einfache Aufgabe schlecht lesbar. Aber die Anwendung muss noch wachsen und wachsen ...


  2. Ziehen Sie Code aus einer Vorlage in eine Komponentenklasse, indem Sie eine Typmethode hinzufügen


     getUserFullName(user: UserServerResponse): string { return [user.first_name, user.last_name].filter(el => !!el).join(' '); } 

    Bereits besser, aber höchstwahrscheinlich wird der vollständige Benutzername nicht an einer Stelle der Anwendung angezeigt, und wir müssen diesen Code duplizieren. Sie können diese Methode von der Komponente zum Service übernehmen. Auf diese Weise werden wir mögliche Codeduplikationen beseitigen, aber diese Option gefällt mir auch nicht wirklich. Und ich mag es nicht, weil sich herausstellt, dass eine allgemeinere Entität ( UserService ) über die Struktur der kleineren User Entität UserService wissen sollte. Es scheint mir nicht ihre Verantwortung zu sein.



Meiner Meinung nach ergibt sich das Problem hauptsächlich aus der Tatsache, dass wir die Serverantwort ausschließlich als Datensatz behandeln. Obwohl es sich tatsächlich um eine Liste von Entitäten aus dem Themenbereich unserer Anwendung handelt - eine Liste von Benutzern. Und wenn wir über die Arbeit mit Entitäten sprechen, lohnt es sich, dafür die am besten geeigneten Werkzeuge zu verwenden - die Methoden der objektorientierten Programmierung.


Beginnen wir mit der Erstellung der Benutzerklasse


 // User export class User { readonly id; readonly firstName; readonly lastName; readonly position; constructor(userData: UserServerResponse) { this.id = userData.id; this.firstName = userData.first_name; this.lastName = userData.last_name; this.position = userData.position; } fullName(): string { return [this.firstName, this.lastName].filter(el => !!el).join(' '); } avatar(): string { return `/users/${this.id}/avatar`; } } 

Der Klassenkonstruktor ist ein Serverantwort-Deserializer. Die Logik zum Bestimmen des vollständigen Benutzernamens wird natürlich zu einer Methode eines Objekts der User sowie zur Logik zum Erhalten eines Avatars. Jetzt werden wir den UserService sodass er uns Objekte der User Klasse als Ergebnis der Verarbeitung der Serverantwort zurückgibt


 // UserService import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {map} from 'rxjs/operators'; import {UserServerResponse} from './user-server-response.interface'; import {User} from './user.model'; @Injectable() export class UserService { constructor(private http: HttpClient) { } getUsers(): Observable<User[]> { return this.http.get<UserServerResponse[]>('/users') .pipe(map(listOfUsers => listOfUsers.map(singleUser => new User(singleUser)))); } } 

Dadurch wird der Code unserer Komponente viel sauberer und lesbarer. Alles, was als Geschäftslogik bezeichnet werden kann, ist in Modellen gekapselt und vollständig wiederverwendbar.


 import {Component} from '@angular/core'; import {UserService} from '../services/user.service'; @Component({ selector: 'app-user-list', template: ` <div *ngFor="let user of users | async"> <img [src]="user.avatar()"> <p><b>{{user.fullName()}}</b>, {{user.position}}</p> </div> ` }) export class UserListComponent { users = this.userService.getUsers(); constructor(private userService: UserService) { } } 

Lassen Sie uns nun die Funktionen unseres Modells erweitern. Theoretisch (in diesem Zusammenhang gefällt mir die Analogie zum ActiveRecord Muster) sollten Benutzermodellobjekte nicht nur dafür verantwortlich sein, Daten über sich selbst abzurufen, sondern auch zu ändern. Beispielsweise können wir möglicherweise das Profilbild des Benutzers ändern. Wie wird das mit solchen Funktionen erweiterte Benutzermodell aussehen?


 // User export class User { // ... constructor(userData: UserServerResponse, private http: HttpClient, private storage: StorageService, private auth: AuthService) { // ... } // ... updateAvatar(file: Blob) { const data = new FormData(); data.append('avatar', file); return this.http.put(`/users/${this.id}/avatar`, data); } } 

Es sieht gut aus, aber das User verwendet jetzt den HttpClient Dienst und kann im Allgemeinen verschiedene andere Dienste verbinden und verwenden - in diesem Fall StorageService und AuthService (sie werden nicht verwendet, sondern nur zum Beispiel hinzugefügt). Es stellt sich heraus, dass wir, wenn wir das User in einem anderen Dienst oder einer anderen Komponente verwenden möchten, alle damit verbundenen Dienste verbinden müssen, um Objekte dieses Modells zu erstellen. Es sieht sehr unpraktisch aus ... Sie können den Injector Dienst verwenden (natürlich muss er auch implementiert werden, aber es wird garantiert nur eine sein) oder sogar eine externe Injektor-Entität erstellen, die Sie nicht implementieren müssen, aber ich sehe die korrektere Art, die Erstellung von User auf dieselbe Weise an den UserService Dienst zu UserService Er ist dafür verantwortlich, die Liste der Benutzer zu erhalten.


 // UserService import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {UserServerResponse} from './user-server-response.interface'; import {User} from './user.model'; @Injectable() export class UserService { constructor(private http: HttpClient, private storage: StorageService, private auth: AuthService) { } createUser(userData: UserServerResponse) { return new User(userData, this.http, this.storage, this.auth); } getUsers(): Observable<User[]> { return this.http.get<UserServerResponse[]>('/users') .pipe(map(listOfUsers => listOfUsers.map(singleUser => this.createUser(singleUser)))); } } 

Aus diesem UserService wir die Methode zur Benutzererstellung auf UserService , die jetzt besser zum Aufrufen der Factory geeignet ist, und die gesamte Arbeit zum Implementieren von Abhängigkeiten auf die Schultern von Angular verlagert. Wir müssen nur den UserService im Konstruktor verbinden.


Lassen Sie uns am Ende die Duplizierung aus den Namen der Methoden entfernen und Konventionen für die Namen der injizierten Abhängigkeiten einführen. Die endgültige Version des Dienstes in meiner Vision sollte so aussehen.


 import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {UserServerResponse} from './user-server-response.interface'; import {User} from './user.model'; @Injectable() export class UserFactory { constructor(private http: HttpClient, private storage: StorageService, private auth: AuthService) { } create(userData: UserServerResponse) { return new User(userData, this.http, this.storage, this.auth); } list(): Observable<User[]> { return this.http.get<UserServerResponse[]>('/users') .pipe(map(listOfUsers => listOfUsers.map(singleUser => this.create(singleUser)))); } } 

Es wird vorgeschlagen, UserFactory unter dem Namen User zu implementieren


 import { Component } from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {UserFactory} from './services/user.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app'; users = this.User.list(); constructor(private User: UserFactory) { } } 

In diesem Fall sieht ein Objekt der UserFactory Klasse wie eine User Klasse mit statischen Methoden zum UserFactory einer Benutzerliste und einer speziellen Methode zum Erstellen neuer Entitäten aus. Die Objekte enthalten alle erforderlichen Geschäftslogikmethoden, die einer bestimmten Entität zugeordnet sind.


Dazu erzählte ich alles, was ich wollte. Ich freue mich darauf, in den Kommentaren zu diskutieren.


Update


Ich wollte allen, die Kommentare abgeben, meinen Dank aussprechen. Sie haben zu Recht bemerkt, dass es sich lohnt, Pipe zu verwenden, um das Problem mit der Anzeige des Namens zu lösen. Ich stimme vollkommen zu und bin selbst überrascht, warum ich diese Entscheidung nicht getroffen habe. Das Hauptziel des Artikels ist es jedoch, ein Beispiel für die Erstellung eines Domänenmodells (in diesem Fall User ) zu zeigen, das die gesamte mit seiner Essenz verbundene Geschäftslogik bequem zusammenfassen kann. Parallel dazu habe ich versucht, das damit verbundene Problem mit der Abhängigkeitsinjektion zu lösen.

Source: https://habr.com/ru/post/de418463/


All Articles