You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web-Dev-For-Beginners/translations/ar/7-bank-project/4-state-management/README.md

21 KiB

بناء تطبيق مصرفي الجزء 4: مفاهيم إدارة الحالة

اختبار ما قبل المحاضرة

اختبار ما قبل المحاضرة

المقدمة

مع نمو تطبيقات الويب، يصبح من الصعب تتبع تدفقات البيانات. أي كود يحصل على البيانات؟ أي صفحة تستهلكها؟ أين ومتى يجب تحديثها؟ من السهل أن ينتهي بك الأمر بكود فوضوي يصعب صيانته. هذا الأمر يصبح أكثر تعقيدًا عندما تحتاج إلى مشاركة البيانات بين صفحات مختلفة في التطبيق، مثل بيانات المستخدم. مفهوم إدارة الحالة كان دائمًا موجودًا في جميع أنواع البرامج، ولكن مع زيادة تعقيد تطبيقات الويب، أصبح نقطة أساسية يجب التفكير فيها أثناء التطوير.

في هذا الجزء الأخير، سنراجع التطبيق الذي قمنا ببنائه لإعادة التفكير في كيفية إدارة الحالة، مما يسمح بدعم تحديث المتصفح في أي وقت، والحفاظ على البيانات عبر جلسات المستخدم.

المتطلبات الأساسية

يجب أن تكون قد أكملت جزء جلب البيانات من تطبيق الويب لهذه الدرس. كما تحتاج إلى تثبيت Node.js وتشغيل خادم API محليًا حتى تتمكن من إدارة بيانات الحساب.

يمكنك اختبار أن الخادم يعمل بشكل صحيح عن طريق تنفيذ هذا الأمر في الطرفية:

curl http://localhost:5000/api
# -> should return "Bank API v1.0.0" as a result

إعادة التفكير في إدارة الحالة

في الدرس السابق، قدمنا مفهومًا أساسيًا للحالة في تطبيقنا باستخدام المتغير العام account الذي يحتوي على بيانات البنك للمستخدم الذي قام بتسجيل الدخول حاليًا. ومع ذلك، فإن التنفيذ الحالي لدينا به بعض العيوب. حاول تحديث الصفحة عندما تكون في لوحة التحكم. ماذا يحدث؟

هناك 3 مشكلات في الكود الحالي:

  • الحالة غير مستمرة، حيث يؤدي تحديث المتصفح إلى إعادتك إلى صفحة تسجيل الدخول.
  • هناك وظائف متعددة تعدل الحالة. مع نمو التطبيق، قد يصبح من الصعب تتبع التغييرات ومن السهل نسيان تحديث واحدة منها.
  • الحالة لا يتم تنظيفها، لذا عند النقر على تسجيل الخروج، تظل بيانات الحساب موجودة حتى وأنت في صفحة تسجيل الدخول.

يمكننا تحديث الكود لمعالجة هذه المشكلات واحدة تلو الأخرى، ولكن ذلك سيؤدي إلى تكرار الكود وجعل التطبيق أكثر تعقيدًا وصعوبة في الصيانة. أو يمكننا التوقف لبضع دقائق وإعادة التفكير في استراتيجيتنا.

ما هي المشكلات التي نحاول حلها هنا؟

إدارة الحالة تدور حول إيجاد نهج جيد لحل هاتين المشكلتين بالتحديد:

  • كيف يمكن الحفاظ على تدفقات البيانات في التطبيق بشكل مفهوم؟
  • كيف يمكن الحفاظ على تزامن بيانات الحالة دائمًا مع واجهة المستخدم (والعكس صحيح)؟

بمجرد أن تعتني بهاتين المشكلتين، قد يتم حل أي مشكلات أخرى لديك بالفعل أو تصبح أسهل في الحل. هناك العديد من النهج الممكنة لحل هذه المشكلات، ولكننا سنختار حلاً شائعًا يتكون من مركزة البيانات وطرق تغييرها. ستتدفق البيانات على النحو التالي:

مخطط يوضح تدفقات البيانات بين HTML، إجراءات المستخدم والحالة

لن نغطي هنا الجزء الذي يتم فيه تحديث العرض تلقائيًا بواسطة البيانات، حيث يرتبط بمفاهيم أكثر تقدمًا في البرمجة التفاعلية. إنه موضوع جيد للمتابعة إذا كنت ترغب في التعمق.

هناك العديد من المكتبات التي تقدم نهجًا مختلفًا لإدارة الحالة، مثل 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() وفتح وحدة التحكم في أدوات تطوير المتصفح.

الحفاظ على الحالة

تحتاج معظم تطبيقات الويب إلى الحفاظ على البيانات لتعمل بشكل صحيح. عادةً ما يتم تخزين جميع البيانات الحرجة في قاعدة بيانات ويتم الوصول إليها عبر خادم API، مثل بيانات حساب المستخدم في حالتنا. ولكن في بعض الأحيان، من المفيد أيضًا الحفاظ على بعض البيانات في تطبيق العميل الذي يعمل في متصفحك، لتحسين تجربة المستخدم أو تحسين أداء التحميل.

عندما تريد الحفاظ على البيانات في متصفحك، هناك بعض الأسئلة المهمة التي يجب أن تسألها لنفسك:

  • هل البيانات حساسة؟ يجب تجنب تخزين أي بيانات حساسة على العميل، مثل كلمات مرور المستخدم.
  • إلى متى تحتاج إلى الاحتفاظ بهذه البيانات؟ هل تخطط للوصول إلى هذه البيانات فقط للجلسة الحالية أم تريد تخزينها إلى الأبد؟

هناك طرق متعددة لتخزين المعلومات داخل تطبيق ويب، بناءً على ما تريد تحقيقه. على سبيل المثال، يمكنك استخدام عناوين 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. بينما نسعى لتحقيق الدقة، يرجى العلم أن الترجمات الآلية قد تحتوي على أخطاء أو عدم دقة. يجب اعتبار المستند الأصلي بلغته الأصلية المصدر الرسمي. للحصول على معلومات حاسمة، يُوصى بالترجمة البشرية الاحترافية. نحن غير مسؤولين عن أي سوء فهم أو تفسيرات خاطئة تنشأ عن استخدام هذه الترجمة.