21 KiB
بناء تطبيق مصرفي الجزء الرابع: مفاهيم إدارة الحالة
اختبار ما قبل المحاضرة
المقدمة
مع نمو تطبيقات الويب، يصبح من الصعب تتبع جميع تدفقات البيانات. أي كود يحصل على البيانات، أي صفحة تستهلكها، أين ومتى تحتاج إلى التحديث... من السهل أن ينتهي بك الأمر بكود فوضوي يصعب صيانته. هذا صحيح بشكل خاص عندما تحتاج إلى مشاركة البيانات بين صفحات مختلفة في التطبيق، مثل بيانات المستخدم. مفهوم إدارة الحالة كان موجودًا دائمًا في جميع أنواع البرامج، ولكن مع استمرار نمو تطبيقات الويب في التعقيد أصبح الآن نقطة رئيسية يجب التفكير فيها أثناء التطوير.
في هذا الجزء الأخير، سنراجع التطبيق الذي قمنا ببنائه لإعادة التفكير في كيفية إدارة الحالة، مما يسمح بدعم تحديث المتصفح في أي وقت، واستمرار البيانات عبر جلسات المستخدم.
المتطلبات الأساسية
يجب أن تكون قد أكملت جزء جلب البيانات من تطبيق الويب لهذه الدرس. تحتاج أيضًا إلى تثبيت Node.js وتشغيل واجهة برمجة التطبيقات الخاصة بالخادم محليًا حتى تتمكن من إدارة بيانات الحساب.
يمكنك اختبار تشغيل الخادم بشكل صحيح عن طريق تنفيذ هذا الأمر في الطرفية:
curl http://localhost:5000/api
# -> should return "Bank API v1.0.0" as a result
إعادة التفكير في إدارة الحالة
في الدرس السابق، قدمنا مفهومًا أساسيًا للحالة في تطبيقنا باستخدام المتغير العام account
الذي يحتوي على بيانات البنك للمستخدم الذي قام بتسجيل الدخول حاليًا. ومع ذلك، فإن التنفيذ الحالي لدينا يحتوي على بعض العيوب. حاول تحديث الصفحة عندما تكون في لوحة التحكم. ماذا يحدث؟
هناك 3 مشاكل في الكود الحالي:
- الحالة غير مستمرة، حيث يؤدي تحديث المتصفح إلى إعادتك إلى صفحة تسجيل الدخول.
- هناك وظائف متعددة تعدل الحالة. مع نمو التطبيق، يمكن أن يصبح من الصعب تتبع التغييرات ومن السهل نسيان تحديث واحدة.
- الحالة غير منظفة، لذا عندما تنقر على تسجيل الخروج تبقى بيانات الحساب موجودة حتى وأنت في صفحة تسجيل الدخول.
يمكننا تحديث الكود لمعالجة هذه المشاكل واحدة تلو الأخرى، ولكن ذلك سيؤدي إلى تكرار الكود وجعل التطبيق أكثر تعقيدًا وصعوبة في الصيانة. أو يمكننا التوقف لبضع دقائق وإعادة التفكير في استراتيجيتنا.
ما هي المشاكل التي نحاول حلها هنا؟
إدارة الحالة تتعلق بإيجاد نهج جيد لحل هاتين المشكلتين بالتحديد:
- كيف يمكن الحفاظ على تدفقات البيانات في التطبيق مفهومة؟
- كيف يمكن الحفاظ على بيانات الحالة متزامنة دائمًا مع واجهة المستخدم (والعكس صحيح)؟
بمجرد أن تعتني بهذه النقاط، قد يتم حل أي مشاكل أخرى لديك بالفعل أو تصبح أسهل في الحل. هناك العديد من النهج الممكنة لحل هذه المشاكل، ولكننا سنختار حلاً شائعًا يتكون من مركزية البيانات وطرق تغييرها. ستتدفق البيانات على النحو التالي:
لن نغطي هنا الجزء الذي يتم فيه تحديث العرض تلقائيًا بواسطة البيانات، حيث يرتبط بمفاهيم أكثر تقدمًا في البرمجة التفاعلية. إنه موضوع متابعة جيد إذا كنت ترغب في التعمق.
✅ هناك العديد من المكتبات التي تقدم نهجًا مختلفًا لإدارة الحالة، مثل Redux الذي يعتبر خيارًا شائعًا. ألقِ نظرة على المفاهيم والأنماط المستخدمة حيث إنها غالبًا ما تكون طريقة جيدة لتعلم المشاكل المحتملة التي قد تواجهها في تطبيقات الويب الكبيرة وكيف يمكن حلها.
المهمة
سنبدأ ببعض إعادة الهيكلة. استبدل تعريف account
بـ:
let account = null;
بـ:
let state = {
account: null
};
الفكرة هي مركزية جميع بيانات التطبيق في كائن حالة واحد. لدينا فقط account
في الحالة الآن لذا لا يتغير الكثير، ولكنه يفتح المجال للتطورات.
علينا أيضًا تحديث الوظائف التي تستخدمه. في وظائف register()
وlogin()
، استبدل account = ...
بـ state.account = ...
;
في بداية وظيفة updateDashboard()
، أضف هذا السطر:
const account = state.account;
لم تجلب هذه إعادة الهيكلة تحسينات كبيرة بحد ذاتها، ولكن الفكرة كانت وضع الأساس للتغييرات القادمة.
تتبع تغييرات البيانات
الآن بعد أن وضعنا كائن state
لتخزين بياناتنا، الخطوة التالية هي مركزية التحديثات. الهدف هو جعل تتبع أي تغييرات ومتى تحدث أسهل.
لتجنب إجراء تغييرات على كائن state
، من الجيد أيضًا اعتباره غير قابل للتغيير، مما يعني أنه لا يمكن تعديله على الإطلاق. يعني ذلك أنه يجب عليك إنشاء كائن حالة جديد إذا كنت تريد تغيير أي شيء فيه. من خلال القيام بذلك، تبني حماية ضد التأثيرات الجانبية غير المرغوب فيها، وتفتح المجال لإمكانيات ميزات جديدة في التطبيق مثل تنفيذ التراجع/الإعادة، مع جعل عملية تصحيح الأخطاء أسهل. على سبيل المثال، يمكنك تسجيل كل تغيير يتم إجراؤه على الحالة والاحتفاظ بسجل للتغييرات لفهم مصدر الخطأ.
في JavaScript، يمكنك استخدام Object.freeze()
لإنشاء نسخة غير قابلة للتغيير من الكائن. إذا حاولت إجراء تغييرات على كائن غير قابل للتغيير، سيتم رفع استثناء.
✅ هل تعرف الفرق بين كائن غير قابل للتغيير سطحيًا وكائن غير قابل للتغيير بعمق؟ يمكنك القراءة عنه هنا.
المهمة
لنقم بإنشاء وظيفة جديدة updateState()
:
function updateState(property, newData) {
state = Object.freeze({
...state,
[property]: newData
});
}
في هذه الوظيفة، نقوم بإنشاء كائن حالة جديد ونسخ البيانات من الحالة السابقة باستخدام عامل الانتشار (...
). ثم نقوم بتجاوز خاصية معينة من كائن الحالة بالبيانات الجديدة باستخدام صيغة الأقواس [property]
للتعيين. أخيرًا، نقوم بتجميد الكائن لمنع التعديلات باستخدام Object.freeze()
. لدينا فقط خاصية account
مخزنة في الحالة الآن، ولكن مع هذا النهج يمكنك إضافة أي عدد من الخصائص التي تحتاجها في الحالة.
سنقوم أيضًا بتحديث تهيئة state
للتأكد من أن الحالة الأولية مجمدة أيضًا:
let state = Object.freeze({
account: null
});
بعد ذلك، قم بتحديث وظيفة register
عن طريق استبدال التعيين state.account = result;
بـ:
updateState('account', result);
قم بنفس الشيء مع وظيفة login
، استبدل state.account = data;
بـ:
updateState('account', data);
سنستغل الفرصة الآن لإصلاح مشكلة عدم تنظيف بيانات الحساب عندما ينقر المستخدم على تسجيل الخروج.
قم بإنشاء وظيفة جديدة logout()
:
function logout() {
updateState('account', null);
navigate('/login');
}
في updateDashboard()
، استبدل التوجيه return navigate('/login');
بـ return logout()
;
حاول تسجيل حساب جديد، تسجيل الخروج ثم تسجيل الدخول مرة أخرى للتحقق من أن كل شيء لا يزال يعمل بشكل صحيح.
نصيحة: يمكنك إلقاء نظرة على جميع تغييرات الحالة عن طريق إضافة
console.log(state)
في أسفلupdateState()
وفتح وحدة التحكم في أدوات تطوير المتصفح.
استمرار الحالة
تحتاج معظم تطبيقات الويب إلى استمرار البيانات لتتمكن من العمل بشكل صحيح. يتم عادةً تخزين جميع البيانات الحرجة في قاعدة بيانات ويتم الوصول إليها عبر واجهة برمجة التطبيقات الخاصة بالخادم، مثل بيانات حساب المستخدم في حالتنا. ولكن في بعض الأحيان، من المثير للاهتمام أيضًا استمرار بعض البيانات في تطبيق العميل الذي يعمل في المتصفح، لتحسين تجربة المستخدم أو تحسين أداء التحميل.
عندما تريد استمرار البيانات في المتصفح، هناك بعض الأسئلة المهمة التي يجب أن تطرحها على نفسك:
- هل البيانات حساسة؟ يجب تجنب تخزين أي بيانات حساسة على العميل، مثل كلمات مرور المستخدم.
- إلى متى تحتاج إلى الاحتفاظ بهذه البيانات؟ هل تخطط للوصول إلى هذه البيانات فقط للجلسة الحالية أم تريد تخزينها إلى الأبد؟
هناك طرق متعددة لتخزين المعلومات داخل تطبيق الويب، اعتمادًا على ما تريد تحقيقه. على سبيل المثال، يمكنك استخدام عناوين URL لتخزين استعلام البحث، وجعله قابلاً للمشاركة بين المستخدمين. يمكنك أيضًا استخدام ملفات تعريف الارتباط HTTP إذا كانت البيانات تحتاج إلى المشاركة مع الخادم، مثل معلومات المصادقة.
خيار آخر هو استخدام واحدة من العديد من واجهات برمجة التطبيقات للمتصفح لتخزين البيانات. اثنتان منها مثيرتان للاهتمام بشكل خاص:
localStorage
: مخزن مفتاح/قيمة يسمح باستمرار البيانات الخاصة بموقع الويب الحالي عبر جلسات مختلفة. البيانات المحفوظة فيه لا تنتهي صلاحيتها أبدًا.sessionStorage
: يعمل هذا بنفس طريقةlocalStorage
باستثناء أن البيانات المخزنة فيه يتم مسحها عند انتهاء الجلسة (عند إغلاق المتصفح).
لاحظ أن كلا واجهتي برمجة التطبيقات تسمح فقط بتخزين النصوص. إذا كنت تريد تخزين كائنات معقدة، ستحتاج إلى تسلسلها إلى صيغة JSON باستخدام JSON.stringify()
.
✅ إذا كنت تريد إنشاء تطبيق ويب لا يعمل مع خادم، من الممكن أيضًا إنشاء قاعدة بيانات على العميل باستخدام واجهة برمجة التطبيقات IndexedDB
. هذا الخيار مخصص لحالات الاستخدام المتقدمة أو إذا كنت بحاجة إلى تخزين كمية كبيرة من البيانات، حيث إنه أكثر تعقيدًا في الاستخدام.
المهمة
نريد أن يبقى المستخدمون مسجلين الدخول حتى ينقروا صراحة على زر تسجيل الخروج، لذا سنستخدم localStorage
لتخزين بيانات الحساب. أولاً، دعنا نحدد مفتاحًا سنستخدمه لتخزين بياناتنا.
const storageKey = 'savedAccount';
ثم أضف هذا السطر في نهاية وظيفة updateState()
:
localStorage.setItem(storageKey, JSON.stringify(state.account));
بهذا، ستظل بيانات حساب المستخدم مستمرة ومحدثة دائمًا حيث قمنا بمركزية جميع تحديثات الحالة سابقًا. هذا هو المكان الذي نبدأ فيه الاستفادة من جميع عمليات إعادة الهيكلة السابقة 🙂.
نظرًا لأن البيانات محفوظة، علينا أيضًا أن نهتم باستعادتها عند تحميل التطبيق. نظرًا لأننا سنبدأ في الحصول على المزيد من كود التهيئة، قد يكون من الجيد إنشاء وظيفة جديدة init
، التي تتضمن أيضًا الكود السابق في أسفل app.js
:
function init() {
const savedAccount = localStorage.getItem(storageKey);
if (savedAccount) {
updateState('account', JSON.parse(savedAccount));
}
// Our previous initialization code
window.onpopstate = () => updateRoute();
updateRoute();
}
init();
هنا نستعيد البيانات المحفوظة، وإذا كانت موجودة نقوم بتحديث الحالة وفقًا لذلك. من المهم القيام بذلك قبل تحديث المسار، حيث قد يكون هناك كود يعتمد على الحالة أثناء تحديث الصفحة.
يمكننا أيضًا جعل صفحة لوحة التحكم الصفحة الافتراضية لتطبيقنا، حيث إننا الآن نستمر في بيانات الحساب. إذا لم يتم العثور على بيانات، فإن لوحة التحكم تتولى إعادة التوجيه إلى صفحة تسجيل الدخول على أي حال. في updateRoute()
، استبدل التوجيه الافتراضي return navigate('/login');
بـ return navigate('/dashboard');
.
الآن قم بتسجيل الدخول إلى التطبيق وحاول تحديث الصفحة. يجب أن تبقى في لوحة التحكم. مع هذا التحديث، قمنا بمعالجة جميع مشاكلنا الأولية...
تحديث البيانات
...لكن قد نكون قد أنشأنا مشكلة جديدة. أوه!
انتقل إلى لوحة التحكم باستخدام حساب test
، ثم قم بتشغيل هذا الأمر في الطرفية لإنشاء معاملة جديدة:
curl --request POST \
--header "Content-Type: application/json" \
--data "{ \"date\": \"2020-07-24\", \"object\": \"Bought book\", \"amount\": -20 }" \
http://localhost:5000/api/accounts/test/transactions
حاول تحديث صفحة لوحة التحكم في المتصفح الآن. ماذا يحدث؟ هل ترى المعاملة الجديدة؟
الحالة مستمرة إلى أجل غير مسمى بفضل localStorage
، ولكن هذا يعني أيضًا أنها لا يتم تحديثها أبدًا حتى تقوم بتسجيل الخروج من التطبيق وتسجيل الدخول مرة أخرى!
إحدى الاستراتيجيات الممكنة لإصلاح ذلك هي إعادة تحميل بيانات الحساب في كل مرة يتم فيها تحميل لوحة التحكم، لتجنب البيانات القديمة.
المهمة
قم بإنشاء وظيفة جديدة updateAccountData
:
async function updateAccountData() {
const account = state.account;
if (!account) {
return logout();
}
const data = await getAccount(account.user);
if (data.error) {
return logout();
}
updateState('account', data);
}
تتحقق هذه الطريقة من أننا مسجلون الدخول حاليًا ثم تعيد تحميل بيانات الحساب من الخادم.
قم بإنشاء وظيفة أخرى باسم refresh
:
async function refresh() {
await updateAccountData();
updateDashboard();
}
تقوم هذه الوظيفة بتحديث بيانات الحساب، ثم تهتم بتحديث HTML الخاص بصفحة لوحة التحكم. هذا ما نحتاج إلى استدعائه عند تحميل مسار لوحة التحكم. قم بتحديث تعريف المسار بـ:
const routes = {
'/login': { templateId: 'login' },
'/dashboard': { templateId: 'dashboard', init: refresh }
};
حاول إعادة تحميل لوحة التحكم الآن، يجب أن تعرض بيانات الحساب المحدثة.
🚀 التحدي
الآن بعد أن نقوم بإعادة تحميل بيانات الحساب في كل مرة يتم فيها تحميل لوحة التحكم، هل تعتقد أننا ما زلنا بحاجة إلى استمرار كل بيانات الحساب؟
حاول العمل معًا لتغيير ما يتم حفظه وتحميله من localStorage
ليشمل فقط ما هو ضروري تمامًا لعمل التطبيق.
اختبار ما بعد المحاضرة
المهمة
تنفيذ مربع الحوار "إضافة معاملة"
إليك مثالاً على النتيجة بعد إكمال المهمة:
إخلاء المسؤولية:
تم ترجمة هذا المستند باستخدام خدمة الترجمة بالذكاء الاصطناعي Co-op Translator. بينما نسعى لتحقيق الدقة، يرجى العلم أن الترجمات الآلية قد تحتوي على أخطاء أو معلومات غير دقيقة. يجب اعتبار المستند الأصلي بلغته الأصلية المصدر الموثوق. للحصول على معلومات حاسمة، يُوصى بالاستعانة بترجمة بشرية احترافية. نحن غير مسؤولين عن أي سوء فهم أو تفسيرات خاطئة تنشأ عن استخدام هذه الترجمة.