تنظیم ریداکس برای REST API — از صفر تا صد
در این مقاله به توضیح در مورد مدیریت حالت با ریداکس (Redux) میپردازیم و توضیحاتی تفصیلی در مورد معماری این راهحل ارائه میکنیم. در این ریپازیتوری گیتهاب (+) نیز تنظیمات کامل ریداکس برای استفاده در اپلیکیشنهای واقعی ارائه شده است. با ما تا انتهای این راهنما همراه باشید تا با شیوه تنظیم ریداکس برای REST API آشنا شوید.
بیان مسئله
ریداکس یک کتابخانه فوقالعاده و در عین سادگی بسیار قدرتمند است. همچنین انعطافپذیری زیادی دارد. این حالت خوبی است زیرا به ما امکان میدهد که آن را به شیوهای که میخواهیم تنظیم کنیم و آن را برای رفع نیازهای اپلیکیشن خود آماده سازیم. اما این انعطافپذیری موجب شده که ادغام آن در اپلیکیشنهای بزرگ پیچیده شود، زیرا روشهای مختلفی برای انجام این کار وجود دارد.
اجماع کلی در مورد روش تنظیم ریداکس برای یک فراخوانی API به صورت زیر است:
- اعلان ثابتها (REQUEST_ACTION, SUCCESS_ACTION, FAIL_ACTION).
- ایجاد یک action creator که مسئول dispatch اکشن درخواست است.
- استفاده از یک سرویس API برای فراخوانی نقطه انتهایی (endpoint) (با استفاده از یک کتابخانه شخص ثالث مانند redux-thunk, saga, observables).
- دریافت یک payload که در صورت موفق بودن SUCCESS_ACTION را ارسال میکند و در غیر این صورت FAIL_ACTION دیسپچ میشود.
- مدیریت Payload در یک ردیوسر (Reducer).
- اتصال ریداکس به ریاکت و کامپوننت (با استفاده از mapStateToProps و mapDispatchToProps).
این مراحل باید برای تکتک فراخوانیهای API تکرار شود. بدین ترتیب به طور مداوم بر حجم کدهای قالبی و بدون قابلت نگهداری اضافه میشود.
راهحل
در این بخش به برسی شیوه حل مشکلی که در بخش قبل اشاره کردیم میپردازیم. به این منظور از یک مثال برای خواندن یک کاربر منفرد استفاده میکنیم.
Action Creator
اکنون نخستین کاری که معمولاً برای خواندن یک کاربر که باید انجام دهیم، اعلان کردن برخی ثابتها به صورت زیر است:
1export const REQUEST_READ_USER = 'REQUEST_READ_USER';
2export const SUCCESS_READ_USER = 'SUCCESS_READ_USER';
3export const FAIL_READ_USER = 'FAIL_READ_USER';
نخستین بهبودی که قرار است انجام دهیم این است که به طور کامل از شر این موارد رها شویم. در واقع هیچ نیازی به اعلان کردن این ثابتها وجود ندارد. به سهولت میتوان آنها را به صورت دینامیک و با چند تست Unit محاسبه کرد و مطمئن شد که همه چیز مطابق انتظار عمل میکند.
مورد بعدی که به صورت معمول باید انجام دهیم، تعریف کردن یک Action Creator به صورت زیر است:
این Action Creator از redux-thunk استفاده میکند تا ابتدا یک اکشن request ارسال کد و سپس فراخوانی API را انجام میدهد و یک اکشن success یا fail بسته به پاسخ اجرا میکند. اما این کد دو مشکل دارد:
- این Action Creator بسیار خاص موجودیت user است. کارکرد خواندن موجودیتها در اغلب موارد همانند تعریف کردن یک اکشن کریتور برای صرفاً کاربر، قابلیت استفاده مجدد چندانی ندارند. اگر بخواهیم یک گره از موجودیتها را بخوانیم، کد باید مشابه باشد.
- با اجرای هر دو اکشن درخواست و همچنین اجرای فراخوانی API در واقع دغدغهها را با هم مخلوط کردهایم. اکشن کریتور باید صرفاً مسئول ارسال یک اکشن باشد.
در ادامه با روش رفع این مشکل آشنا میشویم. Action creator به روشی بازنویسی میشود که تنها مسئولیت آن اجرای یک اکشن REQUEST باشد. همزمان باید بتواند برای هر موجودیت در سیستم استفاده شود. ظاهر آن چنین است:
همچنان که از روی نام این action creator میبینید، برخلاف (readUser) در بخش قبلی، حالت ژنریک دارد. از آن میتوان برای هر موجودیتی در سیستم استفاده کرد. این اکشن کریتور دو آرگومان میگیرد که یکی entityName و دیگری ID موجودیتی است که میخواهیم بخوانیم. اکشن کریتور یک اکشن با فیلدهای زیر اجرا میکند:
- نوع که به صورت REQUEST_READ_${entityName} است. تنها دلیل این که entityName با حروف بزرگ نوشتیم این است که یکنواختی حفظ شود.
- urlParams که پارامترهایی را که برای محاسبه نقطه انتهایی API مورد فراخوانی لازم است نگهداری میکند.
- متادیتا که برای ردیوسرها و میانافزارها استفاده میشود.
بنابراین تا به اینجا یک اکشن کریتور ایجاد کردیم که با هر موجودیتی در سیستم کار میکند، نوع به صورت دینامیک محاسبه میشود و از این رو دیگر به هیچ ثابتی نیاز نداریم. همچنین تنها هدف آن بازگشت یک اکشن است.
میانافزار API
در ادامه باید یک میانافزار API ایجاد کنیم که همه منطقی برای ارسال فراخوانی API و سپس نمایش موفقیت یا شکست اکشن بسته به پاسخ لازم است را در آن قرار میدهیم. شکل نهایی آن به صورت زیر است:
در کد فوق به هر اکشنی که نوع آن با REQUEST_READ آغاز شود گوش میدهیم، سپس نقطه انتهایی API را بسته به این که پاسخ یک اکشن SUCCESS یا FAIL اجرا کند، فرامیخوانیم.
دو نکته وجود دارد که باید توجه داشته باشیم:
- urlParams که در اکشن درخواست در بخش قبل تعریف شده است برای محاسبه نقطه انتهایی API مورد استفاده قرار میگیرد.
- نوع اکشن مجدداً به صورت دینامیک محاسبه میشود.
بنابراین میانافزار API برای هر موجودیتی در سیستم کار میکند، ثابتهای نوع اکشن را حذف میکند و از کتابخانههای خارجی برای فراهم ساختن امکان فراخوانی API بهره میگیرد.
میانافزار نرمالسازی
در ادامه باید پاسخی که از API میگیریم را نرمالسازی کنیم. پیش از آن که با شیوه انجام این کار آشنا شویم، باید ببینیم منظور از نرمالسازی چیست؟ بسیاری از اپلیکیشنها با دادههایی سر و کار دارند که تودرتو هستند یا ماهیتی رابطهای دارند. نمونهای از آن به صورت زیر است:
این یک موجودیت post است که یک نویسنده و برخی نظرات دارند. این نظرات هم هر کدام یک نویسنده دارند.
با استفاده از نرمالسازی میتوان موجودیتهای تودرتو را با ID آنها بازگشت داد و در دیکشنریها گردآوری کرد. در ادامه یک نسخه نرمالسازی شده از post را میبینید:
همه دادههای تودرتو اینک به یک موجودیت از طریق کلید id اشاره میکنند.
مزیت عمده نرمالسازی دادهها در این است که بهروزرسانی بسیار آسان میشود. اگر برای مثال نام کاربر با id شماره 1 را از «Jeff» به «Peter» عوض کنیم، در رویکرد نخست باید این کار در دو جا انجام یابد. اما زمانی که دادهها نرمالسازی شوند، این بهروزرسانی را تنها در یک محل انجام میدهیم. میتوان دید که رویکرد نخست در اپلیکیشنهای بزرگ غیر قابل نگهداری خواهد بود. در نسخه غیر نرمال ممکن است در نهایت مجبور شویم یک حالت را در 20 محل مختلف بهروزرسانی کنیم.
میانافزار برای نرمالسازی payload به صورت زیر است:
در کد فوق به هر اکشنی که نوع آن با SUCCES_READ آغاز شود گوش میکنیم و سپس دادههای payload را بهروزرسانی میکنیم.
نکته: ما باری همه نیازهای نرمالسازی از کتابخانه normalizer (+) بهره میگیریم.
ردیوسرها و ساختمان Store
شیوه سازماندهی Store به صورت زیر است:
همه دادهها از API میآیند و زیر کلید entities قرار میگیرند. در اینجا یک کلید برای هر موجودیت در سیستم نگهداری میکنیم.
زیر هر کلید موجودیت موارد زیر وجود دارند:
- یک کلید byId که payload ورودی از API یعنی ساختمان نرمالسازی شده را نگه میدارد.
- یک کلید readIds که وضعیت فراخوانیهای API را نگهداری میکند.
برای نیل به این مقصود به یک reducer creator نیاز داریم. منظور از reducer creator یک تابع است که یک آرگومان میگیرد و یک ردیوسر مانند زیر بازگشت میدهد:
این ردیوسر کریتور به نام getReducers آرگومانی به نام entityName میگیرد و دو ردیوسر بازگشت میدهد.
نکته مهم
ما باید این سازنده ردیوسر را برای هر موجودیت در سیستم فرابخوانیم. بنابراین هر موجودیت یک ردیوسر byId و readIds خواهد داشت. این نکته مهمی است که برای درک طرز کار ردیوسرها باید بدانیم. هر اکشن ریداکس از همه این ردیوسرها رد میشود. معنی این حرف آن است که در هر ردیوسر byId و readIds باید تنها انواع اکشن و یا payload اکشن را که با موجودیت مرتبط با ردیوسر در ارتباط است در نظر بگیریم. برای درک بهتر این موضوع بهتر است ریپوی گیتهاب این پروژه را که در انتهای مقاله آماده است ملاحظه کنید.
byId Reducer
ردیوسر byId به هر نوع اکشنی که با SUCCESS آغاز شود گوش میدهد و اگر payload به صورت دادههای این نوع موجودیت باشد، در حالت ادغام خواهد شد. اگر برای نمونه payload ما شبیه دادههای موجود در payload نرمالسازی شده فوق باشد و این ردیوسر byId از موجودیت comment باشد، بخش نظر payloaf در حالت ادغام خواهد شد.
readIds Reducer
در این کد روی هر اکشنی که نوع آن با READ آغاز شود گوش میدهیم و با ردیوسر readIds مرتبط است. اگر برای نمونه اکشن به صورت REQUEST_READ_USER باشد، و این ردیوسر به صورت موجودیت user باشد، اقدام میکنیم تا حالت را بهروزرسانی کنیم.
اتصال کامپوننت مرتبه بالا
آخرین کاری که باید انجام دهیم، کاهش کد قالبی در زمان اتصال یافتن به کد ریداکس در React است. به طور معمول هر کامپوننت React که بخواهیم به ریداکس وصل کنیم باید از کتابخانه اتصال به نام react-redux استفاده کنیم و سپس یک mapStateToProps و یک تابع mapDispatchToProps به این منظور تعریف کنیم. برای رسیدن به این قابلیت استفاده مجدد باید کد را به «کامپوننت مرتبه بالا» (HOC) انتقال دهیم که مانند زیر است:
1/* Dependencies */
2import { connect } from 'react-redux';
3
4/* Actions */
5import { readEntity } from '../../../../redux/actions';
6
7/* Selectors */
8import { selectEntity, selectReadEntityStatus } from '../../../../redux/selectors';
9
10const Container = ({ children, ...rest }) => children(rest);
11
12const mapStateToProps = (state, ownProps) => ({
13 entity: selectEntity(state, ownProps.entityName, ownProps.id),
14 status: selectReadEntityStatus(state, ownProps.entityName, ownProps.id),
15});
16
17const mapDispatchToProps = (dispatch, ownProps) => ({
18 read(options) {
19 dispatch(
20 readEntity(ownProps.entityName, ownProps.id, options),
21 );
22 },
23});
24
25export default connect(mapStateToProps, mapDispatchToProps)(Container);
این HOC دو آرگومان میگیرد که یکی entityName و دیگری یک id است. سپس 3 آرگومان به فرزندان ارسال میکند که به شرح زیر هستند:
- یک تابع خواندن که اکشن کریتور readEntity را فراخوانی میکند که در بخشهای قبلی دیدیم.
- entity که دادههای انتخاب شده از ردیوسر byId مرتبط است.
- Status که نشانگر وضعیت فراخوانی API انتخابی از ردیوسر readIds مرتبط است.
این HOC میتواند به صورت زیر مورد استفاده قرار گیرد:
1class User extends React.Component {
2 componentDidMount() {
3 const { read } = this.props;
4 read();
5 }
6
7 render() {
8 const { entity: user, status } = this.props;
9
10 if (status && !status.isFetching) {
11 return (
12 <div>
13 {user.name}
14 </div>
15 );
16 }
17 return <div>Loading user...</div>;
18 }
19}
20
21const ReadUser = () => (
22 <ReadEntityContainer entityName='user' id={1}>
23 { ({ read, status, entity }) => <User read={read} status={status} entity={entity} /> }
24 </ReadEntityContainer>
25);
در خط 22 کد فوق مشغول پیادهسازی آن HOC هستیم که قبلاً دیدیم. دو آرگومان مورد نیاز را ارسال میکنیم. یکی entityName است که در این مورد به صورت یک user خواهد بود که میخواهیم یک کاربر را بخواند. id نیز در این مثال مقدار 1 دارد که باید کاربر با شناسه 1 را بخواند.
این HOC سه آرگومان به صورت read, statu و entity در اختیار ما قرار میدهد. سپس در کامپوننت User میتوانیم read را پس از نصب شدن کامپوننت فراخوانی کنیم. همچنین میتوانیم از status استفاده کنیم و ببینیم آیا فراخوانی API پایان یافته یا نه. اگر چنین باشد در ادامه نام کاربر را نمایش میدهیم و در غیر این صورت میتوانیم متن …Loading را نشان دهیم.
بدین ترتیب میبینیم که میزان کد قالبی در کامپوننتهای React تا چه حد کاهش یافته است. تنها نکتهای که مانده است این است که باید ریاکت را به ریداکس وصل کنیم و فراخوانی API خواندن را برای استفاده از HOC مورد استفاده قرار دهیم.
جمعبندی
در این بخش مواردی که در این مقاله مطرح شدند را جمعبندی میکنیم.
- در این راهنما دیدیم که امکان حذف ثابتهای نوع اکشن وجود دارد.
- در ادامه یک اکشن کریتور ژنریک به نام ایجاد کردیم که میتواند از سوی هر موجودیت در سیستم مورد استفاده قرار گیرد و تنها مسئولیت آن اجرای اکشن درخواست است.
- یک میانافزار API تعریف کردیم که نقطه انتهایی اجرای اکشن success یا fail را بسته به پاسخ فرامیخواند.
- یک میانافزار normalize تعریف کردیم که مسئول نرمالسازی payload است.
- یک ردیوسر کریتور getReducers تعریف کردیم که دو ردیوسر فرعی به نامهای byId و readIds بازگشت میدهد.
- منطق را به جابجا کردیم تا کامپوننتهای ریاکت را با در یک کامپوننت مرتبه بالا به ریداکس وصل کنیم.
سخن پایانی
لازم به اشاره است که همه کدهای فوق کاملاً ژنریک هستند. این بدان معنی است که میتوانیم هر موجودیت را در سیستم بدون نوشتن هیچ کد اضافی بخوانیم. اما برای رسیدن به پوشش 90 درصد یا بالاتر در مورد نیازهای فراخوانی API باید موارد زیر را نیز مدیریت کنیم:
- ایجاد (Create)
- خواندن (Read)
- بهروزرسانی (Update)
- حذف (Delete)
اتصال یا جداسازی یک موجودیت از دیگری در یک رابطه «چند به چند» (many to many).
همچنین باید امکان کار با موجودیتهای منفرد یا چندگانه را فراهم سازیم، یعنی باید بتوانیم یک کاربر منفرد یا چند کاربر را بخوانیم یا بتوانیم یک نوشته یا چند نوشته را همزمان حذف کنیم.
واقعیت این است که به این منظور نیاز به نوشتن کدهای زیادی نداریم. کدبیس کامل را میتوانید در این ریپوی گیتهاب (+) ملاحظه کنید.
اگر این نوشته برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش کتابخانه Redux (ریداکس)
- ریداکس (Redux) — مبانی مقدماتی
- راهنمای مقدماتی ریداکس (Redux) — به زبان ساده
==