آلات الحالات المحدودة مع @ wordpress / data و TypeScript
نشرت: 2021-09-16لنفترض أننا نريد تصميم شاشة تسجيل دخول في تطبيقنا. عندما يراه المستخدم لأول مرة ، يوجد نموذج فارغ. يملأ المستخدم الحقول ، وبمجرد تعيينها جميعًا ، يضغطون على زر تسجيل الدخول للتحقق من صحة بيانات الاعتماد ، وكذلك تسجيل الدخول. إذا نجحت عملية التحقق ، ينتقلون إلى الشاشة التالية. ولكن إذا لم يحدث ذلك ، فسيتم تقديم رسالة خطأ لهم ويطلب منهم المحاولة مرة أخرى.
إذا كنت ستنفذ حالة مثل هذه الشاشة وقرأت منشوراتي حول كيفية تحديد حالة التطبيق باستخدام TypeScript و @wordpress/data
، فأنا أراهن أنك ستفعل شيئًا كهذا:
type State = { readonly username: string; readonly password: string; readonly isLoggingIn: boolean; readonly errorMessage: string; };
التي لديها كل الحقول اللازمة لإدارة الدولة ولكن ... هل هذا أفضل ما يمكننا القيام به؟
آلات الحالة المحدودة
آلة الحالة المحدودة (FSM) هي ، بشكل عام ، نموذج رياضي يسمح لنا بتحديد مجموعة محدودة من الحالات والتحولات المحتملة بينها. (مزيد من المعلومات على ويكيبيديا). يعجبني هذا التجريد لأنه يناسب احتياجاتنا تمامًا عندما يتعلق الأمر بنمذجة حالة تطبيقاتنا. على سبيل المثال ، قد يبدو مخطط الحالة لشاشة تسجيل الدخول كما يلي:

والتي ، كما ترى ، تلتقط بطريقة واضحة وموجزة السلوك الذي نريد تنفيذه.
تحديد الدول باستخدام @wordpress/data
في سلسلتنا التمهيدية لـ React ، رأينا كيفية استخدام حزمة @wordpress/data
لإنشاء حالة تطبيقنا وإدارتها. لقد فعلنا ذلك من خلال تحديد المكونات الرئيسية لمتجر البيانات لدينا:
- مجموعة من المحددات للاستعلام عن الحالة
- مجموعة من الإجراءات لبدء طلب التحديث
- وظيفة مخفض ، نظرًا للحالة الحالية وإجراء التحديث ، لتحديث الحالة
في جوهرها ، تتصرف المخازن في @wordpress/data
بالفعل مثل آلات الحالة المحدودة ، نظرًا لأن وظيفة المخفض "تنتقل" من حالة إلى أخرى باستخدام إجراء:
( state: State, action: Action ) => State
كيفية تعريف آلة الحالة المحدودة باستخدام TypeScript في @wordpress/data
يبدو أن @wordpress/data
قريبة جدًا من نموذج FSM الذي قدمناه أعلاه: نحن نفتقد فقط الحالات المحددة التي يمكن أن نكون فيها والانتقالات بينها ... لذلك دعونا نلقي نظرة فاحصة على كيف يمكننا إصلاح هذه المشكلات ، خطوة بخطوة.
الدول الصريحة
لقد بدأنا هذا المنشور من خلال إظهار إمكانية تنفيذ حالة تطبيقنا. كان اقتراحنا الأول عبارة عن مجموعة من السمات التي أنجزت المهمة ، لكنها لا تبدو وكأنها آلة دولة محدودة على الإطلاق. لذلك لنبدأ بالتأكد من تحديد حالاتنا بشكل صريح في نموذجنا:
type State = | Form | LoggingIn | Success | Error; type Form = { readonly status: 'form'; readonly username: string; readonly password: string; }; type LoggingIn = { readonly status: 'logging-in'; readonly username: string; readonly password: string; }; type Success = { readonly status: 'success'; // ... }; type Error = { readonly status: 'error'; readonly message: string; };
واضح جدا ، أليس كذلك؟ يوجد نوع واحد لكل ولاية ويتم تعريف الحالة العامة للتطبيق على أنها اتحاد أنواع مميز. سميت بذلك لأن (أ) الدولة هي "اتحاد أنواع مختلفة" (أي أنه إما Form
أو LoggingIn
أو أي شيء آخر) و (ب) لدينا سمة (في هذه الحالة ، status
) التي تتيح لنا التمييز بين الحالة المحددة التي نمتلكها في كل لحظة.
أعتقد أن هذا الحل أفضل بكثير من ذلك الذي كان لدينا في البداية. في حلنا الأصلي ، تمكنا من تحديد "الحالات غير الصالحة" ، لأنه كان من الممكن تسجيل الدخول (فقط isLoggingIn
على القيمة true
) وفي نفس الوقت ، يكون في حالة خطأ (فقط errorMessage
إلى قيمة أخرى غير القيمة الفارغة سلسلة). لكن من الواضح أن هذا لا معنى له! أي واحد هو؟ هل نقوم بتسجيل الدخول أم من المفترض أن نظهر خطأ؟
من ناحية أخرى ، يكون الحل الجديد أكثر دقة عندما يتعلق الأمر بتمثيل الدولة: فهو يجعل الحالات "غير الصالحة" "مستحيلة". إذا كنا في LoggingIn
، فلا توجد طريقة لإعداد رسالة خطأ (لا توجد سمة لها!). إذا أظهرنا Error
، فلا يمكنك تسجيل الدخول. أفضل طريقة ، ألا تعتقد ذلك؟
أجراءات
في @wordpress/data
، تعمل الإجراءات على تحديث المتجر في دولتنا. نظرًا لأننا نصمم حالتنا على أنها ولايات ميكرونيزيا الموحدة ، فإن أفعالنا ستكون الأسهم من مخططنا الأصلي. كل ما عليك فعله هو تصميمها على أنها اتحاد تمييزي من الأنواع (وفقًا للاتفاقية ، عادةً ما يتم تسمية المُميِّز type
) وأنت على ما يرام:
type SetCredentials = { readonly type: 'SET_CREDENTIALS'; readonly username: string; readonly password: string; }; type Login = { readonly type: 'LOGIN'; }; type ShowApp = { readonly type: 'SHOW_APP'; // ... }; type ShowError = { readonly type: 'SHOW_ERROR'; readonly message: string; }; type BackToLoginForm = { readonly type: 'BACK_TO_LOGIN_FORM'; };
من المفترض أن تمثل أفعالنا الأسهم الموجودة في مخططنا الأصلي ، أليس كذلك؟ حسنًا ، لا أعرف شيئًا عنك ، لكنني أعتقد أن أنواع الإجراءات هذه لا تشبه الأسهم على الإطلاق! للسهام اتجاه (ينتقلون من حالة إلى أخرى) ؛ الإجراءات لا.

نحتاج إلى طريقة للتوضيح في التعليمات البرمجية الخاصة بنا أن بعض الإجراءات تكون مفيدة فقط للانتقال من حالة إلى أخرى. وأفضل حل وجدته حتى الآن هو استخدام المخفض نفسه:
function reducer( state: State, action: Action ): State { switch ( state.status ) { case 'form': switch ( action.type ) { case 'SET_CREDENTIALS': return { status: 'ready', username: action.username, password: action.password, }; case 'LOGIN': return { ...state, status: 'logging-in' }; } case ...: ... } return state; }
إذا بدأ مخفضنا بتصفية status
state
، فإننا نعرف "مصدر" سهمنا. بعد ذلك ، ننظر إلى الإجراء الحالي ، وإذا كان "سهمًا إلى الخارج" ، فإننا ننشئ حالة "الهدف" الجديدة.
يمكنك أن ترى بوضوح كيف يعمل هذا في المقتطف أعلاه. إذا كانت status
الحالية هي form
، فإن الإجراء SET_CREDENTIALS
يأخذنا إلى نفس الحالة التي كنا فيها (تحديث بيانات الاعتماد ، duh) ويغير إجراء LOGIN
الحالة إلى logging-in
. يتم تجاهل أي إجراء آخر في هذه الحالة ، وبالتالي ، فإن الحالة لم تتغير.
كل هذا جيد وممتاز ولكن ... لا أعرف ، لا أحب الكود كثيرًا ، أليس كذلك؟ أريد التأكد من أن الكود الخاص بي واضح وموجز وواضح بذاته وآمن من النوع. هل يمكننا فعل ذلك؟
مخفض مكتوب بقوة
لإصلاح الفوضى التي وصلنا إليها للتو ، نحتاج فقط إلى إعادة تشكيل reducer
بحيث يكون لكل حالة ممكنة في ولايات ميكرونيزيا الموحدة مخفضها الخاص. إذا قمنا بذلك بشكل صحيح ، فسيتم كتابتها بقوة وسيكون من الواضح ما هو مسموح به وما هو غير مسموح به.
لنبدأ بإنشاء نوع لكل حالة في تطبيقنا. سيجمع كل نوع الإجراءات التي يمكن أن تنقلنا من حالة معينة إلى مكان آخر. على سبيل المثال ، تحتوي حالة Form
لدينا على سهمين خارجيين ( SetCredentials
and Login
) ، لذلك دعونا ننشئ نوع FormAction
مع اتحاد الإجراءين. كرر العملية لكل ولاية وستحصل على هذا:
type FormAction = SetCredentials | Login; type LoggingInAction = ShowError | ShowApp; type SuccessAction = ...; type ErrorAction = BackToLoginForm; type Action = | FormAction | LoggingInAction | SuccessAction | ErrorAction;
بعد ذلك ، حدد جميع مخفضات السرعة التي نحتاجها. مرة أخرى ، واحد لكل ولاية:
function reduceForm( state: Form, action: FormAction ): Form | LoggingIn { switch ( action.type ) { switch ( action.type ) { case 'SET_CREDENTIALS': return { status: 'ready', username: action.username, password: action.password, }; case 'LOGIN': return { ...state, status: 'logging-in' }; } } reduceLoggingIn: ( state: LoggingIn, action: LoggingInAction ) => Success | Error reduceError: ( state: Error, action: ErrorAction ) => Form reduceSuccess: ( state: Success, action: SuccessAction ) => ...
بفضل TypeScript والأنواع التي حددناها للتو ، يمكننا الآن أن نكون دقيقين للغاية بشأن حالة المصدر ومجموعة الإجراءات التي يتوقعها كل مخفض ، بالإضافة إلى الحالة (الحالات) المستهدفة التي يمكننا الحصول عليها نتيجة لذلك.
أخيرًا ، نحتاج ببساطة إلى ربط كل ذلك بوظيفة reducer
فريدة:
function reducer( state: State, action: Action ): State { switch ( state.status ) { case 'form': return reduceForm( state, action as FormActions ) ?? state; case 'logging-in': return reduceLoggingIn( state, action as LoggingInActions ) ?? state; case 'error': return reduceError( state, action as ErrorActions ) ?? state; case 'success': return reduceSuccess( state, action as SuccessActions ) ?? state; } }
الكود الناتج ، في رأيي ، أوضح وأسهل في الحفاظ عليه. علاوة على ذلك ، أصبح بإمكان TypeScript الآن التحقق مما نقوم به والتقاط أي سيناريوهات مفقودة. مدهش!
خاتمة
في هذا المنشور ، رأينا ما هي آلة الحالة المحدودة وكيف يمكننا تنفيذ واحدة في @wordpress/data
store باستخدام TypeScript. حلنا النهائي بسيط للغاية ، ومع ذلك ، فإنه يوفر الكثير من المزايا: فهو يلتقط حالة تطبيقنا والغرض منه بشكل أفضل ، ويعزز قوة TypeScript.
أتمنى أن تكون قد أحببت هذا المنشور! إذا قمت بذلك ، يرجى مشاركتها مع أصدقائك وزملائك. واسمحوا لي أن أعرف كيف تعالج هذه المشاكل في قسم التعليقات أدناه.
الصورة المميزة باتريك هندري على Unsplash.