كتاب الطبخ بلا إنترنت

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

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

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

عند التثبيت - كتبعية

عند التثبيت: كتبعية
عند التثبيت: كعنصر تابع.

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

مثالي: CSS والصور والخطوط وJS والنماذج... وأي شيء يمكن أن تعتبره ثابتًا لهذه "النسخة" من موقعك.

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

self.addEventListener('install', function (event) {
  event.waitUntil(
    caches.open('mysite-static-v3').then(function (cache) {
      return cache.addAll([
        '/css/whatever-v3.css',
        '/css/imgs/sprites-v6.png',
        '/css/fonts/whatever-v8.woff',
        '/js/all-min-v4.js',
        // etc.
      ]);
    }),
  );
});

يتعهد event.waitUntil بتحديد مدة التثبيت ونجاحه. إذا تم رفض الوعد، يعني ذلك أنّ عملية التثبيت قد تعذّرت، وسيتم إيقاف مشغّل الخدمة هذا (إذا كان هناك إصدار سابق قيد التشغيل، سيتم تركه سليمًا). caches.open() وcache.addAll() وعدّات الإرجاع وفي حال تعذُّر جلب أي من الموارد، يتم رفض طلب cache.addAll().

في trained-to-thrill، أستخدم هذا الإجراء لتخزين مواد العرض الثابتة في ذاكرة التخزين المؤقت.

عند التثبيت، وليس كاعتمادية

عند التثبيت، وليس كتبعية.
عند التثبيت، وليس كعنصر تابع.

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

مناسبة لـ: الموارد الأكبر حجمًا التي لا تكون مطلوبة على الفور، مثل مواد العرض للمستويات اللاحقة في اللعبة

self.addEventListener('install', function (event) {
  event.waitUntil(
    caches.open('mygame-core-v1').then(function (cache) {
      cache
        .addAll
        // levels 11–20
        ();
      return cache
        .addAll
        // core assets and levels 1–10
        ();
    }),
  );
});

لا ينقل المثال أعلاه وعد cache.addAll بالمستويات من 11 إلى 20 إلى event.waitUntil، لذا حتى إذا تعذّر إكماله، ستظل اللعبة متاحة بلا إنترنت. بالطبع، عليك مراعاة احتمال عدم توفّر هذه المستويات وإعادة محاولة خزنها في حال عدم توفّرها.

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

تفعيل

عند التفعيل
تفعيل الميزة:

مناسبة لعمليات: التنظيف ونقل البيانات

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

self.addEventListener('activate', function (event) {
  event.waitUntil(
    caches.keys().then(function (cacheNames) {
      return Promise.all(
        cacheNames
          .filter(function (cacheName) {
            // Return true if you want to remove this cache,
            // but remember that caches are shared across
            // the whole origin
          })
          .map(function (cacheName) {
            return caches.delete(cacheName);
          }),
      );
    }),
  );
});

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

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

عند تفاعل المستخدم

عند تفاعل المستخدم.
عند تفاعل المستخدم:

مناسب لـ: عندما لا يمكن إزالة الموقع الإلكتروني بالكامل من الإنترنت، واخترت السماح للمستخدم باختيار المحتوى الذي يريده متاحًا بلا إنترنت. على سبيل المثال، فيديو على YouTube أو مقالة على هٰذه الصفحة: Wikipedia أو معرض معيّن على Flickr

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

document.querySelector('.cache-article').addEventListener('click', function (event) {
  event.preventDefault();

  var id = this.dataset.articleId;
  caches.open('mysite-article-' + id).then(function (cache) {
    fetch('/get-article-urls?id=' + id)
      .then(function (response) {
        // /get-article-urls returns a JSON-encoded array of
        // resource URLs that a given article depends on
        return response.json();
      })
      .then(function (urls) {
        cache.addAll(urls);
      });
  });
});

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

استجابة الشبكة

استجابة الشبكة
في حال استجابة الشبكة

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

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

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

self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.open('mysite-dynamic').then(function (cache) {
      return cache.match(event.request).then(function (response) {
        return (
          response ||
          fetch(event.request).then(function (response) {
            cache.put(event.request, response.clone());
            return response;
          })
        );
      });
    }),
  );
});

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

في trained-to-thrill، أستخدم هذا الإجراء ل تخزين صور Flickr.

بيانات قديمة أثناء إعادة التحقّق

Stale-while-revalidate
Stale-while-revalidate:

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

إذا كان هناك إصدار محفوظ مؤقتًا متاحًا، استخدِمه، ولكن ابحث عن تحديث في المرة القادمة.

self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.open('mysite-dynamic').then(function (cache) {
      return cache.match(event.request).then(function (response) {
        var fetchPromise = fetch(event.request).then(function (networkResponse) {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        return response || fetchPromise;
      });
    }),
  );
});

يشبه ذلك إلى حد كبير stale-while-revalidate في HTTP.

عند ظهور رسالة فورية

في رسالة الإشعار الفوري
في رسالة الإشعار الفوري:

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

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

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

يتم تعديل هذا الرمز في ذاكرة التخزين المؤقت قبل عرض إشعار:

self.addEventListener('push', function (event) {
  if (event.data.text() == 'new-email') {
    event.waitUntil(
      caches
        .open('mysite-dynamic')
        .then(function (cache) {
          return fetch('/inbox.json').then(function (response) {
            cache.put('/inbox.json', response.clone());
            return response.json();
          });
        })
        .then(function (emails) {
          registration.showNotification('New email', {
            body: 'From ' + emails[0].from.name,
            tag: 'new-email',
          });
        }),
    );
  }
});

self.addEventListener('notificationclick', function (event) {
  if (event.notification.tag == 'new-email') {
    // Assume that all of the resources needed to render
    // /inbox/ have previously been cached, e.g. as part
    // of the install handler.
    new WindowClient('/inbox/');
  }
});

تفعيل المزامنة في الخلفية

تفعيل المزامنة في الخلفية
تفعيل المزامنة في الخلفية

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

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

self.addEventListener('sync', function (event) {
  if (event.id == 'update-leaderboard') {
    event.waitUntil(
      caches.open('mygame-dynamic').then(function (cache) {
        return cache.add('/leaderboard.json');
      }),
    );
  }
});

الاحتفاظ بالبيانات في ذاكرة التخزين المؤقت

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

ولا يتم تحديد المبلغ الذي تحصل عليه. ويختلف ذلك حسب الجهاز وظروف التخزين. يمكنك معرفة عدد النقاط التي حصلت عليها من خلال:

if (navigator.storage && navigator.storage.estimate) {
  const quota = await navigator.storage.estimate();
  // quota.usage -> Number of bytes used.
  // quota.quota -> Maximum number of bytes available.
  const percentageUsed = (quota.usage / quota.quota) * 100;
  console.log(`You've used ${percentageUsed}% of the available storage.`);
  const remaining = quota.quota - quota.usage;
  console.log(`You can write up to ${remaining} more bytes.`);
}

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

لحلّ هذه المشكلة، يمكنك استخدام واجهة StorageManager:

// From a page:
navigator.storage.persist()
.then(function(persisted) {
  if (persisted) {
    // Hurrah, your data is here to stay!
  } else {
   // So sad, your data may get chucked. Sorry.
});

وبالطبع، يتعين على المستخدم منح الإذن. لإجراء ذلك، استخدِم Permissions API.

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

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

اقتراحات العرض: الاستجابة للطلبات

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

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

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

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

self.addEventListener('fetch', function (event) {
  // If a match isn't found in the cache, the response
  // will look like a connection error
  event.respondWith(caches.match(event.request));
});

…على الرغم من أنّك لا تحتاج في أغلب الأحيان إلى التعامل مع هذا الموقف تحديدًا، فإنّ Cache، الرجوع إلى الشبكة يتناول هذا الموقف.

الشبكة فقط

الشبكة فقط
الشبكة فقط:

مناسب لـ: العناصر التي لا تتوفّر لها مكافئ بلا إنترنت، مثل إشعارات "إحصاءات Google" وطلبات GET غير ذلك.

self.addEventListener('fetch', function (event) {
  event.respondWith(fetch(event.request));
  // or simply don't call event.respondWith, which
  // will result in default browser behavior
});

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

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

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

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

self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.match(event.request).then(function (response) {
      return response || fetch(event.request);
    }),
  );
});

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

ذاكرة التخزين المؤقت وسباق الشبكة

تنافس ذاكرة التخزين المؤقت والشبكة
ذاكرة التخزين المؤقت وسباق الشبكات

مثالي: مواد العرض الصغيرة التي تتتبّع مستوى أدائها على الأجهزة التي لديها وصول بطيء إلى القرص.

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

// Promise.race is no good to us because it rejects if
// a promise rejects before fulfilling. Let's make a proper
// race function:
function promiseAny(promises) {
  return new Promise((resolve, reject) => {
    // make sure promises are all promises
    promises = promises.map((p) => Promise.resolve(p));
    // resolve this promise as soon as one resolves
    promises.forEach((p) => p.then(resolve));
    // reject if all promises reject
    promises.reduce((a, b) => a.catch(() => b)).catch(() => reject(Error('All failed')));
  });
}

self.addEventListener('fetch', function (event) {
  event.respondWith(promiseAny([caches.match(event.request), fetch(event.request)]));
});

إعادة الشبكة إلى ذاكرة التخزين المؤقت

تعود الشبكة إلى ذاكرة التخزين المؤقت.
الشبكة تستخدِم ذاكرة التخزين المؤقت

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

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

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

self.addEventListener('fetch', function (event) {
  event.respondWith(
    fetch(event.request).catch(function () {
      return caches.match(event.request);
    }),
  );
});

ذاكرة التخزين المؤقت ثم الشبكة

ذاكرة التخزين المؤقت ثم الشبكة
ذاكرة التخزين المؤقت ثم الشبكة

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

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

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

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

الرمز في الصفحة:

var networkDataReceived = false;

startSpinner();

// fetch fresh data
var networkUpdate = fetch('/data.json')
  .then(function (response) {
    return response.json();
  })
  .then(function (data) {
    networkDataReceived = true;
    updatePage(data);
  });

// fetch cached data
caches
  .match('/data.json')
  .then(function (response) {
    if (!response) throw Error('No data');
    return response.json();
  })
  .then(function (data) {
    // don't overwrite newer network data
    if (!networkDataReceived) {
      updatePage(data);
    }
  })
  .catch(function () {
    // we didn't get cached data, the network is our last hope:
    return networkUpdate;
  })
  .catch(showErrorMessage)
  .then(stopSpinner);

الرمز البرمجي في مشغّل الخدمات:

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

self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.open('mysite-dynamic').then(function (cache) {
      return fetch(event.request).then(function (response) {
        cache.put(event.request, response.clone());
        return response;
      });
    }),
  );
});

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

عنصر احتياطي عام

الإجراء الاحتياطي العام
العنصر البديل العام:

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

مناسبة لـ: الصور الثانوية، مثل الصور الرمزية وطلبات POST التي تعذّر إرسالها والصفحة التي تعرض الرسالة "غير متوفّرة بلا اتصال بالإنترنت"

self.addEventListener('fetch', function (event) {
  event.respondWith(
    // Try the cache
    caches
      .match(event.request)
      .then(function (response) {
        // Fall back to network
        return response || fetch(event.request);
      })
      .catch(function () {
        // If both fail, show a generic fallback:
        return caches.match('/offline.html');
        // However, in reality you'd have many different
        // fallbacks, depending on URL and headers.
        // Eg, a fallback silhouette image for avatars.
      }),
  );
});

من المرجح أن يكون العنصر الذي تتراجع إليه هو تبعية التثبيت.

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

النماذج من جهة مشغّل الخدمات

إنشاء النماذج من جهة ServiceWorker
النماذج من جهة ServiceWorker:

مثالي: للصفحات التي لا يمكن تخزين استجابة الخادم مؤقتًا فيها.

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

importScripts('templating-engine.js');

self.addEventListener('fetch', function (event) {
  var requestURL = new URL(event.request.url);

  event.respondWith(
    Promise.all([
      caches.match('/article-template.html').then(function (response) {
        return response.text();
      }),
      caches.match(requestURL.path + '.json').then(function (response) {
        return response.json();
      }),
    ]).then(function (responses) {
      var template = responses[0];
      var data = responses[1];

      return new Response(renderTemplate(template, data), {
        headers: {
          'Content-Type': 'text/html',
        },
      });
    }),
  );
});

تجميع العناصر معًا

يمكنك استخدام أيّ من هذه الطرق. في الواقع، من المرجّح أن تستخدم العديد منها استنادًا إلى عنوان URL للطلب. على سبيل المثال، يستخدم trained-to-thrill ما يلي:

ما عليك سوى الاطّلاع على الطلب وتحديد الإجراء الذي تريد اتّخاذه:

self.addEventListener('fetch', function (event) {
  // Parse the URL:
  var requestURL = new URL(event.request.url);

  // Handle requests to a particular host specifically
  if (requestURL.hostname == 'api.example.com') {
    event.respondWith(/* some combination of patterns */);
    return;
  }
  // Routing for local URLs
  if (requestURL.origin == location.origin) {
    // Handle article URLs
    if (/^\/article\//.test(requestURL.pathname)) {
      event.respondWith(/* some other combination of patterns */);
      return;
    }
    if (/\.webp$/.test(requestURL.pathname)) {
      event.respondWith(/* some other combination of patterns */);
      return;
    }
    if (request.method == 'POST') {
      event.respondWith(/* some other combination of patterns */);
      return;
    }
    if (/cheese/.test(requestURL.pathname)) {
      event.respondWith(
        new Response('Flagrant cheese error', {
          status: 512,
        }),
      );
      return;
    }
  }

  // A sensible default pattern
  event.respondWith(
    caches.match(event.request).then(function (response) {
      return response || fetch(event.request);
    }),
  );
});

…وهكذا.

المساهمون

…للحصول على الرموز الجميلة:

شكرًا لجيمس بوسنيك على رصد العديد من الأخطاء المزعجة قبل أن أضغط على "نشر".

مراجع إضافية