استراتيجيات التخزين المؤقت للعاملين في مجال الخدمات

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

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

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

واجهة Cache مقابل ذاكرة التخزين المؤقت لـ HTTP

إذا لم يسبق لك العمل على واجهة Cache، قد يكون من المغري استخدام واجهة برمجة التطبيقات المماثلة لذاكرة التخزين المؤقت لبروتوكول HTTP أو على الأقل مرتبطة بها. لكن الأمر ليس كذلك.

  • وواجهة Cache هي آلية تخزين مؤقت منفصلة تمامًا عن ذاكرة التخزين المؤقت لبروتوكول HTTP.
  • أيًا كانت إعدادات Cache-Control التي تستخدمها للتأثير في ذاكرة التخزين المؤقت لبروتوكول HTTP، ليس لها أي تأثير على مواد العرض التي يتم تخزينها في واجهة Cache.

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

في المقابل، إنّ واجهة Cache هي ذاكرة تخزين مؤقت عالية المستوى يتم تشغيلها باستخدام JavaScript API. يوفّر هذا الأمر مرونة أكبر مقارنةً باستخدام أزواج مفاتيح/قيم HTTP مبسّطة نسبيًا، ويشكل أحد نصف ما يجعل استراتيجيات التخزين المؤقت ممكنة. في ما يلي بعض طرق واجهة برمجة التطبيقات المهمة بشأن ذاكرات التخزين المؤقت لمشغِّلات الخدمات:

  • CacheStorage.open لإنشاء مثيل Cache جديد.
  • Cache.add وCache.put لتخزين استجابات الشبكة في ذاكرة التخزين المؤقت لمشغِّل الخدمات.
  • Cache.match لتحديد موقع استجابة مخبأة في مثيل Cache.
  • Cache.delete لإزالة استجابة مخزّنة مؤقتًا من مثيل Cache.

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

حدث fetch المتواضع

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

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('install', (event) => {
  event.waitUntil(caches.open(cacheName));
});

self.addEventListener('fetch', async (event) => {
  // Is this a request for an image?
  if (event.request.destination === 'image') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Respond with the image from the cache or from the network
      return cache.match(event.request).then((cachedResponse) => {
        return cachedResponse || fetch(event.request.url).then((fetchedResponse) => {
          // Add the network response to the cache for future visits.
          // Note: we need to make a copy of the response to save it in
          // the cache and use the original as the request response.
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

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

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

يحتوي عنصر event في الجلب على سمة request التي تتضمّن بعض المعلومات المفيدة لمساعدتك في تحديد نوع كل طلب:

  • url، وهو عنوان URL لطلب الشبكة الذي يعالجه حاليًا حدث fetch.
  • method، وهي طريقة الطلب (على سبيل المثال، GET أو POST).
  • mode، التي تصف وضع الطلب. غالبًا ما يتم استخدام القيمة 'navigate' للتمييز بين طلبات مستندات HTML والطلبات الأخرى.
  • destination، الذي يصف نوع المحتوى المطلوب بطريقة تتجنّب استخدام امتداد ملف مادة العرض المطلوبة.

مصطلح "غير متزامن" هو اسم اللعبة. تذكَّر أنّ فعالية install تقدّم طريقة event.waitUntil التي تستغرق وقتًا وعدًا، وتنتظر حتى يتم حلّ المشكلة قبل مواصلة التفعيل. يقدّم حدث fetch طريقة event.respondWith مماثلة يمكنك استخدامها لعرض نتيجة طلب fetch غير متزامن أو ردّ تم عرضه باستخدام طريقة match لواجهة Cache.

استراتيجيات التخزين المؤقت

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

ذاكرة التخزين المؤقت فقط

يعرض التدفق من الصفحة إلى مشغّل الخدمات إلى ذاكرة التخزين المؤقت.

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

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

// Assets to precache
const precachedAssets = [
  '/possum1.jpg',
  '/possum2.jpg',
  '/possum3.jpg',
  '/possum4.jpg'
];

self.addEventListener('install', (event) => {
  // Precache assets on install
  event.waitUntil(caches.open(cacheName).then((cache) => {
    return cache.addAll(precachedAssets);
  }));
});

self.addEventListener('fetch', (event) => {
  // Is this one of our precached assets?
  const url = new URL(event.request.url);
  const isPrecachedRequest = precachedAssets.includes(url.pathname);

  if (isPrecachedRequest) {
    // Grab the precached asset from the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request.url);
    }));
  } else {
    // Go to the network
    return;
  }
});

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

الشبكة فقط

يعرض التدفق من الصفحة إلى مشغّل الخدمات إلى الشبكة.

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

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

التخزين المؤقت أولاً، للرجوع إلى الشبكة

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

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

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

إليك مثال على هذه الاستراتيجية، ويمكنك اختبارها في عرض توضيحي مباشر:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a request for an image
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the cache first
      return cache.match(event.request.url).then((cachedResponse) => {
        // Return a cached response if we have one
        if (cachedResponse) {
          return cachedResponse;
        }

        // Otherwise, hit the network
        return fetch(event.request).then((fetchedResponse) => {
          // Add the network response to the cache for later visits
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

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

الشبكة أولاً، ثم الرجوع إلى ذاكرة التخزين المؤقت

يعرض التدفق من الصفحة إلى مشغّل الخدمات ثم إلى الشبكة ثم إلى ذاكرة التخزين المؤقت إذا لم تكن الشبكة متاحة.

إذا كنت تريد قلب خيار "ذاكرة التخزين المؤقت أولًا ثم الشبكة ثانيًا" رأسًا على الأقدام، فسينتهي بك الأمر باستخدام استراتيجية "الشبكة أولاً، ثم ذاكرة التخزين المؤقت ثانيًا"، كما يبدو الأمر:

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

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

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a navigation request
  if (event.request.mode === 'navigate') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the network first
      return fetch(event.request.url).then((fetchedResponse) => {
        cache.put(event.request, fetchedResponse.clone());

        return fetchedResponse;
      }).catch(() => {
        // If the network is unavailable, get
        return cache.match(event.request.url);
      });
    }));
  } else {
    return;
  }
});

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

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

إعادة التحقق القديمة

يعرض التدفق من الصفحة إلى مشغّل الخدمات، إلى ذاكرة التخزين المؤقت، ثم من الشبكة إلى ذاكرة التخزين المؤقت.

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

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

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

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        const fetchedResponse = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());

          return networkResponse;
        });

        return cachedResponse || fetchedResponse;
      });
    }));
  } else {
    return;
  }
});

يمكنك أن ترى ذلك عمليًّا في عرض توضيحي آخر مباشر، لا سيّما إذا كنت تنتبه إلى علامة تبويب "الشبكة" في أدوات المطوّرين على المتصفّح، وعارض CacheStorage الخاص به (إذا كانت أدوات المطوّرين في المتصفّح تتضمّن مثل هذه الأداة).

الانتقال إلى Workbox

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