میان افزارها (Middleware) در Redux

31 فروردین 1398
درسنامه درس 21 از سری آموزش react (ری اکت)
React-redux-middlewares

در درس قبلی، Redux را به برنامه خود اضافه کردیم، با reducer ها کار کردیم، actionCreator را بروزرسانی و Redux را به کامپوننت های ری اکت متصل کردیم. در این درس با یکی دیگر از قابلیت های قدرتمند Redux به نام Redux middleware آشنا خواهیم شد.

Redux Middleware (میان افزار Redux)

middleware ها معمولا به سرویس های نرم افزاری گفته می شوند که قابلیت های مختلف نرم افزار را به هم متصل می کند.

در redux، middleware ها به عنوان یک لایه اضافی بین اکشن ارسال شده و Reducer قرار می گیرد:

[ Action ] <-> [ Middleware ] <-> [ Dispatcher ]

به عنوان مثال لاگین کردن، گزارش های خطا، مسیریابی، مدیریت درخواست های غیرهمزمان همگی نمونه هایی از middleware هستند.

مدیریت درخواست های غیرهمزمان، مثل یک درخواست http به یک سرور را در نظر بگیرید. middleware ها یکی از بهترین قابلیت هایی است که می تواند در انجام اینکار به ما کمک کند.

Api Middleware

ما می خواهیم چند middleware برای مدیریت ایجاد درخواست های غیرهمزمان در برنامه مان ایجاد کنیم. middlewareها بین اکشن و reducer ها قرار می گیرند. middleware ها اکشن های ارسال شده را گرفته و کدهای آنها به همراه جزئیات اکشن و state های فعلی را اجرا می کنند.

middleware ها یک انتزاع قدرتمند را ارائه می دهند. بیایید ببینیم که چطور باید از آنها برای مدیریت برنامه خود استفاده کنیم.

در ادامه کار با currentTime redux که در درس قبلی روی آن کار می کردیم، یک middleware برای واکشی زمان جاری از سروری که در چند درس قبلی زمان جاری را از آن دریافت می کردیم، ایجاد می کنیم.

قبل از شروع کار، باید currentTime را از rootReducer که در فایل reducer.js قرار دارد گرفته و به فایل خودمان اضافه کنیم.

برای شروع، ابتدا با فایل redux/currentTime.js کار می کنیم. در این فایل دو آبجکت را export می کنیم:

  • initialState: وضعیت ابتدایی برای این قسمت از ساختار درختی state
  • reducer: دسته بندی Reducerها
import * as types from './types';

export const initialState = {
  currentTime: new Date().toString(),
}

export const reducer = (state = initialState, action) => {
  switch(action.type) {
    case types.FETCH_NEW_TIME:
      return { ...state, currentTime: action.payload}
    default:
      return state;
  }
}

export default reducer

بعد از خارج کردن currentTime از reducer ، باید فایل reducer.js را بروزرسانی کنیم تا بتواند یک فایل جدید در rootReducer دریافت کند. خوشبختانه اینکار خیلی راحت است.

import { combineReducers } from 'redux';

import * as currentUser from './currentUser';
import * as currentTime from './currentTime';

export const rootReducer = combineReducers({
  currentTime: currentTime.reducer,
  currentUser: currentUser.reducer,
})

export const initialState = {
  currentTime: currentTime.initialState,
  currentUser: currentUser.initialState,
}

export default rootReducer

در انتها، تابع configureStore را برای خارج کردن rootReducer و initialState از فایل موردنظر، بروزرسانی می کنیم:

import { rootReducer, initialState } from './reducers'
// ...
export const configureStore = () => {
  const store = createStore(
    rootReducer,
    initialState,
  );

  return store;
}

Middleware (میان افزار)

middleware ها توابعی هستند که یک store را دریافت می کنند، و همان طور که انتظار می رود یک متدی که تابع next را دریافت کرده و یک تابع دیگر که یک اکشن را به عنوان پارامتر دریافت می کند، را به عنوان خروجی بر می گرداند. آیا سردرگم شدید؟

اجازه بدهید با یک مثال به طور عملی با این موضوع آشنا شویم.

نوشتن یک middleware ساده

در این قسمت می خواهیم یک middleware خیلی ساده ایجاد کنیم تا به طرز کار آنها آشنا شویم و ببینیم که چطور می شود آنها را به برنامه خود اضافه کنیم.

سینتکس یک middleware مانند زیر است:

const loggingMiddleware = (store) => (next) => (action) => {
  // Our middleware
}

آیا چیزی از سینتکس آن نفهمیدید؟ نگران نباشید، این اولین باری است که این سینتکس را می بینیم. حال می خواهیم نگاه عمیق تری به آن بیندازیم و ببینیم که چطور کار می کند. ابتدا عبارت logginMiddleware بالا را مطابق زیر بازنویسی می کنیم:

const loggingMiddleware = function(store) {
  // Called when calling applyMiddleware so
  // our middleware can have access to the store

  return function(next) {
    // next is the following action to be run
    // after this middleware

    return function(action) {
      // finally, this is where our logic lives for
      // our middleware.
    }
  }
}

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

const loggingMiddleware = (store) => (next) => (action) => {
  // Our middleware
  console.log(`Redux Log:`, action)
  // call the next function
  next(action);
}

middleware باعث می شود که در Store هر زمانی که یک اکشن فراخوانی شده، جزئیات آن اکشن را توسط console.log نمایش دهیم.

همچنین برای اعمال middleware به برنامه، از تابع applyMiddleware که در آرگومان سوم متد ()createStore قرار می گیرد، استفاده می کنیم.

import { createStore, applyMiddleware } from 'redux';

برای اعمال middleware همچنین می توانیم متد ()applyMiddleware را در تابع ()createStore فراخوانی کنیم. در فایل src/redux/configureStore.js متد ()applyMiddleware را به متد ()createStore اضافه می کنیم.

  const store = createStore(
    rootReducer,
    initialState,
    applyMiddleware(
      apiMiddleware,
      loggingMiddleware,
    )
  );

حال middleware در محل مورد نظر قرار می گیرد. کنسول مرورگرتان را باز کرده و همان طور که می بینید تمام اکشن هایی که فراخوانی شده اند، نمایش داده می شوند. حال روی دکمه update کلیک کنید تا خروجی را ببینید.

همان طور که دیدید، middleware ها به ما اجازه می دهند تا یک تابع را در زنجیره فراخوانی Redux action اضافه کنیم. داخل این توابع می توانیم در اکشن، به state دسترسی داشته باشیم و همچنین اکشن های دیگر را ارسال کنیم.

می توانیم یک middleware یی بنویسیم که تنها اکشن هایی متناظر با درخواست های api را دریافت کند.این middleware می تواند تنها اکشن هایی که یک ویژگی به خصوصی دارند را دریافت کنند. برای مثال می توانیم یک آبجکت meta با نوعی برابر 'api' روی اکشن داشته باشیم. از این قابلیت استفاده می کنیم تا مطمئن شویم middleware مان اکشن های دیگری که با درخواست های api مرتبط نیستند را مدیریت نکنند.

const apiMiddleware = store => next => action => {
  if (!action.meta || action.meta.type !== 'api') {
    return next(action);
  }

  // This is an api request
}

اگر آبجکت meta اکشنی از نوع 'api' داشت، آن درخواست را توسط apiMiddleware دریافت می کنیم.

حال updateTime() ، actionCreator را برای وارد کردن این خصوصیات به داخل یک درخواست api تغییر می دهیم.

ماژول currentTime که در مسیر src/redux/currentTIme.js قرار دارد را باز کرده و تابع ()fetchNewTime را پیدا کنید.

سپس url آن را به آبجکت meta برای این درخواست، ارسال می کنیم. حتی می توانیم داخل فراخوانی actionCreator پارامتر هم دریافت کنیم.

const host = 'https://andthetimeis.com'
export const fetchNewTime = ({ timezone = 'pst', str='now'}) => ({
  type: types.FETCH_NEW_TIME,
  payload: new Date().toString(),
  meta: {
    type: 'api',
    url: host + '/' + timezone + '/' + str + '.json'
  }
})

هنگامی که ما دکمه را برای بروزرسانی زمان فشار دهیم، apiMiddleware مان این درخواست را قبل از اینکه به reducer برسد، دریافت می کند. برای هر فراخوانی که ما آن را در middleware دریافت می کنیم می توانیم بخش آبجکت meta را جدا کرده و با استفاده از این گزینه ها یک درخواست ایجاد کنیم. همچنین می توانیم کل آبجکت meta را از طریق متد ()fetch هم ارسال کنیم.

مراحلی که api middleware مان باید طی کند عبارتند از:

  • پیدا کردن آدرس درخواست و نوشتن optionهای درخواست از meta دریافت شد
  • ایجاد یک درخواست
  • تبدیل درخواست به یک آبجکت جاوا اسکریپت
  • ارسال پاسخ به کاربر یا Redux

حال بیایید قدم به قدم جلو برویم. ابتدا url را گرفته و یک fetchOption برای ارسال به ()fetch ایجاد می کنیم. توضیحات ارائه شده تا به اینجا را در کد زیر نشان داده ایم:

const apiMiddleware = store => next => action => {
  if (!action.meta || action.meta.type !== 'api') {
    return next(action);
  }
  // This is an api request

  // Find the request URL and compose request options from meta
  const {url} = action.meta;
  const fetchOptions = Object.assign({}, action.meta);

  // Make the request
  fetch(url, fetchOptions)
    // convert the response to json
    .then(resp => resp.json())
    .then(json => {
      // respond back to the user
      // by dispatching the original action without
      // the meta object
      let newAction = Object.assign({}, action, {
        payload: json.dateString
      });
      delete newAction.meta;
      store.dispatch(newAction);
    })
}

export default apiMiddleware

ما چندین گزینه برای نحوه ارسال پاسخ به کاربر در زنجیره Redux داریم.

ما ترجیح می دهیم با همان نوع پاسخ، بدون تگ meta پاسخ بدهیم و بدنه پاسخ را به عنوان payload اکشن جدید قرار دهیم.

به این ترتیب، ما مجبور نیستیم redux reducer را تغییر دهیم تا پاسخ را به گونه ای متفاوت مدیریت کنیم.

همچنین به یک پاسخ منفرد (تکی) هم محدود نیستیم. فرض کنید وقتی که درخواست کامل شد، کاربر یک کالبک onSuccess را برای فراخوانی ارسال می کند.

ما می توانیم کالبک onSuccess را فراخوانی کرده و سپس آن را به زنجیره ارسال کنیم:

const apiMiddleware = store => next => action => {
  if (!action.meta || action.meta.type !== 'api') {
    return next(action);
  }
  // This is an api request

  // Find the request URL and compose request options from meta
  const {url} = action.meta;
  const fetchOptions = Object.assign({}, action.meta);

  // Make the request
  fetch(url, fetchOptions)
    // convert the response to json
    .then(resp => resp.json())
    .then(json => {
      if (typeof action.meta.onSuccess === 'function') {
        action.meta.onSuccess(json);
      }
      return json; // For the next promise in the chain
    })
    .then(json => {
      // respond back to the user
      // by dispatching the original action without
      // the meta object
      let newAction = Object.assign({}, action, {
        payload: json.dateString
      });
      delete newAction.meta;
      store.dispatch(newAction);
    })
}

حال با بروزرسانی کد زیر، api middleware را به زنجیره مان اضافه می کنیم.

import { createStore, applyMiddleware } from 'redux';
import { rootReducer, initialState } from './reducers'

import loggingMiddleware from './loggingMiddleware';
import apiMiddleware from './apiMiddleware';

export const configureStore = () => {
  const store = createStore(
    rootReducer,
    initialState,
    applyMiddleware(
      apiMiddleware,
      loggingMiddleware,
    )
  );

  return store;
}

export default configureStore;

دقت داشته باشید که ما مجبور نیستیم که کدهای ویومان را برای بروزرسانی نحوه دستکاری داده در ساختار state تغییر بدهیم:

این ساده ترینmiddleware یی بود که نوشتیم، اما اصول کلی را به خوبی به شما نشان دادیم. آیا فکر کردید که چطور می شود یک سرویس Caching (کش کردن) نوشت تا برای داده ای که قبلاً داشته ایم، درخواست جدید ارسال نکنیم؟چطور می شود درخواست های معلق را مدیریت کرد؟ برای مثال چطور می توان یک spinner برای درخواست هایی که هنوز کامل نشده اند، ایجاد کرد؟

بسیارخوب، تا به اینجا به تسلط خوبی بر Redux رسیدیم و می توانیم به مرحله بعدی برویم. در جلسات بعدی سعی می کنیم به این سوالات پاسخ دهیم.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش react (ری اکت) توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.