ساخت اپلیکیشن React Redux با وب توکن جاوا اسکریپت – از صفر تا صد
در این راهنما با روش ساخت اپلیکیشنهای ریداکس ریاکت از صفر آشنا خواهیم شد. نقطه تمرکز ما مدیریت فرایند ثبت نام و ورود کاربر به حساب خود است و سپس مراحل احراز هویت و اعتبارسنجی هویت با توکن وب JSON به اختصار JWT انجام مییابد. به خاطر داشته باشید که ساختارهای فایل و رویکرد ارائه شده در این راهنما تنها یکی از گزینههای مختلف ممکن است. با ما همراه باشید تا با مراحل ساخت اپلیکیشن React Redux با وب توکن جاوا اسکریپت آشنا شوید.
توجه داشته باشید که درخواستهای ما در این اپلیکیشن به یک API بکاند Ruby on Rails ارسال میشود که از قبل ساخته شده است. در صورتی که علاقهمند باشید در این مورد اطلاعات بیشتری داشته باشید، میتوانید به این ریپوی گیتهاب (+) مراجعه کنید.
راهاندازی
در این بخش ابتدا یک اپلیکیشن ریاکت با استفاده از اسکریپت Create React App میسازیم.
در ترمینال دستور زیر را اجرا کنید:
npx create-react-app jwt-react-redux
دایرکتوری را به jwt-react-redux عوض کرده و آن را باز کنید. در این دایرکتوری دستور زیر را برای نصب Redux، React Redux و Redux Thunk اجرا کنید:
npm install redux react-redux redux-thunk
اکنون از نظر پکیج، همه موارد لازم برای ادغام ریداکس در اپلیکیشن ریاکت خود را نصب کردهایم.
ما به صورت پیشدستانه در مورد زمانی که اپلیکیشن توسعه یابد و دادههای بیشتری لازم باشد فکر کردهایم و از این رو بهتر است اکشنهای خود و ردیوسرها را در دو پوشه مجزا سازماندهی کنیم. در دایرکتوری src دو پوشه به نامهای action و reducers ایجاد میکنیم. همچنین باید یک پوشه به نام components برای چندین کامپوننت که اپلیکیشن خواهد داشت بسازیم. فعلاً این پوشهها را خالی میگذاریم و روی ادغام ریداکس متمرکز میشویم.
ادغام ریداکس
به مسیر rc/index.js بروید. در ابتدای فایل باید دستور زیر را اضافه کنیم:
import {Provider} from 'react-redux'
Provider قرار است به ما امکان بدهد که store ریداکس (که شامل حالت است) را در اختیار همه کامپوننتهایی که اپلیکیشن را تشکیل میدهند قرار دهیم، یعنی Store نباید به صورت دستی به اطراف ارسال شود. به جای آن کامپوننتهای منفرد که میخواهند به Store دسترسی یابند، باید این دسترسی به آنها داده شود.
ما با Provider به عنوان یک کامپوننت رفتار میکنیم و درون تگهای Provider بقیه اپلیکیشن را ارائه میکنیم که در این مورد کامپوننت App است. یک prop منفرد باید به Provider ارسال شود که Store است و بقیه اپلیکیشن از طریق آن به اشتراک گذاشته میشود.
اکنون باید Store را با استفاده از متد زیر در redux ایجاد کنیم. در بخش فوقانی دستور زیر را اضافه میکنیم:
import {createStore} from 'redux'
createStore تابعی است که یک بار در ابتدای اپلیکیشن برای ایجاد Store ریداکس استفاده میشود. این تابع یک آرگومان میگیرد که تابعی است که باید یک شیء بازگشت دهد. این تابع آن چیزی است که به نام ردیوسر (reducer) شناخته میشود.
ردیوسرها
ما میتوانیم چندین تابع ردیوسر داشته باشیم که هر یک مسئول یک بخش از اطلاعات در حالت باشند. در این راهنما میخواهیم اطلاعات کاربران لاگین کرده و یک مثال دیگر یعنی فهرستی از کامنتها داشته باشیم. بهتر است یک تابع ردیوسر برای هر نوع اطلاعات داشته باشیم.
زیر src/reducers دو فایل به نامهای userReducer.js و commentReducer.js ایجاد میکنیم. کار خود را با userReducer.js آغاز میکنیم. میخواهیم یک متغیر به نام userReducer تعریف کرده و آن را روی یک تابع تعیین کنیم:
این تابع یک حالت پیشفرض به صورت آرگومان اول دارد و یک شیء اکشن به عنوان آرگومان دوم میگیرد. در بلوک کد، شیء اکشن یک مشخصه به صورت نوع (type) دارد. بر اساس این نوع، تغییرهای خاصی در حالت رخ خواهد داد. معمولاً از یک گزاره سوئیچ برای مدیریت شرایط مختلف استفاده میشود.
این ردیوسر باید در فایل دیگری استفاده شود و از این رو باید آن را اکسپورت کنیم. از آنجا که تنها میخواهیم یک تابع را اکسپورت کنیم از export default استفاده میکنیم.
اینک به فایل commentReducer.js میرویم. این تابع به عنوان یک مثال عمل میکند و از این رو اطلاعاتی در آن قرار نمیگیرد. به طور مشابه یک متغیر به نام commentReducer ایجاد میکنیم و روی تابع تنظیم میکنیم. حالت پیشفرض یک آرایه خالی خواهد بود که آرگومان اول است و یک شیء اکشن آرگومان دوم را تشکیل میدهد. این تابع را به عنوان اکسپورت پیشفرض قرار میدهیم.
اینک دو ردیوسر داریم و هر دوی آنها را در Store ریداکس قرار میدهیم. با این حال، createStore تنها میتواند یک ردیوسر بگیرد. برای جلوگیری از بروز یک وضعیت بغرنج، از تابع دیگری از ریداکس استفاده میکنیم که به ما امکان میدهد تا چندین ردیوسر را با هم ترکیب کنیم.
فایل دیگری را زیر دایرکتوری src/reducers ایجاد کرده و نام آن را index.js میگذاریم. در این فایل، combineReducers را از ریداکس و ردیوسرها ایمپورت میکنیم.
combineReducers یک آرگومان میگیرد که شیئی است که جفتهای کلید-مقدار ارائه میکند. کلید نماینده نام حالت ریشه است و مقدار نیز ردیوسری خواهد بود که آن را مدیریت میکند. زمانی که اقدام به استفاده از export default برای rootReducer بکنیم، نماینده شیئی خواهد بود که به createStore ارسال کردیم.
اینک به فایل src/index.js میرویم و rootReducer را ایمپورت میکنیم، applyMiddleware را از ریداکس ایمپورت میکنیم و Thunk را نیز از redux-thunk ایمپورت میکنیم. به سادهترین بیان، applyMiddleware و Thunk برای مدیریت واکشی ناهمگام در یک dispatch استفاده میشوند. rootReducer آرگومان نخست برای createStore و applyMiddleware(thunk) آرگومان دوم خواهد بود.
اکنون Store ریداکس را ایجاد کردهایم. گام بعدی ساخت اکشنهایی است که باید ردیوسرها را بهروزرسانی کنند.
اکشنها
از آنجا که commentsReducer به عنوان یک مثال عمل میکند، حالت آن تغییر نخواهد یافت. ما در این بخش روی ایجاد اکشنهایی برای userReducer متمرکز میشویم. در دایرکتوری src/actions یک فایل به نام userActions.js ایجاد میکنیم. در این فایل، چیزی به نام «اکشنساز» (action creators) را ایجاد میکنیم. اکشنسازها تابعهایی هستند که برای ایجاد تغییرهای مناسب در حالت استفاده میشوند. برای کسب اطلاعت بیشتر در این مورد به مستندات (+) مراجعه کنید.
همچنین متدهایی داریم که درخواستهای ناهمگام را مدیریت میکنند. درون این متدها درخواستهای واکشی را ایجاد میکنیم و روی دادههای بازگشتی و در صورت نیاز دادههایی که به صورت آرگومان میآیند، یک dispatch با شیء اکشن مناسب اجرا میکنیم.
fetchUser، signUserUp و autoLogin مثالهایی از مدیرت درخواستهای واکشی به سرور هستند. باید یادآوری کنیم که سرور در این راهنما یک API روبی است که توضیح در مورد کارکرد آن خارج از حیطه این مقاله است.
نکته: زمانی که درخواست موفق باشد، سرور یک شیء با مشخصههای user و token بازگشت میدهد. Token نماینده JWT تولید شده از سوی سرور است و در مرورگر به صورت زیر ذخیره میشود:
localStorage.setItem("token", data.token)
زمانی که درخواست دیگری به سرور ارسال میشود، سرور ملزم به احراز هویت و اعطای دسترسی میشود و توکن میتواند از طریق دستور زیر به دست آید:
localStorage.getItem("token")
یادآوری: این اکشنساز یک تابع است که شیئی بازگشت میدهد. در واقع صرفاً فراخوانی آن موجب ایجاد تغییر در حالت نمیشود. یک تابع از Redux به نام dispatch باید با یک شیء به عنوان آرگومان فراخوانی شود تا با Store ارتباط بگیرد. Dispatch میتواند از سوی کامپوننت یا از سوی فایل اکشن فراخوانی شود.
ساختار متدهای fetchUser، signUserUp و autoLogin به دلیل وجود applyMiddleware(thunk) ممکن شده است و در زمان ایجاد Store در آن ادغام شده است. هنوز نشان ندادهایم، اما این دو متد از کامپوننت درون یک dispatch فراخوانی میشوند. با این حال، چنان که اشاره کردیم، dispatch تنها میتواند یک اکشن بگیرد. با استفاده از applyMiddleware(thunk) میتوانیم یک تعریف تابع ارسال کنیم.
نکته: برای کسب اطلاعات بیشتر در مورد applyMiddleware میتوانید به صفحه گیتهاب Thunk (+) مراجعه کنید.
از آنجا که چندین تابع داریم، طبیعی است که بخواهیم برای هر تابع به جز setUser یک export داشته باشیم. این تابع تنها درون چارچوب fetchUser، signUserUp و autoLogin استفاده میشود که در همان فایل قرار دارند. اکنون Store، ردیوسرها و اکشنها را تنظیم کردهایم. کار بعدی که باید انجام دهیم، بررسی روش اتصال کامپوننتهای ریاکت به Store است.
اتصال کامپوننتها به Store با استفاده از connect
در این راهنما، تنها روی سه کامپوننت متمرکز خواهیم بود. یکی App که تولید شده است و دو دیگر LoginComponent و SignUpComponent هستند که ایجاد خواهیم کرد. در ادامه برخی تغییرها روی کامپوننت App ایجاد میکنیم و اتصال را به store برقرار میسازیم.
در ادامه فایلهای LoginComponent و SignUpComponent را در src/components ایجاد میکنیم، اما فعلاً میتوانیم آنها را ایمپورت کنیم. منبع کلیدی دیگر که ایمپورت خواهیم کرد، یک اکسپورت نامدار از کتابخانه React Redux است.
بررسی متد ()connect
در این بخش به بررسی طرز کار متد ()connect میپردازیم. فراخوانی ()connect در عمل یک تابع بازگشت میدهد. ما میخواهیم آن تابع بازگشتی را اجرا کنیم و کامپوننتی که باید به Store وصل شود را به آن ارسال کنیم. این کامپوننت در مثال مورد برسی ما App است. زمانی که این کار انجام شد، کامپوننت App به استور وصل خواهد شد.
در متد ()connect، نخستین آرگومان که ارسال میشود یک تابع است که به طور معمول mapStateToProps نامیده میشود. mapStateToProps یک آرگومان میگیرد که نماینده حالت است. این تابع باید یک شیء با اطلاعات لازم برای حالت بازگشت دهد. در این مورد تنها باید اطلاعات userReducer بازگشت یابد. برای دسترسی به این شیء باید به this.props.userReducer اشاره کنیم.
()connect یک آرگومان دوم نیز میگیرد که به طور معمول mapDispatchToProps نامیده میشود. این آرگومان نیز مشابه mapStateToProps یک آرگومان میگیرد که نماینده dispatch است. هدف آن بازگشت دادن یک شیء است که prop-های مختلف را تعریف میکند و این prop-ها قرار است dispatch را فراخوانی کنند. این props به طور معمول با تابعهای arrow تعریف میشوند. درون این فراخوانیهای disptach، میتوانیم تابعهایی که در actions تعریف کردهایم از قبیل fetchUser و signUserUp را فراخوانی کنیم، اما این موارد نخست باید ایمپورت شوند.
نکته: همه کامپوننتها به اطلاعات Store یا ارسال dispatch به Store نیاز ندارند. اگر فقط به اطلاعات نیاز داشته باشیم، میتوانیم mapStateToProps را در connect(mapStateToProps) ارسال کنیم. در غیر این صورت اگر تنها لازم باشد کامپوننت یک dispatch ارسال کند، آرگومان نخست را null قرار میدهیم و mapDispatchToProps را به عنوان آرگومان دوم به صورت زیر ارسال میکنیم:
connect(null, mapDispatchToProps)
لاگین/ثبت نام کاربر
اکنون که میتوانیم با استفاده از connect(null, mapDispatchToProps) به Store دسترسی داشته باشیم، باید شروع به دریافت اطلاعات کاربر از سرور و ذخیرهسازی آنها در State بکنیم. در این زمینه LoginComponent و SignUpComponent بسیار مشابه هم هستند، به جز این که یک فیلد اضافی داریم و یک dispatch به دو متد متفاوت ارسال میکنیم. کار خود را با LoginComponent آغاز میکنیم.
لاگین
در API مربوط به Ruby on Rails زمانی که یک کاربر لاگین میکند، باید نام کاربری و رمز عبور را ارسال کنیم. در این کامپوننت یک فرم را پیادهسازی میکنیم و باید آن را کنترل کنیم. یک حالت کامپوننت لوکال قرار میدهیم تا رد ورودیها را بگیریم. در این کامپوننت، تنها باید اکشن fetchUser را به همراه connect از React Redux ایمپورت کنیم.
این کامپوننت صرفاً باید یک dispatch را به Store ارسال کند. و هیچ نیازی به دریافت اطلاعات از آن ندارد. بنابراین connect را روی (null, mapDispatchToProps) تنظیم میکنیم. زمانی که فرم تحویل شد، یک dispatch به متد fetchUser ارسال میشود. fetchUser اطلاعت احراز هویت ذخیره شده در حالت لوکال را به عنوان آرگومان میگیرد.
به عنوان یادآوری درون تابع fetchUser یک درخواست واکشی به سرور ارسال میکنیم تا کاربر لاگین شود. اگر تطبیق با اطلاعات احراز هویت صورت بگیرد، یک شیء شامل مشخصههای کاربر و توکن JWT بازگشت مییابد. این توکن از طریق localStorage ذخیره میشود و روشی برای احراز هویت و اعطای دسترسی به حساب میآید.
Dispatch دیگری درون تابع فراخوانی میشود و شیء بازگشتی از اکشنساز setUser را میگیرد. این شیء به Store ارسال میشود و با reducers ارتباط گرفته و اطلاعات حالت کاربر را تبدیل میکند.
ثبت نام
در API مربوط به Ruby on Rails، زمانی که یک کاربر ثبت نام میکند، باید نام کاربری، رمز عبور و سن را ارسال کنیم. بخش SignUpComponent دقیقاً همانند LoginComponent است و یک فیلد ورودی برای سن وجود دارد. ضمناً باید اکسپورت نامدار signUserUp را از actions ایمپورت کنیم:
این کامپوننت نیز مشابه LoginComponent تنها به dispatch اهمیت میدهد. با این حال، زمانی که فرم تحویل میشود، یک dispatch به متد signUserUp ارسال میشود که اطلاعات احراز هویت ذخیره شده در حالت لوکال را به عنوان آرگومان میگیرد. دقیقاً همین فرایند درون تابع signUserUp رخ میدهد. JWT درون localStorage ذخیره میشود و یک dispatch به reducers دارای نوع action و اطلاعت کاربر ارسال میشود.
لاگین خودکار
اگر به کامپوننت App بازگردیم، متوجه میشویم که یک متد چرخه عمری به نام componentDidMount در آن وجود دارد. دلیلش این است که وقتی اپلیکیشن بارگذاری میشود، بتوانیم به طور خودکار کاربر را احراز هویت کرده و اطلاعات دسترسی کاربری که قبلاً لاگین کرده است را به دست آوریم. برای عملیاتی شدن این وضعیت، از JWT که در localStorage ذخیره شده، استفاده میکنیم و آن را به سروری که کاربر مرتبط را در اختیار ما گذاشته ارسال میکنیم.
ابتدا باید اکسپورت نامدار autoLogin را از actions ایمپورت کنیم. mapDispatchToProps در connect تنظیم شده و نام prop هم autoLogin تعیین گشته است. به جای componentDidMount باید prop را فراخوانی کنیم و نیازی به ارسال هیچ آرگومانی وجود ندارد.
درون تابع autoLogin در actions یک درخواست GET به صورت fetch به سرور ارسال میکنیم. در هدر کلید احراز هویت روی Bearer <token> تنظیم میشود. فاصله بین Bearer و token مهم است، زیرا سرور این قالببندی را از ما انتظار دارد.
سرور درخواست را دریافت میکند و بر اساس JWT کاربر را احراز هویت کرده و یا اعطای دسترسی میکند. زمانی که پاسخ بازگشت یافت، فرایند مشابهی برای ذخیرهسازی JWT درون localStorage صورت میگیرد و یک dispatch به reducers شامل نوع action و اطلاعت کاربر ارسال میشود.
خروج کاربر
کارکرد logout در این اپلیکیشن طراحی نشده، اما اکشن آن ایجاد شده است. بسته به این که بخواهیم کدام کامپوننت این کارکرد را داشته باشد، اکسپورت نامدار logUserOut را در آن از actions ایمپورت میکنیم. یک dispatch نیز به Store ارسال میکنیم که آرگومان آن logUserOut است، چون یک شیء محسوب میشود. شیء کاربر در userReducer از مشخصهها پاک میشود و همچنین ()localStorage.clear برای حذف JWT فراخوانی میشود.
بهینهسازی با استفاده از قلابهای React Redux
React Redux یک سری قلابها دارد که جایگزینی برای تابع محسوب میشوند. این قلابها امکان اتصال به Store ریداکس را فراهم میسازند و میتوان به dispatch بدون این که لازم باشد کامپوننتها درون ()connect قرار گیرند، اتصال یافت. ما کامپوننت کلاس App خود را به یک کامپوننت تابعی قالببندی مجدد میکنیم. از آنجا که اکنون یک کامپوننت تابعی در اختیار داریم،، دیگر به متد چرخه عمری دسترسی نداریم. بنابراین باید اکسپورت نامدار useEffect را از ریاکت و موارد useSelector و useDispatch را این بار به جای connect از React Redux ایمپورت کنیم.
اینک به جای componentDidMount اقدام به پیادهسازی تابع useEffect کردهایم. این تابع دو آرگومان میگیرد که یک تابع و یک آرایه خالی است. تابع هر بار که کامپوننت رندر شود، اجرا میشود. آرایه شامل فهرستی از وابستگیها است. اگر مقدار یکی از وابستگیها تغییر یابد، useEffect دوباره اجرا خواهد شد. آن را به صورت یک آرایه خالی رها میکنیم تا از اجرای حلقه بینهایت رندرکنندهها جلوگیری کنیم.
پیشنهاد میکنیم برای کسب اطلاعات بیشتر در مورد قلابهای React به مقاله زیر مراجعه کنید:
useSelector معادل mapStateToProps است. useSelector یک تابع میگیرد که در آن State آرگومان است و صراحتاً یک مقدار خاص در حالت بازگشت میدهیم. useDispatch معادل mapDispatchToProps است. فراخوانی آن موجب بازگشت dispatch میشود که autoLogin را میگیرد.
سخن پایانی
به این ترتیب به پایان این مقاله میرسیم. در این راهنما یک اپلیکیشن React Redux ایجاد کردیم که JWT را برای احراز هویت و اعطای دسترسی کاربر مدیریت میکند. این مقاله شامل مبانی کاملاً مقدماتی بوده است و به هیچ وجه، مواد استثنایی مانند حالتی که هیچ کاربری در پاسخ بازگشت نیابد، بررسی نشده است. شما میتوانید این شالوده مقدماتی را توسعه دهید و قابلیتهای لاگ اوت و حالتهای استثنایی را نیز اضافه کنید. کد کامل این اپلیکیشن در این ریپوی گیتهاب (+) ارائه شده است.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش JavaScript ES6 (جاوا اسکریپت)
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- طراحی احراز هویت مقدماتی با React — به زبان ساده
- آموزش ری اکت (React) — مجموعه مقالات مجله فرادرس
==