angular-ngrx-data - إدارة الدولة و CRUD في خمس دقائق

الصورة
حتى الآن ، لا يكتمل تطبيق SPA واحد كبير بدون إدارة الدولة . هناك العديد من الحلول للزاوية في هذا المجال. الأكثر شعبية من هذه هي NgRx . ينفذ نمط Redux باستخدام مكتبة RxJs ولديه أدوات جيدة.

في هذه المقالة ، سنتناول باختصار وحدات NgRx الرئيسية ونركز بشكل أكثر تفصيلاً على مكتبة البيانات الزاويّة ngrx ، والتي تسمح لك بعمل CRUD الكامل مع إدارة الحالة في خمس دقائق.

مراجعة NgRx


يمكنك قراءة المزيد عن NgRx في المقالات التالية:

- تطبيقات تفاعلية على Angular / NGRX. الجزء 1. مقدمة
- تطبيقات تفاعلية على Angular / NGRX. الجزء 2. المخزن
- تطبيقات تفاعلية على Angular / NGRX. الجزء 3. الآثار

النظر بإيجاز في الوحدات الرئيسية من NgRx ، إيجابياتها وسلبياتها.

NgRx / store - يطبق نمط Redux.

تنفيذ متجر بسيط
counter.actions.ts
export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT'; export const RESET = 'RESET'; 

عداد. مخفض

 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; } } 
.
الاتصال بالوحدة النمطية
 import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import { counterReducer } from './counter'; @NgModule({ imports: [StoreModule.forRoot({ count: counterReducer })], }) export class AppModule {} 

استخدم في المكون
 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 - يسمح لك بتتبع التغييرات في التطبيق من خلال redux-devtools .

مثال اتصال
 import { StoreDevtoolsModule } from '@ngrx/store-devtools'; @NgModule({ imports: [ StoreModule.forRoot(reducers), //      StoreModule StoreDevtoolsModule.instrument({ maxAge: 25, //   25  }), ], }) export class AppModule {} 


NgRx / تأثيرات - يسمح لك بإضافة البيانات الواردة في التطبيق إلى المستودع ، مثل طلبات http.

مثال
./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) {} } 

ربط التأثير بالوحدة النمطية

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


NgRx / كيان - يوفر القدرة على العمل مع صفائف البيانات.

مثال
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; 

مخفضات / 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] ); 


ما هي النتيجة؟


نحصل على إدارة كاملة للدولة مع مجموعة من المزايا:

- مصدر بيانات واحد للتطبيق ،
- يتم تخزين الحالة بشكل منفصل عن التطبيق ،
- أسلوب كتابة واحد لجميع المطورين في المشروع ،
- changeDetectionStrategy.OnPush في جميع مكونات التطبيق ،
- تصحيح أخطاء مريح من خلال أدوات redux-devtools ،
- سهولة الاختبار ، كما المخفضات هي وظائف نقية.

ولكن هناك أيضًا عيوب:

- عدد كبير من الوحدات التي تبدو غير مفهومة ،
- الكثير من نفس النوع من التعليمات البرمجية التي لن تنظر إليها بدون حزن ،
- صعوبة الإتقان بسبب كل ما سبق.

الخام


كقاعدة ، يتم شغل جزء كبير من التطبيق من خلال العمل مع الكائنات (الإنشاء والقراءة والتحديث والحذف) ، لذلك ، من أجل راحة العمل ، تم اختراع مفهوم CRUD (إنشاء ، قراءة ، تحديث ، حذف). وبالتالي ، يتم توحيد العمليات الأساسية للعمل مع جميع أنواع الكائنات. لقد كان مزدهرًا لفترة طويلة على الواجهة الخلفية. تساعد العديد من المكتبات في تنفيذ هذه الوظيفة والتخلص من العمل الروتيني.

في NgRx ، تكون وحدة الكيان مسؤولة عن CRUD ، وإذا نظرت إلى مثال على تنفيذها ، يمكنك أن ترى على الفور أن هذا هو الجزء الأكبر والأكثر تعقيدًا من NgRx . لهذا السبب قام جون بابا وورد بيل بإنشاء بيانات ngrx-الزاوي .

بيانات ngrx الزاوي


angular-ngrx-data هي مكتبة الوظيفة الإضافية NgRx التي تسمح لك بالعمل مع صفائف البيانات دون كتابة كود إضافي.
بالإضافة إلى إنشاء إدارة ولاية كاملة ، تتعهد بإنشاء خدمات باستخدام http للتفاعل مع الخادم.

تأمل في مثال


التثبيت

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

وحدة بيانات Angular-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 {} 

اتصل بالتطبيق

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

لقد حصلنا للتو على واجهة برمجة التطبيقات التي تم إنشاؤها للعمل مع الواجهة الخلفية وتكامل واجهة برمجة التطبيقات مع NgRx ، دون كتابة تأثير واحد ومخفض ومحدث ومحدد.

دعونا نفحص بمزيد من التفصيل ما يحدث هنا.


يضبط ثابت dataServiceConfig الافتراضي التكوين لواجهة برمجة التطبيقات الخاصة بنا ويتصل بوحدة موفري الخدمة . تشير خاصية الجذر إلى أين تذهب للطلبات. إذا لم يتم تعيينه ، فسيكون الإعداد الافتراضي هو "api".

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

يحدد ثابت لكيان Metadata أسماء المخزن الذي سيتم إنشاؤه عند توصيل NgrxDataModule.forRoot .

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

يتكون المسار إلى API من المسار الأساسي (في حالتنا "crud") واسم المتجر.
على سبيل المثال ، للحصول على مستخدم برقم معين ، سيكون المسار "crud / user / {userId}".

للحصول على قائمة كاملة بالمستخدمين ، تتم إضافة الحرف "s" - "crud / user s " إلى نهاية اسم المتجر افتراضيًا.

إذا كنت بحاجة إلى مسار مختلف للحصول على القائمة الكاملة (على سبيل المثال ، "أبطال" وليس "أبطال") ، فيمكنك تغييره عن طريق تعيين أسماء الجمع وربطها بـ NgrxDataModule.forRoot .

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

الاتصال في المكون


للاتصال في المكون ، تحتاج إلى تمرير مُنشئ serviceServices إلى المُنشئ واستخدام طريقة getEntityCollectionService لتحديد خدمة التخزين المطلوب

 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'); } ... } 

لربط القائمة بالمكون ، يكفي أخذ الخاصية $ $ للكيانات من الخدمة ، وللحصول على البيانات من الخادم ، اتصل بطريقة getAll () .

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

بالإضافة إلى البيانات الأساسية ، يمكنك الحصول على:

- تحميل $ ، تحميل $ - الحصول على حالة تحميل البيانات ،
- أخطاء $ - أخطاء عند تشغيل الخدمة ،
- عدد $ - إجمالي عدد السجلات في المستودع.

الطرق الرئيسية للتفاعل مع الخادم:

- getAll () - الحصول على القائمة الكاملة للبيانات ،
- getWithQuery (استعلام) - الحصول على قائمة تمت تصفيتها باستخدام معلمات الاستعلام ،
- getByKey (id) - الحصول على سجل واحد بواسطة المعرف ،
- إضافة (كيان) - إضافة كيان جديد مع طلب دعم ،
- حذف (كيان) - حذف كيان مع طلب دعم ،
- تحديث (كيان) - تحديث الكيان بطلب النسخ.

طرق التخزين المحلية:

- addManyToCache (كيان) - إضافة مجموعة من الكيانات الجديدة إلى المستودع ،
- addOneToCache (كيان) - إضافة كيان جديد فقط إلى المستودع ،
- removeOneFromCache (id) - إزالة كيان واحد من المستودع ،
- updateOneInCache (كيان) - تحديث الكيان في المستودع ،
- upsertOneInCache (كيان) - في حالة وجود كيان بالمعرف المحدد ، يتم تحديثه ، إن لم يكن ، يتم إنشاء كيان جديد ،
- وآخرون

مثال على استخدام المكون

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

تنقسم جميع طرق بيانات angular-ngrx إلى العمل محليًا والتفاعل مع الخادم. هذا يسمح لك باستخدام المكتبة عند معالجة البيانات على كل من العميل واستخدام الخادم.

تسجيل الدخول


للتسجيل ، تحتاج إلى إدخال EntityServices في مكون أو خدمة واستخدام الخصائص:

- المخفضة الإجراءات $ - لإجراءات التسجيل ،
- لكياناكشنجراسز - لأخطاء التسجيل.

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

الانتقال إلى مستودع NgRx الرئيسي


كما تم الإعلان عنه في ng-conf 2018 ، سيتم ترحيل البيانات الزاويّة ngrx إلى مستودع NgRx الرئيسي قريبًا .

الحد من Boilerplate مع فيديو نقاش NgRx - براندون روبرتس ومايك رايان


المراجع


مبدعو بيانات anguar-ngrx:
- جون بابا twitter.com/John_Papa
- وارد بيل twitter.com/wardbell

المستودعات الرسمية:
- NgRx
- بيانات ngrx الزاوي

مثال على التطبيق:
- مع NgRx بدون بيانات ngrx الزاوي
- مع بيانات NgRx الزاوي- ngrx

المجتمع الزاوي الناطق بالروسية على برقية

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


All Articles