angular-ngrx-data - manajemen negara dan CRUD dalam lima menit

gambar
Sampai saat ini, tidak ada satu pun aplikasi SPA besar yang lengkap tanpa manajemen negara . Ada beberapa solusi untuk Angular di bidang ini. Yang paling populer adalah NgRx . Ini mengimplementasikan pola Redux menggunakan perpustakaan RxJs dan memiliki alat yang baik.

Pada artikel ini, kita akan membahas modul NgRx utama secara singkat dan fokus lebih detail pada pustaka sudut-ngrx-data , yang memungkinkan Anda untuk membuat CRUD penuh dengan manajemen negara dalam lima menit.

Ulasan NgRx


Anda dapat membaca lebih lanjut tentang NgRx di artikel berikut:

- Aplikasi reaktif pada Angular / NGRX. Bagian 1. Pendahuluan
- Aplikasi reaktif pada Angular / NGRX. Bagian 2. Toko
- Aplikasi reaktif pada Angular / NGRX. Bagian 3. Efek

Secara singkat pertimbangkan modul utama NgRx , pro dan kontra.

NgRx / store - mengimplementasikan pola Redux.

Implementasi toko sederhana
counter.actions.ts
export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT'; export const RESET = 'RESET'; 

counter.reducer.ts

 import { Action } from '@ngrx/store'; const initialState = 0; export function counterReducer(state: number = initialState, action: Action) { switch (action.type) { case INCREMENT: return state + 1; case DECREMENT: return state - 1; case RESET: return 0; default: return state; } } 
.
Koneksi ke modul
 import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import { counterReducer } from './counter'; @NgModule({ imports: [StoreModule.forRoot({ count: counterReducer })], }) export class AppModule {} 

Gunakan dalam komponen
 import { Component } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { Observable } from 'rxjs'; import { INCREMENT, DECREMENT, RESET } from './counter'; interface AppState { count: number; } @Component({ selector: 'app-my-counter', template: ` <button (click)="increment()">Increment</button> <div>Current Count: {{ count$ | async }}</div> <button (click)="decrement()">Decrement</button> <button (click)="reset()">Reset Counter</button> `, }) export class MyCounterComponent { count$: Observable<number>; constructor(private store: Store<AppState>) { this.count$ = store.pipe(select('count')); } increment() { this.store.dispatch({ type: INCREMENT }); } decrement() { this.store.dispatch({ type: DECREMENT }); } reset() { this.store.dispatch({ type: RESET }); } } 


NgRx / store-devtools - memungkinkan Anda untuk melacak perubahan dalam aplikasi melalui redux-devtools .

Contoh Koneksi
 import { StoreDevtoolsModule } from '@ngrx/store-devtools'; @NgModule({ imports: [ StoreModule.forRoot(reducers), //      StoreModule StoreDevtoolsModule.instrument({ maxAge: 25, //   25  }), ], }) export class AppModule {} 


NgRx / efek - memungkinkan Anda untuk menambahkan data yang masuk ke aplikasi ke repositori, seperti permintaan http.

Contoh
./effects/auth.effects.ts
 import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Action } from '@ngrx/store'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { Observable, of } from 'rxjs'; import { catchError, map, mergeMap } from 'rxjs/operators'; @Injectable() export class AuthEffects { // Listen for the 'LOGIN' action @Effect() login$: Observable<Action> = this.actions$.pipe( ofType('LOGIN'), mergeMap(action => this.http.post('/auth', action.payload).pipe( // If successful, dispatch success action with result map(data => ({ type: 'LOGIN_SUCCESS', payload: data })), // If request fails, dispatch failed action catchError(() => of({ type: 'LOGIN_FAILED' })) ) ) ); constructor(private http: HttpClient, private actions$: Actions) {} } 

Menghubungkan efek ke modul

 import { EffectsModule } from '@ngrx/effects'; import { AuthEffects } from './effects/auth.effects'; @NgModule({ imports: [EffectsModule.forRoot([AuthEffects])], }) export class AppModule {} 


NgRx / entitas - menyediakan kemampuan untuk bekerja dengan array data.

Contoh
user.model.ts

 export interface User { id: string; name: string; } 

user.actions.ts

 import { Action } from '@ngrx/store'; import { Update } from '@ngrx/entity'; import { User } from './user.model'; export enum UserActionTypes { LOAD_USERS = '[User] Load Users', ADD_USER = '[User] Add User', UPSERT_USER = '[User] Upsert User', ADD_USERS = '[User] Add Users', UPSERT_USERS = '[User] Upsert Users', UPDATE_USER = '[User] Update User', UPDATE_USERS = '[User] Update Users', DELETE_USER = '[User] Delete User', DELETE_USERS = '[User] Delete Users', CLEAR_USERS = '[User] Clear Users', } export class LoadUsers implements Action { readonly type = UserActionTypes.LOAD_USERS; constructor(public payload: { users: User[] }) {} } export class AddUser implements Action { readonly type = UserActionTypes.ADD_USER; constructor(public payload: { user: User }) {} } export class UpsertUser implements Action { readonly type = UserActionTypes.UPSERT_USER; constructor(public payload: { user: User }) {} } export class AddUsers implements Action { readonly type = UserActionTypes.ADD_USERS; constructor(public payload: { users: User[] }) {} } export class UpsertUsers implements Action { readonly type = UserActionTypes.UPSERT_USERS; constructor(public payload: { users: User[] }) {} } export class UpdateUser implements Action { readonly type = UserActionTypes.UPDATE_USER; constructor(public payload: { user: Update<User> }) {} } export class UpdateUsers implements Action { readonly type = UserActionTypes.UPDATE_USERS; constructor(public payload: { users: Update<User>[] }) {} } export class DeleteUser implements Action { readonly type = UserActionTypes.DELETE_USER; constructor(public payload: { id: string }) {} } export class DeleteUsers implements Action { readonly type = UserActionTypes.DELETE_USERS; constructor(public payload: { ids: string[] }) {} } export class ClearUsers implements Action { readonly type = UserActionTypes.CLEAR_USERS; } export type UserActionsUnion = | LoadUsers | AddUser | UpsertUser | AddUsers | UpsertUsers | UpdateUser | UpdateUsers | DeleteUser | DeleteUsers | ClearUsers; 

user.reducer.ts
 import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; import { User } from './user.model'; import { UserActionsUnion, UserActionTypes } from './user.actions'; export interface State extends EntityState<User> { // additional entities state properties selectedUserId: number | null; } export const adapter: EntityAdapter<User> = createEntityAdapter<User>(); export const initialState: State = adapter.getInitialState({ // additional entity state properties selectedUserId: null, }); export function reducer(state = initialState, action: UserActionsUnion): State { switch (action.type) { case UserActionTypes.ADD_USER: { return adapter.addOne(action.payload.user, state); } case UserActionTypes.UPSERT_USER: { return adapter.upsertOne(action.payload.user, state); } case UserActionTypes.ADD_USERS: { return adapter.addMany(action.payload.users, state); } case UserActionTypes.UPSERT_USERS: { return adapter.upsertMany(action.payload.users, state); } case UserActionTypes.UPDATE_USER: { return adapter.updateOne(action.payload.user, state); } case UserActionTypes.UPDATE_USERS: { return adapter.updateMany(action.payload.users, state); } case UserActionTypes.DELETE_USER: { return adapter.removeOne(action.payload.id, state); } case UserActionTypes.DELETE_USERS: { return adapter.removeMany(action.payload.ids, state); } case UserActionTypes.LOAD_USERS: { return adapter.addAll(action.payload.users, state); } case UserActionTypes.CLEAR_USERS: { return adapter.removeAll({ ...state, selectedUserId: null }); } default: { return state; } } } export const getSelectedUserId = (state: State) => state.selectedUserId; // get the selectors const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); // select the array of user ids export const selectUserIds = selectIds; // select the dictionary of user entities export const selectUserEntities = selectEntities; // select the array of users export const selectAllUsers = selectAll; // select the total user count export const selectUserTotal = selectTotal; 

reduksi / index.ts

 import { createSelector, createFeatureSelector, ActionReducerMap, } from '@ngrx/store'; import * as fromUser from './user.reducer'; export interface State { users: fromUser.State; } export const reducers: ActionReducerMap<State> = { users: fromUser.reducer, }; export const selectUserState = createFeatureSelector<fromUser.State>('users'); export const selectUserIds = createSelector( selectUserState, fromUser.selectUserIds ); export const selectUserEntities = createSelector( selectUserState, fromUser.selectUserEntities ); export const selectAllUsers = createSelector( selectUserState, fromUser.selectAllUsers ); export const selectUserTotal = createSelector( selectUserState, fromUser.selectUserTotal ); export const selectCurrentUserId = createSelector( selectUserState, fromUser.getSelectedUserId ); export const selectCurrentUser = createSelector( selectUserEntities, selectCurrentUserId, (userEntities, userId) => userEntities[userId] ); 


Apa hasilnya?


Kami mendapatkan manajemen negara penuh dengan banyak keuntungan:

- sumber data tunggal untuk aplikasi,
- negara disimpan secara terpisah dari aplikasi,
- gaya penulisan tunggal untuk semua pengembang dalam proyek,
- changeDetectionStrategy.OnPush di semua komponen aplikasi,
- debugging yang nyaman melalui redux-devtools ,
- kemudahan pengujian, seperti reduksi adalah fungsi murni.

Namun ada juga kelemahannya:

- Sejumlah besar modul yang tampaknya tidak dapat dipahami,
- banyak jenis kode yang sama yang tidak akan Anda lihat tanpa kesedihan
- Kesulitan dalam menguasai karena semua hal di atas.

CRUD


Sebagai aturan, sebagian besar aplikasi ditempati oleh pekerjaan dengan objek (pembuatan, membaca, memperbarui, menghapus), oleh karena itu, untuk kenyamanan pekerjaan, konsep CRUD telah dibuat (Buat, Baca, Perbarui, Hapus). Dengan demikian, operasi dasar untuk bekerja dengan semua jenis objek distandarisasi. Telah booming untuk waktu yang lama di backend. Banyak perpustakaan membantu menerapkan fungsi ini dan menyingkirkan pekerjaan rutin.

Dalam NgRx , modul entitas bertanggung jawab untuk CRUD , dan jika Anda melihat contoh implementasinya, Anda dapat segera melihat bahwa ini adalah bagian terbesar dan paling kompleks dari NgRx . Itulah sebabnya John Papa dan Ward Bell membuat data angular-ngrx .

angular-ngrx-data


angular-ngrx-data adalah pustaka add-in NgRx yang memungkinkan Anda untuk bekerja dengan array data tanpa menulis kode tambahan.
Selain menciptakan manajemen negara yang lengkap, ia melakukan pembuatan layanan dengan http untuk berinteraksi dengan server.

Pertimbangkan sebuah contoh


Instalasi

 npm install --save @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools ngrx-data 

Modul data sudut-ngrx

 import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { EntityMetadataMap, NgrxDataModule, DefaultDataServiceConfig } from 'ngrx-data'; const defaultDataServiceConfig: DefaultDataServiceConfig = { root: 'crud' }; export const entityMetadata: EntityMetadataMap = { Hero: {}, User:{} }; export const pluralNames = { Hero: 'heroes' }; @NgModule({ imports: [ CommonModule, NgrxDataModule.forRoot({ entityMetadata, pluralNames }) ], declarations: [], providers: [ { provide: DefaultDataServiceConfig, useValue: defaultDataServiceConfig } ] }) export class EntityStoreModule {} 

Hubungkan ke aplikasi

 @NgModule({ imports: [ BrowserModule, HttpClientModule, StoreModule.forRoot({}), EffectsModule.forRoot([]), EntityStoreModule, StoreDevtoolsModule.instrument({ maxAge: 25, }), ], declarations: [ AppComponent ], providers: [], bootstrap: [AppComponent] }) export class AppModule {} 

Kami baru saja mendapatkan API yang dihasilkan untuk bekerja dengan back-end dan integrasi API dengan NgRx , tanpa menulis efek tunggal, peredam dan tindakan serta pemilih.

Mari kita teliti lebih detail apa yang terjadi di sini.


Konstanta defaultDataServiceConfig menetapkan konfigurasi untuk API kami dan menghubungkan ke modul penyedia . Properti root menunjukkan ke mana harus mencari permintaan. Jika tidak disetel, maka defaultnya adalah "api".

 const defaultDataServiceConfig: DefaultDataServiceConfig = { root: 'crud' }; 

Konstanta entityMetadata mendefinisikan nama-nama toko yang akan dibuat ketika NgrxDataModule.forRoot terhubung.

 export const entityMetadata: EntityMetadataMap = { Hero: {}, User:{} }; ... NgrxDataModule.forRoot({ entityMetadata, pluralNames }) 

Jalur ke API terdiri dari jalur dasar (dalam kasus kami "kasar") dan nama toko.
Misalnya, untuk mendapatkan pengguna dengan nomor tertentu, pathnya adalah “crud / user / {userId}”.

Untuk mendapatkan daftar pengguna yang lengkap, huruf "s" - "crud / user s " ditambahkan ke akhir nama toko secara default.

Jika Anda memerlukan rute yang berbeda untuk mendapatkan daftar lengkap (misalnya, "pahlawan" dan bukan "pahlawan"), Anda dapat mengubahnya dengan mengatur nama jamak dan menghubungkannya ke NgrxDataModule.forRoot .

 export const pluralNames = { Hero: 'heroes' }; ... NgrxDataModule.forRoot({ entityMetadata, pluralNames }) 

Koneksi dalam komponen


Untuk menghubungkan dalam komponen, Anda harus meneruskan konstruktor entitasServices ke konstruktor dan menggunakan metode getEntityCollectionService untuk memilih layanan penyimpanan yang diinginkan

 import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Observable } from 'rxjs'; import { Hero } from '@appModels/hero'; import { EntityServices, EntityCollectionService } from 'ngrx-data'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) export class HeroesComponent implements OnInit { heroes$: Observable<Hero[]>; heroesService: EntityCollectionService<Hero>; constructor(entityServices: EntityServices) { this.heroesService = entityServices.getEntityCollectionService('Hero'); } ... } 

Untuk mengikat daftar ke komponen, cukup untuk mengambil entitas $ properti dari layanan, dan untuk mendapatkan data dari server, panggil metode getAll () .

 ngOnInit() { this.heroes$ = this.heroesService.entities$; this.heroesService.getAll(); } 

Selain data dasar, Anda juga bisa mendapatkan:

- dimuat $ , memuat $ - mendapatkan status memuat data,
- kesalahan $ - kesalahan saat layanan berjalan,
- hitung $ - jumlah total catatan dalam repositori.

Metode utama berinteraksi dengan server:

- getAll () - mendapatkan seluruh daftar data,
- getWithQuery (kueri) - mendapatkan daftar yang difilter menggunakan parameter kueri,
- getByKey (id) - mendapatkan satu catatan dengan pengidentifikasi,
- add (entitas) - menambahkan entitas baru dengan permintaan dukungan,
- delete (entitas) - menghapus entitas dengan permintaan dukungan,
- perbarui (entitas) - perbarui entitas dengan permintaan dukungan.

Metode penyimpanan lokal:

- addManyToCache (entitas) - menambahkan array entitas baru ke repositori,
- addOneToCache (entitas) - menambahkan entitas baru hanya ke repositori,
- removeOneFromCache (id) - hapus satu entitas dari repositori,
- updateOneInCache (entitas) - perbarui entitas dalam repositori,
- upsertOneInCache (entitas) - jika entitas dengan id yang ditentukan ada, itu diperbarui, jika tidak, yang baru dibuat,
- dan lainnya

Contoh penggunaan komponen

 import { EntityCollectionService, EntityServices } from 'ngrx-data'; import { Hero } from '../../core'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) export class HeroesComponent implements OnInit { heroes$: Observable<Hero[]>; heroesService: EntityCollectionService<Hero>; constructor(entityServices: EntityServices) { this.heroesService = entityServices.getEntityCollectionService('Hero'); } ngOnInit() { this.heroes$ = this.heroesService.entities$; this.getHeroes(); } getHeroes() { this.heroesService.getAll(); } addHero(hero: Hero) { this.heroesService.add(hero); } deleteHero(hero: Hero) { this.heroesService.delete(hero.id); } updateHero(hero: Hero) { this.heroesService.update(hero); } } 

Semua metode angular-ngrx-data dibagi menjadi bekerja secara lokal dan berinteraksi dengan server. Ini memungkinkan Anda untuk menggunakan pustaka saat memanipulasi data pada klien dan menggunakan server.

Penebangan


Untuk masuk, Anda perlu menyuntikkan EntityServices ke dalam komponen atau layanan dan menggunakan properti:

--ucedActions $ - untuk tindakan logging,
- entityActionErrors $ - untuk kesalahan logging.

 import { Component, OnInit } from '@angular/core'; import { MessageService } from '@appServices/message.service'; import { EntityServices } from 'ngrx-data'; @Component({ selector: 'app-messages', templateUrl: './messages.component.html', styleUrls: ['./messages.component.css'] }) export class MessagesComponent implements OnInit { constructor( public messageService: MessageService, private entityServices: EntityServices ) {} ngOnInit() { this.entityServices.reducedActions$.subscribe(res => { if (res && res.type) { this.messageService.add(res.type); } }); } } 

Pindah ke repositori NgRx utama


Seperti yang diumumkan pada ng-conf 2018 , angular-ngrx-data akan segera dimigrasikan ke repositori NgRx utama.

Mengurangi Boilerplate dengan video pembicaraan NgRx - Brandon Roberts & Mike Ryan


Referensi


Pembuat anguar-ngrx-data:
- John Papa twitter.com/John_Papa
- Ward Bell twitter.com/wardbell

Repositori resmi:
- NgRx
- angular-ngrx-data

Contoh aplikasi:
- dengan NgRx tanpa angular-ngrx-data
- dengan NgRx dan data angular-ngrx

Komunitas Angular berbahasa Rusia di Telegram

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


All Articles