آشنایی با NgRx در انگولار — به زبان ساده

۱۲۰۴ بازدید
آخرین به‌روزرسانی: ۱۱ شهریور ۱۴۰۲
زمان مطالعه: ۹ دقیقه
آشنایی با NgRx در انگولار — به زبان ساده

اغلب افرادی که به تازگی شروع به کار با NgRx در انگولار می‌کنند، آن را در ابتدا دشوار می‌یابند. ممکن است نیاز به مطالعه و پژوهش زیادی برای درک مبانی مقدماتی NgRx داشته باشید. هدف اصلی این مقاله ارائه دانشی در این مورد است تا خوانندگان بتوانند با سرعت و سهولت بیشتری طرز کار با NgRx در انگولار را بیاموزند.

NgRx چیست؟

برای شروع باید اشاره کنیم که NgRx اختصاری برای عبارت «اکستنشن‌های واکنشی انگولار» (Angular Reactive Extensions) است. NgRx یک سیستم مدیریت حالت است که بر مبنای الگوی redux طراحی شده است. پیش از آن که وارد جزییات آن بشویم ابتدا باید مفهوم «حالت» (State) را در یک اپلیکیشن انگولار بشناسیم.

حالت

از لحاظ نظری حالت یک اپلیکیشن کل حافظه آن را تشکیل می‌دهد. به بیان ساده حالت اپلیکیشن از داده‌هایی تشکیل یافته است که از طریق فراخوانی‌های API، ورودی‌های کاربر، حالت UI ارائه شده، ترجیحات اپلیکیشن و غیره به دست می‌آید. یک مثال ساده و مناسب از حالت اپلیکیشن می‌تواند لیستی از مشتریان باشد که در یک اپلیکیشن CRM نگهداری می‌شود.

در ادامه تلاش می‌کنیم حالت اپلیکیشن را در چارچوب یک اپلیکیشن انگولار درک کنیم. همان طور که همه می‌دانید یک اپلیکیشن انگولار به طور معمول از کامپوننت‌های زیادی تشکیل می‌یابد. هر کدام از این کامپوننت‌ها حالت خاص خود را دارند و هیچ اطلاعی از حالت کامپوننت‌های دیگر ندارند. برای اشتراک اطلاعات بین کامپوننت‌های والد-فرزند از دکوراتورهای Input@ و Output@ استفاده می‌کنیم. با این حال این رویکرد تنها در صورتی مقدور است که اپلیکیشن تنها از چند کامپوننت مانند تصویر زیر تشکیل یافته باشد:

NgRx در انگولار

زمانی که تعداد کامپوننت‌ها افزایش می‌یابد، ارسال اطلاعات بین کامپوننت‌ها صرفاً از طریق دکوراتورهای Input@ و Output@ دشوار می‌شود. در تصویر زیر این حالت بهتر قابل مشاهده است.

NgRx در انگولار

اگر مجبور باشید اطلاعات را از کامپوننت سه به کامپوننت شش ارسال کنید، باید چهار بار پرش کنید و سه کامپوننت دیگر را نیز درگیر کنید. همچنان که می‌بینید، این روش پیچیده‌ای است و این روش برای مدیریت حالت مستعد بروز خطا است. به همین جهت الگوریتم ریداکس را مورد استفاده قرار می‌دهیم.

Redux

ریداکس الگویی است که برای ساده‌سازی فرایند مدیریت حالت در اپلیکیشن‌های جاوا اسکریپت (نه فقط انگولار) مورد استفاده قرار می‌گیرد. ریداکس به طور عمده بر مبنای سه اصل اساسی کار می‌کند که در ادامه توضیح می‌دهیم:

  • مبدأ منفرد واقعیت (Single source of truth) – این اصل به آن معنی است که حالت اپلیکیشن در یک درخت شیء درون یک Store منفرد نگهداری می‌شود. این استور مسئول ذخیره‌سازی داده‌ها و ارائه آن‌ها به کامپوننت‌ها در موارد تقاضا است. بر اساس این معماری گردش داده‌ها بین استور و کامپوننت‌ها به جای کامپوننت به کامپوننت صورت می‌گیرد. شکل زیر این اصل را به خوبی نمایش می‌دهد:

NgRx در انگولار

  • حالت فقط-خواندنی (Read-only state) – به بیان دیگر حالت تغییرناپذیر است. این وضعیت لزوماً به آن معنی نیست که حالت همواره ثابت می‌ماند و نمی‌توان آن را تغییر داد. بلکه به این معنی است که امکان تغییر دادن مستقیم حالت وجود ندارد. برای ایجاد تغییر در حالت باید اکشن‌هایی را از بخش‌های مختلف اپلیکیشن به استور ارسال کنید.
  • حالت با «تابع‌های خالص» (pure functions) ویرایش می‌شود - ارسال اکشن موجب اجرای یک مجموعه تابع خالص به نام «ردیوسر» (reducers) می‌شود. ردیوسرها مسئول ویرایش حالت به روش‌های مختلف بر اساس اکشن دریافتی هستند. نکته کلیدی در اینجا آن است که یک ردیوسر همواره شیء حالت جدیدی با برخی تغییرات بازگشت می‌دهد.

NgRx

NgRx به یک گروه از کتابخانه‌ها گفته می‌شود که از الگوی ریداکس الهام گرفته‌اند. همان طور که از نام آن مشخص است NgRx به طور خاص برای اپلیکیشن‌های انگولار به عنوان یک راه‌حل مدیریت حالت نوشته شده است. در بخش بعدی به بررسی دقیق‌تر این کتابخانه می‌پردازیم.

عناصر بنیادی NgRx شامل Store Action ،Reducers ،Selectors و Effects

در این بخش برخی از عناصر بنیادی تشکیل‌دهنده NgRx را بررسی می‌کنیم.

Store

استور یک عنصر کلیدی در کل فرایند مدیریت حالت محسوب می‌شود. استور حالت را نگهداری می‌کند و موجب تسهیل تعامل بین کامپوننت‌ها و حالت می‌شود. می‌توان از طریق تزریق وابستگی انگولار چنان که در ادامه دیده می‌شود، یک ارجاع به استور به دست آورد:

1constructor(private store: Store<AppState>) {}

ارجاع استور می‌تواند متعاقباً برای دو عملیات اصلی استفاده شود:

  • ارسال اکشن‌ها به استور از طریق متد store.dispatch(…)‎ که به نوبه خود ردیوسرها و افکت‌ها را تحریک می‌کند.
  • بازیابی حالت اپلیکیشن از طریق سلکتورها.

ساختار یک درخت شیء حالت

فرض کنید اپلیکیشن شما شامل دو ماژول قابلیت است که User و Product نام دارند. هر کدام از این ماژول‌ها بخش‌های متفاوتی از حالت کلی را مدیریت می‌کنند. اطلاعات Product همواره در بخش products در حالت باقی می‌ماند. اطلاعات User نیز همواره در بخش User حالت جای می‌گیرد. این بخش‌ها به نام Slice نامیده می‌شوند.

NgRx در انگولار

اکشن‌ها

اکشن یک دستورالعمل است که به استور ارسال می‌شود و می‌تواند برخی متادیتا (payload) داشته باشد. بر اساس نوع اکشن، استور تصمیم می‌گیرد که کدام عملیات باید اجرا شود. در کد یک اکشن به وسیله یک شیء قدیمی جاوا اسکریپت نمایش می‌یابد که دو خصوصیت اصلی به نام‌های type و payload دارد. Payload یک خصوصیت اختیاری است که از سوی ردیوسرها برای ویرایش حالت استفاده می‌شود. قطعه کد زیر این مفهوم را به تصویر می‌کشد:

NgRx در انگولار

1{
2  "type": "Login Action",
3  "payload": {
4    userProfile: user
5  }
6}

NgRx نسخه 8 یک تابع کارکردی به نام createAction برای تعریف action creator ارائه کرده است. توجه کنید که این action creators است و نه action. در ادامه نمونه‌ای از کد آن را می‌بینید:

1export const login = createAction(
2    "[Login Page] User Login",
3    props<{user: User}>()
4);

سپس می‌توانید از اکشن کریتور به نام login که یک تابع است برای ساختن اکشن و ارسال آن به استور مانند زیر استفاده کنید. user شیء payload است که به اکشن ارسال می‌شود:

1this.store.dispatch(login({user}));

ردیوسرها

ردیوسرها مسئول ویرایش حالت و بازگشت یک شیء حالت جدید با تغییرات اعمال شده هستند. ردیوسرها دو پارامتر می‌گیرند که یکی حالت جاری و دیگری اکشن مورد نظر است ردیوسر بر اساس نوع اکشن دریافتی، ویرایش خاصی را روی حالت جاری اعمال کرده و حالت جدیدی می‌سازد. این مفهوم در تصویر زیر نمایش یافته است:

NgRx در انگولار

NgRx نیز مشابه اکشن‌ها یک تابع کارکردی به نام createReducer برای ایجاد ردیوسرها ارائه کرده است. یک فراخوانی تابع معمولی createReducer می‌تواند مانند زیر باشد:

1export const initialAuthState: AuthState = {
2    user: undefined
3};
4
5export const authReducer = createReducer(
6
7    initialAuthState,
8
9    on(AuthActions.login, (state, action) => {
10        return {
11            user: action.user
12        }
13    }),
14
15    on(AuthActions.logout, (state, action) => {
16        return {
17            user: undefined
18        }
19    })
20
21);

چنان که می‌بینید، این تابع در حالت اولیه تابع‌های تغییر حالت «یک به چند» را می‌گیرد که شیوه واکنش به اکشن‌های مختلف را تعریف می‌کند. هر کدام از این تابع‌های تغییر حالت، حالت جاری و پارامترهای اکشن را به عنوان آرگومان می‌گیرند و یک حالت جدید بازگشت می‌دهند.

افکت‌ها

با استفاده از افکت‌ها می‌توان کارهای خاصی در زمان ارسال اکشن به استور اجرا کرد. این مفهوم را با بررسی یک مثال آموزش می‌دهیم. زمانی که یک کاربر با موفقیت به حساب خود در یک اپلیکیشن وارد می‌شود یک اکشن با نوع Login Action به استور ارسال می‌شود که حامل اطلاعات کاربر در payload است. یک تابع ردیوسر به این اکشن گوش می‌دهد و حالت را با اطلاعات کاربر ویرایش می‌کند. به علاوه به عنوان یک تأثیر جانبی می‌توانید اطلاعات کاربر را در local storage مرورگر نیز ذخیره کنید. به این ترتیب از تأثیر جانبی کمی توان برای اجرای برخی کارهای اضافی استفاده کرد.

چندین روش برای ایجاد افکت در NgRx وجود دارند. در ادامه یک روش خام و خودگویا برای ایجاد افکت می‌بینید. لطفاً توجه داشته باشید که عموماً از این روش برای ایجاد افکت استفاده نمی‌کنیم. این تنها یک مثال برای توضیح وقایعی است که در پشت پرده رخ می‌دهند.

1@Injectable()
2export class AuthEffects {
3
4    constructor(private actions$: Actions, private router: Router) {
5
6      const login$ = this.actions$
7                      .pipe(
8                        ofType(AuthActions.login),
9                        tap(action => localStorage.setItem('user',
10                                JSON.stringify(action.user))
11                        )
12                      );
13
14      login$.subscribe();
15
16    }
17}
  • یک observable به نام actions$ اکشن‌های دریافتی از سوی استور را ارسال می‌کند. این مقادیر از یک زنجیره عملگرها عبور می‌کنند.
  • ofType نخستین عملگر مورد استفاده است. این یک عملگر خاص است که از سوی NgRx برای فیلتر کردن اکشن‌ها بر مبنای نوعشان ارائه شده است. در این نمونه تنها اکشن‌های از نوع login مجاز به عبور به بقیه بخش‌های زنجیره هستند.
  • tap عملگر دوم است که در زنجیره برای ذخیره اطلاعات کاربر در حافظه لوکال مرورگر مورد استفاده قرار می‌گیرد. عملگر tap به طور کلی برای اجرای تأثیرات جانبی در یک زنجیره عملگر استفاده می‌شود.
  • در نهایت یک اشتراک دستی در یک observable به نام login$ داریم.

با این حال این رویکرد چندین عیب عمده دارد. باید به صورت دستی در observable مشترک شوید که رویه مناسبی نیست. بدین ترتیب همواره باید به صورت دستی ثبت نام کنید که منجر به فقدان قابلیت نگهداری می‌شود.

  • اگر خطایی در زنجیره عملگر رخ دهد، observable خطا را در خروجی ارائه می‌دهد و مقادیر بعدی یعنی اکشن‌ها نیز ارسال می‌شوند. در نتیجه تأثیر جانبی اجرا نمی‌شود. از این رو باید مکانیسمی داشته باشیم که به صورت دستی وهله‌ای از observanle ایجاد کرده و در صورت بروز خطا مجدداً در آن ثبت نام کنیم.
  • برای غلبه بر این مشکلات، NgRx یک تابع کارکردی به نام createEffect برای ساخت افکت جانبی ارائه کرده است. یک تابع معمول createEffect مانند زیر است:
1    login$ = createEffect(() =>
2        this.actions$
3            .pipe(
4                ofType(AuthActions.login),
5                tap(action => localStorage.setItem('user',
6                        JSON.stringify(action.user))
7                )
8            )
9    ,
10    {dispatch: false});

متد createEffect یک تابع می‌گیرد که یک observable و یک شیء پیکربندی (اختیاری) به عنوان پارامتر بازگشت می‌دهد.

NgRx اشتراک در observanle بازگشتی از سوی تابع پشتیبان را مدیریت می‌کند و از این رو نیازی به اشتراک یا لغو اشتراک دستی نیست. به علاوه اگر خطایی در زنجیره عملگر رخ دهد، NgRx یک observable جدید بازگشت می‌دهد و مجدداً مشترک می‌شود تا مطمئن شود که تأثیر جانبی همواره اجرا خواهد شد.

اگر dispatch مقدار پیش‌فرض true را در شیء پیکربندی داشته باشد، متد createEffect یک <Observable<Action بازگشت می‌دهد. در غیر این صورت یک <Observable<Unknown بازگشت خواهد داد. و در غیر این صورت مشخصه dispatch مقدار true دارد. NgRx در observable بازگشتی از نوع Observable<Action>‎ مشترک می‌شود و اکشن‌های دریافتی را به استور ارسال می‌کند.

اگر اکشن دریافتی را به نوع متفاوتی از اکشن در زنجیره عملگرها نگاشت نکنید، باید dispatch را روی flase تنظیم کنید. در غیر این صورت اجرای آن موجب تولید یک حلقه نامتناهی می‌شود، زیرا همان اکشن دوباره ارسال می‌شود و در استریم actions$ بارها و بارها دریافت می‌شود. برای نمونه dispatch را در کد زیر نباید روی false تنظیم کنید، زیرا اکشن اصلی را به نوع متفاوتی از اکشن در زنجیره عملگرها نگاشت می‌کند.

1loadCourses$ = createEffect(
2    () => this.actions$
3        .pipe(
4            ofType(CourseActions.loadAllCourses),
5            concatMap(action =>
6                this.coursesHttpService.findAllCourses()),
7            map(courses => allCoursesLoaded({courses}))
8
9        )
10);

در سناریوی زیر:

  • افکت اقدام به دریافت اکشن‌های از نوع loadAllCourses می‌کند.
  • یک API اجرا می‌شود و course-ها به عنوان تأثیر جانبی بارگذاری می‌شوند.
  • پاسخ API به اکشن از نوع allCoursesLoaded به course-های بارگذاری شده نگاشت می‌شود و به صورت payload به اکشن ارسال می‌شود.
  • در نهایت اکشن theallCoursesLoaded که کاربر ایجاد کرده است به استور ارسال می‌شود. این کار از سوی NgRx در پس‌زمینه انجام می‌یابد.
  • یک ردیوسر به اکشن ورودی allCoursesLoaded گوش می‌دهد و حالت را با کورس‌های بارگذاری شده ویرایش می‌کند.

سلکتورها

سلکتورها تابع‌های خالصی هستند که برای به دست آوردن قطعه‌هایی از حالت استور مورد استفاده قرار می‌گیرند چنان که در کد زیر می‌بینید می‌توان حتی بدون استفاده از سلکتور نیز به حالت کوئری زد. اما این رویکرد برخی معایب عمده دارد:

1const isLoggedIn$ = this.store.pipe(
2map(state => !!state.user)
3);
  • استور یک observable است که می‌توان در آن مشترک شد. هر زمان که استور یک اکشن دریافت کند، شیء حالت را به مشترکان خود ارسال می‌کند.
  • می‌توان از تابع نگاشت برای به دست آوردن قطعه‌هایی از حالت استفاده کرد و هر نوع محاسبه لازم را اجرا نمود. در مثال فوق ما قطعه user را در درخت شیء حالت به دست آورده و آن را به مقدار بولی تبدیل می‌کنیم تا تشخیص دهیم آیا کاربر لاگین کرده است یا نه.
  • می‌توان هم به صورت دستی در observable به نام isLoggedIn$ مشترک شد و هم از یک قالب انگولار با پایپ ناهمگام برای خواندن مقادیر ارسالی استفاده کرد.

با این حال این رویکرد یک عیب عمده دارد. به طور کلی استور اکشن‌ها را به طور مکرر از بخش‌های مختلف اپلیکیشن دریافت می‌کند. همچنان که در پیاده‌سازی قبل هم دیدیم، هر بار که اپلیکیشن یک اکشن دریافت می‌کند، یک شیء حالت از سوی استور ارسال می‌شود و این شیء حالت دوباره وارد تابع نگاشت می‌شود و UI را به‌روزرسانی می‌کند.

با این حال، اگر نتیجه تابع نگاشت از آخرین بار تغییر نیافته باشد، نیازی برای به‌روزرسانی مجدد UI وجود ندارد. برای نمونه اگر نتیجه عبارت زیر تغییر نیافته باشد، لازم نیست نتیجه را به UI/مشترک بفرستیم:

1map(state =>!!state.user)

برای رسیدن به این وضعیت NgRx عملگری خاصی به نام select معرفی کرده است. با استفاده از عملگر select کد فوق به صورت زیر درمی‌آید:

1const isLoggedIn$ = this.store.pipe(
2select(state => !!state.user)
3);

در صورتی که نتیجه تابع نگاشت از آخرین بار تغییر نیافته باشد، عملگر select از ارسال مقادیر به UI/subscriber جلوگیری می‌کند.

این رویکرد را می‌توان از این هم بیشتر بهبود داد. حتی اگر عملگر select مقادیر تغییر نیافته را به UI/subscriber نفرستد، همچنان شیء حالت را به مقایسه و تعیین نتیجه در هر بار می‌گیرد.

همچنان که پیش‌تر اشاره کردیم یک state از سوی observable زمانی ارسال می‌شود که یک اکشن از اپلیکیشن دریافت شده باشد. اگر این حالت تغییر نیافته باشد، نتیجه مقایسه تابع نگاشت نیز تغییر نمی‌یابد از این رو نیازی به محاسبه مجدد در حالتی که شیء state نسبت به دفعه قبل تغییر نکرده باشد وجود نخواهد داشت. این همان جایی است که سلکتور به کار می‌آید.

سلکتور یک تابع خالص است که حافظه اجرای قبلی را نگهداری می‌کند. تا زمانی که ورودی تغییر نیافته باشد، خروجی محاسبه مجدد نمی‌شود. به جای آن خروجی از سوی حافظه بازگشت خواهد یابد. این فرایند «خاطرسپاری» (memoization) نام دارد.

NgRx یک تابع کاربردی به نام createSelector ارائه کرده است که سلکتورها را می‌سازد و ظرفیت خاطرسپاری ایجاد می‌کند. در ادامه مثالی از یک تابع کاربردی createSelector می‌بینید:

1export const isLoggedIn = createSelector(
2    state => state['auth'],
3    auth =>  !!auth.user
4);

تابع createSelector تابع‌های نگاشت یک به چند را می‌گیرد و قطعه‌های مختلفی از حالت و یک تابع پروجکتور بازگشت می‌دهد که محاسبه را اجرا می‌کند. تابع پروجکتور در صورتی که قطعه حالت نسبت به بار قبل تغییر نکرده باشد، اجرا نمی‌شود. برای استفاده از تابع سلکتور ایجاد شده باید یک آرگومان به عملگر select ارسال شود.

1this.isLoggedIn$ = this.store
2  .pipe(
3    select(isLoggedIn)
4  );

تعامل بین کامپوننت‌های NgRx

تصویر زیر تفاوت تعامل‌های کامپوننت‌های مختلف با همدیگر در اکوسیستم NgRX را نشان می‌دهد:

NgRx در انگولار

مزایا و معایب NgRx

در این بخش برخی از مزایا و معایب این کتابخانه را بررسی می‌کنیم.

مزایا

  • اصل «منبع منفرد حقیقت» موجب می‌شود که اشتراک اطلاعات در اپلیکیشن انگولار آسان‌تر صورت بپذیرد.
  • حالت اپلیکیشن نمی‌تواند مستقیماً از سوی کامپوننت‌ها تغییر یابد و تنها ردیوسرها می‌توانند حالت را تغییر دهند و این وضعیت موجب سهولت دیباگ کردن می‌شود.

معایب

  • یادگیری NgRx در ابتدا دشوار است.
  • اپلیکیشن سنگین‌تر می‌شود زیرا باید موارد مختلفی از ردیوسرها، سلکتورها، افکت‌ها و غیره را تعریف کنید.

هدف عمده این مقاله ایجاد آشنایی مقدماتی با مفاهیم NgRx بوده است. امیدواریم در این مسیر موفق بوده باشیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۷ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programming
۲ دیدگاه برای «آشنایی با NgRx در انگولار — به زبان ساده»

سلام خسته نباشید
مفهوماشو خوب توضیح دادین دمتون گرم

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *