Software Engineering8 يوليو 20268 min read

بناء تطبيقات جوال تعمل دون اتصال أولا: أنماط قابلة للتوسع

تفشل الشبكات على هواتف حقيقية في أماكن حقيقية. يعامل تصميم العمل دون اتصال أولا الجهاز المحلي بوصفه مصدر الحقيقة كي يبقى تطبيقك مفيدا على أي حال. إليك الأنماط التي تصمد وأنت تنمو.

بقلم Innovation T Team


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

لماذا يهم العمل دون اتصال أولا

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

لا يتعلق العمل دون اتصال أولا بانعدام الاتصال فقط. فهو يعالج أيضا الحالة الأكثر شيوعا بكثير، وهي الاتصال البطيء وغير الموثوق. تأتي عمليات القراءة من مخزن محلي، فتظهر الشاشات على الفور. وتُلتقط عمليات الكتابة محليا وتُؤكَّد للمستخدم فورا، ثم تُوفَّق مع الخادم في الخلفية. النتيجة هي تطبيق يبدو سريعا في كل مكان، لا على شبكة WiFi المكتب وحدها. هذه السرعة المدرَكة ميزة تنافسية حقيقية، وهي أحد الأسباب التي تجعلنا نزنها مبكرا حين نساعد العملاء على اختيار أساس (انظر ملاحظاتنا حول اختيار حزمة تقنية لبرمجيات SaaS في 2026).

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

مخازن البيانات المحلية أولا

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

تنقسم الخيارات إلى بضع عائلات. تصلح مخازن المفتاح والقيمة للإعدادات الصغيرة والرموز. أما قواعد البيانات العلائقية المضمّنة مثل SQLite (غالبا عبر غلاف) فتتعامل جيدا مع البيانات المنظمة القابلة للاستعلام. وتضيف المخازن المستندية والتفاعلية مثل WatermelonDB أو Realm أو PouchDB تتبع التغييرات وقابلية الملاحظة كي تتحدث واجهتك تلقائيا حين تتغير البيانات المحلية. أما محركات المزامنة الأحدث مثل ElectricSQL و PowerSync فتدفع بجزء أكبر من منطق المزامنة الصعب إلى المنصة نفسها.

يهم مبدآن بصرف النظر عن الأداة. أولا، صمّم بياناتك بحيث يستطيع العميل العمل بشكل مستقل، وهو ما يعني عادة إلغاء التطبيع قليلا ومنح كل سجل معرفا ثابتا يولّده العميل (UUID) بدلا من انتظار معرف من الخادم. ثانيا، حافظ على حد واضح بين الحالة المحلية وحالة المزامنة كي تعرف دائما ما جرى حفظه بشكل دائم، وما هو في قائمة الانتظار، وما جرى تأكيده.

وضع عمليات التغيير في قائمة انتظار

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

ينبغي أن تحمل كل عملية تغيير في قائمة الانتظار سياقا كافيا كي يُعاد تشغيلها لاحقا دون الواجهة الأصلية: نوع العملية، ومعرف السجل الهدف، والحقول المتغيرة، وطابع زمني من العميل، ومفتاح تكرار آمن (idempotency key). هذا المفتاح هو ما يتيح لك إعادة المحاولة بأمان. فإذا نجح الطلب لكن ضاعت الاستجابة، فإن إعادة تشغيله يجب ألا تنشئ نسخة مكررة.

type Mutation = {
  id: string;          // idempotency key (UUID)
  entity: "note";
  op: "create" | "update" | "delete";
  recordId: string;    // client-generated, stable
  payload: Record<string, unknown>;
  updatedAt: number;   // client clock, for ordering
};

// On any local change, enqueue then apply optimistically.
async function saveNote(note: Note) {
  await db.notes.put(note);            // local source of truth
  await outbox.enqueue(buildMutation(note));
}

تفرّغ حلقة مزامنة تعمل في الخلفية هذا الصندوق الصادر حين يعود الاتصال، فترسل عمليات التغيير بالترتيب، وتحترم استجابات الخادم، وتستخدم تراجعا أسيّا (exponential backoff) عند الفشل. صمّم نقاط النهاية في خادمك كي تقبل مفتاح التكرار الآمن وتعامل التكرارات بوصفها عمليات بلا أثر. هذا القرار وحده يزيل فئة كاملة من علل السجلات المكررة. وهو يهم أيضا على جانب API، ولهذا نعامل التكرار الآمن بوصفه شاغلا من الدرجة الأولى حين نصمم واجهات API يحبها المطورون.

الواجهة التفاؤلية

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

القاعدة التي تبقيها آمنة: طبّق التغيير محليا، وضع علامة على السجل بوصفه معلقا، ووفّق حين يؤكد الخادم أو يرفض. فإذا قبل الخادم، فامسح علامة التعليق. وإذا رفض (خطأ تحقق، أو تغير في الصلاحيات)، فتراجع عن التغيير المحلي وأظهر رسالة واضحة غير حاجبة. لا تدع تحديثا تفاؤليا ينحرف بصمت عن حقيقة الخادم أبدا. يسامح المستخدمون رسالة موجزة مثل "تعذر الحفظ، اضغط لإعادة المحاولة" أكثر بكثير مما يسامحون بيانات تختفي بهدوء.

استراتيجيات المزامنة: آخر كتابة تفوز مقابل CRDT

المزامنة هي المكان الذي تتراكم فيه خيارات التصميم، فاختر بتعمّد.

آخر كتابة تفوز (LWW، last-write-wins) هي الاستراتيجية الأبسط. يحمل كل سجل طابعا زمنيا أو رقم إصدار، وحين تتصادم نسختان، تفوز الأحدث. من السهل تنفيذها والتفكير فيها، وهي مناسبة للبيانات التي يكون فيها فقدان تعديل أقدم مقبولا: إعدادات المستخدم، أو حقل في الملف الشخصي، أو مستند ذو مالك واحد. ضعفها أنها تتجاهل بصمت الكتابة الخاسرة، وهو ما لا يُقبل للبيانات التعاونية أو التراكمية. كما تعتمد آخر كتابة تفوز على الساعات، وساعات الأجهزة تنحرف، لذا فضّل الإصدارات التي يعيّنها الخادم أو العدادات المنطقية على الوقت الخام للجهاز حيثما استطعت.

أنواع البيانات المكررة الخالية من التعارض (CRDT، conflict-free replicated data types) هي بنى بيانات مصممة كي تندمج التعديلات المتزامنة بشكل حتمي دون فقدان النية. يمكن لمستخدمين يحرران المستند نفسه دون اتصال أن يعودا كلاهما إلى الاتصال وتُدمج تغييراتهما بدلا من أن يطمس أحدهما الآخر. تنفّذ مكتبات مثل Yjs و Automerge هذا للنصوص والقوائم والخرائط والعدادات. الكلفة هي تعقيد إضافي، وحمولات وبيانات وصفية أكبر، ومنحنى تعلم أشد انحدارا.

من الحلول الوسط المفيدة الدمج على مستوى الحقل أو على مستوى العملية: عامل الحقول المستقلة بوصفها مستقلة كي لا تتعارض تعديلات سمات مختلفة أبدا، ولا تستدعِ حلا أثقل إلا حين يتصادم الحقل نفسه فعلا. كثير من التطبيقات لا تحتاج قط إلى CRDT كاملة؛ فهي تحتاج آخر كتابة تفوز لمعظم الحقول ودمجا دقيقا للحقول القليلة التعاونية حقا.

حل التعارضات

أيا كانت الاستراتيجية التي تختارها، قرّر منذ البداية كيف تظهر التعارضات. هناك ثلاثة خيارات عريضة: الحل تلقائيا (دمج آخر كتابة تفوز أو CRDT)، أو الحل بالسياسة (قواعد يحددها الخادم، مثل "لا يمكن للمخزون إلا أن ينقص")، أو الإحالة إلى المستخدم (عرض النسختين وترك الاختيار له). تمزج معظم التطبيقات الحقيقية الثلاثة جميعا. أتمِت الحالات الآمنة، وطبّق سياسة على الحالات الحرجة للأعمال، واحفظ الحل البشري للتصادم الحقيقي النادر الذي يكون فيه التخمين الخاطئ مكلفا. سجّل التعارضات حتى حين تحلّها تلقائيا، لأن تلك السجلات هي أسرع طريقة لمعرفة أين يخطئ نموذجك.

التعامل مع رموز المصادقة دون اتصال

المصادقة فخ هادئ في تصميم العمل دون اتصال أولا. إذا كان تطبيقك لا يستطيع العمل دون رمز جديد، فهو ليس فعلا يعمل دون اتصال أولا. خزّن بيانات الاعتماد بأمان على الجهاز باستخدام مخزن مفاتيح المنصة (iOS Keychain، Android Keystore)، لا في تخزين محلي بنص واضح أبدا. احتفظ برمز وصول قصير العمر إلى جانب رمز تحديث أطول عمرا، ودع التطبيق يعمل على الصلاحيات المخزنة محليا حين يكون دون اتصال بدلا من حجب الواجهة على تحديث الرمز.

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

اختبار الشبكات غير المستقرة

الشيفرة التي تعمل دون اتصال أولا وتُختبر عبر الإنترنت فقط هي شيفرة غير مختبرة. أنماط الإخفاق التي تهمك (الإرسال الجزئي، والاستجابات المفقودة، وقطع الاتصال في منتصف المزامنة، وانحراف الساعة) تظهر تحديدا في الظروف التي تخفيها بيئة اختبار عادية.

ابنِ القدرة على محاكاة الشبكات السيئة داخل سير عملك. استخدم أدوات على مستوى نظام التشغيل (Network Link Conditioner في iOS، وملفات تعريف شبكة محاكي Android) وأدوات وكيل مثل Charles أو منصة اختبار مخصصة لحقن زمن الاستجابة، وإسقاط الحزم، وقطع الاتصالات في منتصف الطلب. اكتب اختبارات آلية تبدّل الاتصال بين وضع العنصر في قائمة الانتظار وتفريغها، وتعيد تشغيل عملية التغيير نفسها مرتين لإثبات التكرار الآمن، وتفرض تعارضات بتحرير السجل نفسه من عميلين. اختبر تحديدا الانتقالات الوعرة: طلب أُرسل لكن استجابته لم تُستلم قط، وتطبيق أُنهي في منتصف المزامنة، وساعة جهاز ضُبطت خطأ. هذه هي العلل التي تصل إلى الإنتاج بخلاف ذلك.

قائمة تحقق التنفيذ

  1. اختر قاعدة بيانات محلية واجعلها مصدر الحقيقة للقراءات والكتابات.
  2. امنح كل سجل معرفا ثابتا يولّده العميل كي يوجد قبل أن يراه الخادم.
  3. التقط عمليات الكتابة في صندوق صادر دائم مع مفتاح تكرار آمن على كل عملية تغيير.
  4. ابنِ حلقة مزامنة في الخلفية مع إعادة تشغيل مرتبة وتراجع أسي.
  5. اعرض بشكل تفاؤلي، وضع علامة على السجلات بوصفها معلقة، ووفّق عند تأكيد الخادم.
  6. اختر استراتيجية مزامنة لكل نوع بيانات: آخر كتابة تفوز للحقول ذات المالك الواحد، و CRDT أو الدمج على مستوى الحقل للحقول التعاونية.
  7. عرّف سياسة تعارض تمزج الحل التلقائي والقائم على السياسة والمدفوع بالمستخدم، وسجّل كل تعارض.
  8. خزّن الرموز في مخزن مفاتيح المنصة، ودع التطبيق يعمل على الصلاحيات المخزنة حين يكون دون اتصال.
  9. أعد التحقق من كل عملية تغيير متزامنة على الخادم؛ ولا تثق أبدا بصلاحيات العميل المخزنة.
  10. اختبر مقابل شبكات غير مستقرة محاكاة، بما في ذلك قطع الاتصال في منتصف المزامنة وإعادة التشغيل المكررة.

من أين تبدأ

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

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

#الجوال#العمل دون اتصال أولا#المزامنة#هندسة البرمجيات

جاهز للبناء مع Innovation T؟

سواء كان الأمر يتعلق بالأمن أو النمو أو الهندسة، يمكن لفريقنا مساعدتك على تنفيذه بإتقان.