تطبيقات أخرى غير SPA: تصاميم بديلة لتطبيقات الويب التقدّمية (PWA)

لنتحدث عن... الهندسة؟

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

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

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

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

تطبيق الويب التقدّمي Stack Overflow

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

التطبيقات المتعددة الصفحات (MPA)

قبل الدخول في التفاصيل، دعونا نحدد بعض المصطلحات ونشرح أجزاء من التكنولوجيا الأساسية. أولاً، سأتناول ما أحب أن أسميه "تطبيقات صفحات متعددة" أو "MPA".

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

وهذا على عكس نموذج تطبيق الصفحة الواحدة (SPA) لإنشاء تطبيقات الويب، الذي يشغّل فيه المتصفح رمز JavaScript لتعديل الصفحة الحالية عندما يزور المستخدِم قسمًا جديدًا. يُعد كل من SPA وMPA نماذج صالحة للاستخدام على حد سواء، ولكن في هذه المشاركة، أردت استكشاف مفاهيم PWA ضمن سياق تطبيق متعدد الصفحات.

سريع بشكل موثوق

لقد سمعتني (وعدد لا يحصى من الآخرين) يستخدمون عبارة "تطبيق الويب التقدمي" أو PWA. قد تكون معتادًا على بعض مواد الخلفية في أي مكان آخر على هذا الموقع.

يمكنك اعتبار تطبيق الويب التقدّمي (PWA) كتطبيق ويب يوفّر تجربة مستخدم من الدرجة الأولى، وبالتالي يكسب مكانًا على الشاشة الرئيسية للمستخدم. يلخّص اختصار "FIRE" جميع السمات التي يجب التفكير فيها عند إنشاء تطبيق ويب تقدّمي (PWA) للإشارة إلى Fast وIT متكامل وRقابل ومتفاعل وجذاب.

سأركّز في هذه المقالة على مجموعة فرعية من هاتين السمتَين: سريعة وموثوقة.

سريعة: مع أنّ كلمة "سريع" تعني أشياء مختلفة في سياقات مختلفة، سأتحدّث عن فوائد سرعة التحميل قدر الإمكان من الشبكة.

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

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

تمكين التقنيات: عاملو الخدمة + واجهة برمجة تطبيقات ذاكرة التخزين المؤقت لتخزين

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

يمكنك إنشاء مشغّل خدمات يستجيب للطلبات الواردة، ويمرر بعضها إلى الشبكة، ويخزن نسخة من الاستجابة لاستخدامها في المستقبل، عبر واجهة برمجة تطبيقات التخزين المؤقت.

مشغّل خدمات يستخدم واجهة برمجة التطبيقات Cache Storage API لحفظ نسخة من استجابة الشبكة.

وفي المرة التالية التي يُجري فيها تطبيق الويب الطلب نفسه، يستطيع عامل الخدمة فحص ذاكرات التخزين المؤقت وعرض الاستجابة المخزَّنة مؤقتًا في السابق.

عامل خدمات يستخدم واجهة برمجة التطبيقات Cache Storage API للاستجابة وتجاوز الشبكة.

إنّ تجنُّب استخدام الشبكة كلما أمكن ذلك هو جزء مهم من تقديم أداء سريع وموثوق به.

JavaScript "Isomorphic"

هناك مفهوم آخر أودّ التطرق إليه وهو ما يشار إليه أحيانًا باسم "مشابهة الشكل" أو "عام" JavaScript. باختصار، تكمن فكرة أنه يمكن مشاركة رمز JavaScript نفسه بين بيئات وقت تشغيل مختلفة. عندما أنشأت تطبيق الويب التقدّمي (PWA)، أردت مشاركة رمز JavaScript بين الخادم الخلفي ومشغّل الخدمة.

هناك الكثير من المناهج الصالحة لمشاركة الرموز البرمجية بهذه الطريقة، ولكن أسلوبي كان استخدام وحدات ES كرمز المصدر النهائي. بعد ذلك، نقلت هذه الوحدات للخادم ومشغّل الخدمات وجمّعتها باستخدام مزيج من Babel وRollup. في مشروعي، الملفات ذات امتداد الملف .mjs هي رمز موجود في وحدة ES.

الخادم

مع وضع هذه المفاهيم والمصطلحات في الاعتبار، دعنا نتعمق في كيفية إنشاء PWA الخاص بي في Stack Overflow. سأبدأ بتناول خادم الخلفية، وشرح كيف يتناسب ذلك مع البنية العامة.

كنت أبحث عن مزيج من خلفية ديناميكية إلى جانب الاستضافة الثابتة، وكان أسلوبي هو استخدام نظام Firebase الأساسي.

تعمل دوال Firebase Cloud تلقائيًا على تشغيل بيئة مستنِدة إلى العُقدة عند توفُّر طلب وارد، وستندمج مع إطار عمل Express HTTP الرائج الذي كنتُ أعرفه من قبل. وتوفّر أيضًا استضافة جاهزة لجميع الموارد الثابتة على موقعي الإلكتروني. لنلقِ نظرة على كيفية معالجة الخادم للطلبات.

عندما يُجري أحد المتصفِّحات طلب انتقال على الخادم الذي نستخدمه، يمر خلال التدفق التالي:

نظرة عامة على إنشاء استجابة تنقل من جهة الخادم

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

هناك جزأين من هذه الصورة يستحقان الاستكشاف بمزيد من التفصيل: التوجيه والقوالب.

يتم الآن تخطيط المسار

في ما يتعلق بالتوجيه، كان نهجي هو استخدام بنية التوجيه الأصلي لإطار عمل Express. وهي مرن بما يكفي لمطابقة بادئات عناوين URL البسيطة، بالإضافة إلى عناوين URL التي تتضمن معلَمات كجزء من المسار. وهنا، أقوم بإنشاء تعيين بين أسماء المسارات لنمط Express الأساسي للمطابقة معه.

const routes = new Map([
  ['about', '/about'],
  ['questions', '/questions/:questionId'],
  ['index', '/'],
]);

export default routes;

ويمكنني عند ذلك الرجوع إلى عملية الربط هذه مباشرةً من رمز الخادم. عندما تكون هناك مطابقة لنمط Express معين، يتجاوب المعالج المناسب مع منطق النموذج الخاص بالمسار المطابق.

import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
  // Templating logic.
});

النماذج من جهة الخادم

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

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

لمعرفة ما أعنيه، يُرجى إلقاء نظرة على الرمز السريع لأحد المسارات:

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

باستخدام write() في العنصر response والإشارة إلى النماذج الجزئية المخزّنة محليًا، يمكنني بدء بث الاستجابة فورًا، بدون حظر أي مصدر بيانات خارجي. يستخدم المتصفح رمز HTML الأولي هذا يعرض واجهة مفيدة ورسالة محمّلة على الفور.

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

بمجرد أن يتلقى تطبيق الويب الاستجابة من واجهة برمجة تطبيقات Stack Exchange، فإنه يستدعي دالة نموذج مخصصة لترجمة البيانات من واجهة برمجة التطبيقات إلى رمز HTML المقابل لها.

لغة النماذج

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

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

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

export function index(tag, items) {
  const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
  const form = `<form method="GET">...</form>`;
  const questionCards = items
    .map(item =>
      questionCard({
        id: item.question_id,
        title: item.title,
      })
    )
    .join('');
  const questions = `<div id="questions">${questionCards}</div>`;
  return title + form + questions;
}

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

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url="${questionUrl(id)}">${title}</a>`;
}

من الجدير بالملاحظة أنّ سمة البيانات التي أضيفها إلى كل رابط، data-cache-url، تم ضبطها على عنوان URL لواجهة برمجة تطبيقات Stack Exchange الذي أحتاجه لعرض السؤال المعنيّ. ضع ذلك في الاعتبار. سأعيد النظر فيه لاحقًا.

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

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

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

مشغّل الخدمات

نظرة عامة على إنشاء استجابة تنقُّل في مشغّل الخدمات.

يجب أن يبدو هذا الرسم التخطيطي مألوفًا - العديد من نفس القطع التي قمت بتناولها سابقًا موجودة هنا بترتيب مختلف قليلاً. لنتصفح تدفق الطلب، مع أخذ عامل الخدمة في الاعتبار.

يتعامل عامل الخدمة لدينا مع طلب تنقل وارد لعنوان URL معين، وكما يفعل الخادم لدي، فهو يستخدم مزيجًا من التوجيه والمنطق النموذجي لمعرفة كيفية الاستجابة.

لم يعُد هذا النهج هو نفسه كما كان من قبل، ولكن مع إعدادات أساسية مختلفة ذات مستوى منخفض، مثل fetch() وواجهة برمجة تطبيقات التخزين المؤقت. وأستخدم مصادر البيانات هذه لإنشاء استجابة HTML، والتي يعيدها عامل الخدمة إلى تطبيق الويب.

Workbox

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

يتم الآن تخطيط المسار

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

كانت استراتيجيتي هي ترجمة كل مسار من مسارات Express إلى تعبير عادي مقابل، مع الاستفادة من مكتبة مفيدة تُعرف باسم regexparam. بعد تنفيذ هذه الترجمة، يمكنني الاستفادة من الدعم المضمَّن في Workbox لتوجيه التعبير العادي.

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

import regExpRoutes from './regexp-routes.mjs';

workbox.routing.registerRoute(
  regExpRoutes.get('index')
  // Templating logic.
);

التخزين المؤقت لمواد العرض الثابتة

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

أقول لـ Workbox عناوين URL التي يجب تخزينها مؤقتًا بشكل مسبق باستخدام ملف إعداد، مشيرًا إلى الدليل الذي يحتوي على جميع الأصول المحلية مع مجموعة من الأنماط المطلوب مطابقتها. تتم قراءة هذا الملف تلقائيًا من خلال واجهة سطر الأوامر في Workspace، والتي run في كل مرة أعيد فيها إنشاء الموقع.

module.exports = {
  globDirectory: 'build',
  globPatterns: ['**/*.{html,js,svg}'],
  // Other options...
};

يأخذ Workbox لقطة من محتوى كل ملف، ويدخل تلقائيًا قائمة عناوين URL والمراجعات هذه في ملف عامل الخدمات النهائي. يحتوي Workbox الآن على كل ما يحتاج إليه لجعل الملفات المخزنة مؤقتًا بشكل مسبق متاحة دائمًا وتحديثها. والنتيجة هي ملف service-worker.js يحتوي على ما يشبه ما يلي:

workbox.precaching.precacheAndRoute([
  {
    url: 'partials/about.html',
    revision: '518747aad9d7e',
  },
  {
    url: 'partials/foot.html',
    revision: '69bf746a9ecc6',
  },
  // etc.
]);

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

بالوضعَين الأفقي والعمودي

بعد ذلك، أريد أن يقوم عامل الخدمة ببث محتوى HTML الجزئي الموجود في ذاكرة التخزين المؤقت وإعادته إلى تطبيق الويب على الفور. هذا جزء مهم من أن تكون "سريعة بشكل موثوق" - فأنا أحصل دائمًا على شيء ذي معنى على الشاشة على الفور. لحسن الحظ، فإن استخدام Streams API في مشغّل الخدمات لدينا يجعل ذلك ممكنًا.

ربما تكون قد سمعت الآن عن Streams API من قبل. زميلي جيك أرشيبالد يغنّي مديحه لسنوات. لقد توقّع بشكل واضح أنّ عام 2016 سيكون عامًا من مصادر تدفق بيانات الويب. وصارت واجهة برمجة تطبيقات Streams API رائعة اليوم كما كانت قبل عامين، ولكن مع فارق كبير.

على الرغم من أنّ ميزة "ساحات المشاركات" هي فقط Chrome في ذلك الوقت، أصبحت واجهة برمجة تطبيقات Streams API متاحة الآن على نطاق أوسع. القصة بشكل عام إيجابية، وباستخدام الرمز الاحتياطي المناسب، لا يوجد ما يمنعك من استخدام مجموعات البث في عامل الخدمة اليوم.

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

const stream = new ReadableStream({
  pull(controller) {
    return sources[0]
      .then(r => r.read())
      .then(result => {
        if (result.done) {
          sources.shift();
          if (sources.length === 0) return controller.close();
          return this.pull(controller);
        } else {
          controller.enqueue(result.value);
        }
      });
  },
});

لكن فهم الآثار الكاملة لهذه التعليمات البرمجية قد لا يكون مناسبًا للجميع. وبدلاً من تحليل هذا المنطق، سنتحدّث عن أسلوبي في التعامل مع بث موظفي الخدمات.

أستخدم برنامج تضمين جديد وعالية المستوى، workbox-streams. باستخدامها، يمكنني تمريرها في مزيج من مصادر البث، من ذاكرات التخزين المؤقت وبيانات وقت التشغيل التي قد تأتي من الشبكة. يعتني Workbox بتنسيق المصادر الفردية وتجميعها معًا في رد واحد متدفق.

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

التخزين المؤقت في وقت التشغيل

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

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

const cacheStrategy = workbox.strategies.cacheFirst({
  cacheName: workbox.core.cacheNames.precache,
});

const apiStrategy = workbox.strategies.staleWhileRevalidate({
  cacheName: API_CACHE_NAME,
  plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});

تقرأ الإستراتيجية الأولى البيانات التي تم تخزينها مؤقتًا بشكل مسبق، مثل قوالب HTML الجزئية.

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

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

workbox.streams.strategy([
  () => cacheStrategy.makeRequest({request: '/head.html'}),
  () => cacheStrategy.makeRequest({request: '/navbar.html'}),
  async ({event, url}) => {
    const tag = url.searchParams.get('tag') || DEFAULT_TAG;
    const listResponse = await apiStrategy.makeRequest(...);
    const data = await listResponse.json();
    return templates.index(tag, data.items);
  },
  () => cacheStrategy.makeRequest({request: '/foot.html'}),
]);

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

تجلب دالة المصدر التالية البيانات من واجهة برمجة تطبيقات Stack Exchange، وتعالج الاستجابة في ملف HTML الذي يتوقعه تطبيق الويب.

تعني استراتيجية إعادة التحقق القديمة أنّه إذا كان لديّ استجابة مخزَّنة مؤقتًا في وقت سابق لطلب واجهة برمجة التطبيقات هذا، فسأتمكّن من بثها إلى الصفحة على الفور، مع تحديث إدخال ذاكرة التخزين المؤقت "في الخلفية" في المرة التالية التي يُطلب فيها ذلك.

أخيرًا، أقوم ببث نسخة مخبأة من التذييل وأغلق علامات HTML النهائية، لإكمال الاستجابة.

مشاركة الرمز يحافظ على مزامنة الأشياء

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

تحسينات ديناميكية تدريجية

لقد اطّلعت على الخادم ومشغّل الخدمات لتطبيق الويب التقدّمي (PWA)، ولكن هناك جزء أخير من المنطق الذي يجب تناوله: هناك قدر صغير من JavaScript يتم تشغيله في كل صفحة من صفحاتي، بعد بثّها بالكامل.

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

البيانات الوصفية للصفحة

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

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

const metadataScript = `<script>
  self._title = '${escape(item.title)}';
</script>`;

بعد ذلك، بعد تحميل صفحتي، أقرأ تلك السلسلة وأعدّل عنوان المستند.

if (self._title) {
  document.title = unescape(self._title);
}

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

تجربة المستخدم بلا إنترنت

يُستخدم التحسين التدريجي الآخر الذي أضفته للفت الانتباه إلى إمكانياتنا في وضع عدم الاتصال. لقد أنشأت تطبيق ويب تقدّميًا موثوقًا به (PWA)، وأريد أن يعرف المستخدمون أنّه يمكنهم تحميل الصفحات التي سبقت زيارتها عندما يكونون غير متصلين بالإنترنت.

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

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

عندما يدخل المتصفح في حالة عدم الاتصال بالإنترنت، أعرض قائمة الروابط غير المخزنة مؤقتًا وأخفِّت الروابط التي لن تعمل. تذكَّر أنّ هذا مجرد تلميح مرئي للمستخدم حول ما يجب أن يتوقعه من تلك الصفحات، وأنا لا أوقِف الروابط أو أمنع المستخدم من التنقل.

const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);

const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filter(card => {
  return !cachedUrls.includes(card.dataset.cacheUrl);
});

const offlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '0.3';
  }
};

const onlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '1.0';
  }
};

window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);

الصعوبات الشائعة

لقد اطّلعتُ على أسلوبي في إنشاء تطبيق ويب تقدّمي (PWA) متعدّد الصفحات. هناك العديد من العوامل التي سيتعين عليك مراعاتها عند التوصل إلى نهجك الخاص، وقد ينتهي بك الأمر باتخاذ خيارات مختلفة عما فعلته. هذه المرونة هي إحدى الأشياء الرائعة التي يتم إنشاؤها للويب.

هناك بعض الصعوبات الشائعة التي قد تواجهها عند اتخاذ قراراتك المعمارية، وأريد أن أوفر عليك بعض العقبات.

عدم تخزين HTML الكامل في ذاكرة التخزين المؤقت

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

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

انحراف الخادم / مشغّل الخدمات

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

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

أسوأ السيناريوهات

التصميم أو التنسيق غير متسق

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

سيناريو الحالة الأسوأ: تعطُّل التوجيه

بدلاً من ذلك، قد يصادف المستخدم عنوان URL يعالجه الخادم، وليس مشغّل الخدمات. الموقع المليء بتصميمات الزومبي والنهايات المسدودة ليس تطبيقًا تقدّميًا موثوقًا به.

نصائح للنجاح

لكنك لست وحدك في هذا! يمكن أن تساعدك النصائح التالية في تجنب هذه المخاطر:

استخدام مكتبات النماذج والتوجيه التي تتضمّن عمليات تنفيذ بلغات متعدّدة

حاول استخدام مكتبات النماذج والتوجيه التي تحتوي على عمليات تنفيذ JavaScript. والآن، أعلم أنه ليس كل مطور برامج لديه الرفاهية في الانتقال من خادم الويب الحالي ولغة النماذج.

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

تفضيل النماذج المتسلسلة بدلاً من النماذج المدمجة

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

التخزين المؤقت لكل من المحتوى الثابت والديناميكي في مشغّل الخدمات

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

الحظر على الشبكة فقط عند الضرورة القصوى

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

المراجِع