
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. EfekSecara singkat pertimbangkan modul utama
NgRx , pro dan kontra.
NgRx / store - mengimplementasikan pola Redux.
Implementasi toko sederhanacounter.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.
Contohuser.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/wardbellRepositori resmi:-
NgRx-
angular-ngrx-dataContoh aplikasi:-
dengan NgRx tanpa angular-ngrx-data-
dengan NgRx dan data angular-ngrxKomunitas Angular berbahasa Rusia di Telegram