دورة حياة عامل الخدمات

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

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

الغاية

يهدف نموذج رحلة المستخدِم إلى:

  • إتاحة استخدام التطبيق بلا إنترنت أولاً
  • السماح لمشغِّل خدمات جديد بالاستعداد بدون إيقاف مشغِّل الخدمات الحالي
  • تأكَّد من أنّ الصفحة ضمن النطاق تخضع لرقابة مشغّل الخدمات نفسه (أو لا يخضع لرقابة أي مشغّل خدمات) طوال الوقت.
  • تأكَّد من أنّ هناك نسخة واحدة فقط من موقعك الإلكتروني تعمل في الوقت نفسه.

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

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

وباختصار:

  • الحدث install هو أول حدث يتلقّاه عامل الخدمة، ولا يحدث إلا مرة واحدة.
  • يشير الوعد الذي يتم تمريره إلى installEvent.waitUntil() إلى مدة التثبيت ونجاحه أو تعذّره.
  • لن يتلقّى عامل الخدمة أحداثًا مثل fetch وpush إلى أن ينتهي تثبيته بنجاح ويصبح "نشطًا".
  • لن تمر عمليات جلب الصفحة تلقائيًا عبر مشغّل خدمات ما لم يتم جلب طلب الصفحة نفسه عبر مشغّل خدمات. لذلك، عليك إعادة تحميل الصفحة للاطّلاع على تأثيرات الخدمة العاملة.
  • يمكن لخدمة clients.claim() إلغاء هذا الإعداد التلقائي والتحكّم في الصفحات غير الخاضعة للتحكّم.

خذ رمز HTML هذا:

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

يسجِّل هذا الرمز عامل خدمة ويضيف صورة كلب بعد 3 ثوانٍ.

في ما يلي مشغّل الخدمة sw.js:

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

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

النطاق والتحكّم

النطاق التلقائي لتسجيل مشغِّل الخدمة هو ./ بالنسبة إلى عنوان URL للنص البرمجي. وهذا يعني أنّه في حال تسجيل مشغّل خدمات على //example.com/foo/bar.js، سيكون نطاق عمله التلقائي هو //example.com/foo/.

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

التنزيل والتحليل والتنفيذ

يتم تنزيل أوّل عامل خدمة عند الاتصال بـ .register(). إذا تعذّر تنزيل النص البرمجي أو تحليله أو إذا حدث خطأ في تنفيذه الأوّلي، سيتم رفض وعد التسجيل، وسيتم تجاهل عامل الخدمة.

تعرِض "أدوات مطوّري البرامج في Chrome" الخطأ في وحدة التحكّم، وفي قسم "عامل الخدمة" ضمن علامة تبويب التطبيق:

خطأ معروض في علامة التبويب &quot;أدوات مطوّري البرامج&quot; لمشغّل الخدمات

تثبيت

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

يُعدّ الحدث install فرصة لك لتخزين كل ما تحتاجه في ذاكرة التخزين المؤقت قبل أن تتمكّن من التحكّم في العملاء. يُعلم الوعد الذي ترسله إلى event.waitUntil() المتصفّح عند اكتمال عملية التثبيت وما إذا كانت ناجحة.

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

تفعيل

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

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

clients.claim

يمكنك التحكّم في العملاء غير الخاضعين للرقابة من خلال استدعاء clients.claim() في worker الخدمة بعد تفعيله.

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

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

تعديل الخدمة

وباختصار:

  • يتم إجراء تحديث في حال حدوث أي مما يلي:
    • رابط يؤدي إلى صفحة ضمن النطاق
    • الأحداث الوظيفية، مثل push وsync، ما لم يكن قد تم التحقّق من توفّر تحديث خلال آخر 24 ساعة
    • يتم استدعاء .register() فقط إذا تغيّر عنوان URL الخاص بعملية الخدمة. ومع ذلك، عليك تجنُّب تغيير عنوان URL الخاص بالعامل.
  • تتجاهل معظم المتصفّحات، بما في ذلك Chrome 68 والإصدارات الأحدث، تلقائيًا رؤوس التخزين المؤقت عند البحث عن تحديثات لنص عامل الخدمة المسجّل. ولا تزال هذه الطلبات تحترم رؤوس التخزين المؤقت عند جلب الموارد المحمَّلة داخل عامل خدمة من خلال importScripts(). يمكنك إلغاء هذا السلوك التلقائي من خلال ضبط الخيار updateViaCache عند تسجيل عامل الخدمة.
  • يُعتبر العامل في الخدمة محدَّثًا إذا كان مختلفًا عن العامل في الخدمة الذي يتوفّر حاليًا في المتصفّح. (نحن بصدد توسيع نطاق هذا الإجراء ليشمل النصوص البرمجية أو الوحدات المستورَدة أيضًا).
  • يتم تشغيل عامل الخدمة المعدَّل إلى جانب العامل الحالي، ويحصل على حدث install الخاص به.
  • إذا كان لدى العامل الجديد رمز حالة غير "حسن" (مثل 404)، أو تعذّر عليه تحليل البيانات، أو ظهرت رسالة خطأ أثناء التنفيذ، أو تم رفضه أثناء التثبيت، يتم تجاهل العامل الجديد، ولكن يبقى العامل الحالي نشطًا.
  • بعد التثبيت بنجاح، سيعمل العامل المعدَّل على wait إلى أن يتوقف العامل الحالي عن التحكّم في أي عملاء. (يُرجى العلم أنّ العملاء يتداخلون أثناء عملية إعادة التحميل).
  • يمنع self.skipWaiting() الانتظار، ما يعني أنّ عامل الخدمة يتم تفعيله فور انتهاء عملية التثبيت.

لنفترض أنّنا غيّرنا نص رمز الخدمة لكي يعرض صورة حصان بدلاً من صورة قطة:

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

اطّلِع على عرض توضيحي لما سبق. من المفترض أن تظهر لك صورة قطة. إليك السبب:

تثبيت

يُرجى العِلم أنّني غيّرت اسم ذاكرة التخزين المؤقت من static-v1 إلى static-v2. وهذا يعني أنّه يمكنني إعداد ذاكرة التخزين المؤقت الجديدة بدون استبدال العناصر في الذاكرة الحالية التي لا يزال يستخدمها العامل القديم للخدمة.

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

Waiting

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

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

أدوات المطوّر تعرِض عامل خدمة جديدًا في انتظار التحميل

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

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

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

تفعيل

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

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

في حال تم تمرير وعد إلى event.waitUntil()، سيتم تخزين الأحداث الوظيفية (fetch وpush وsync وما إلى ذلك) في ذاكرة التخزين المؤقت إلى أن يتم حلّ الوعد. وبالتالي، عند بدء حدث fetch، يكتمل التفعيل بالكامل.

تخطّي مرحلة الانتظار

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

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

لا يهمّ وقت الاتصال بـ skipWaiting()، ما دام ذلك خلال فترة الانتظار أو قبلها. من الشائع جدًا استدعاء هذا الإجراء في الحدث install:

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

ولكن قد تحتاج إلى استدعائه كنتيجة postMessage() إلى عامل الخدمة. على سبيل المثال، تريد skipWaiting() بعد تفاعل أحد المستخدِمين.

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

تحديثات يدوية

كما ذكرنا سابقًا، يبحث المتصفّح عن التحديثات تلقائيًا بعد عمليات التنقّل والأحداث الوظيفية، ولكن يمكنك أيضًا تشغيلها يدويًا:

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

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

تجنُّب تغيير عنوان URL لنص برمجي عامل الخدمة

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

وقد تواجهك مشكلة مثل هذه:

  1. يسجِّل index.html sw-v1.js كمشغّل خدمات.
  2. تُخزِّن sw-v1.js index.html وتُقدّمه، لذا يعمل بلا إنترنت أولاً.
  3. عدِّلت index.html لتسجيل sw-v2.js الجديد.

في حال اتّباع الخطوات أعلاه، لن يحصل المستخدم على sw-v2.js مطلقًا، لأنّ sw-v1.js يعرض الإصدار القديم من index.html من ذاكرته المؤقتة. لقد وضعت نفسك في موقف يتطلّب منك تعديل عامل الخدمة من أجل تعديل عامل الخدمة. يا للعجب.

ومع ذلك، في العرض الترويجي أعلاه، غيّرت عنوان URL لعامل الخدمة. ويمكنك التبديل بين الإصدارَين من أجل العرض الترويجي. لا أُجري ذلك في مرحلة الإنتاج.

تسهيل عملية التطوير

تم تصميم دورة حياة مشغّل الخدمة مع مراعاة المستخدم، ولكنّها تكون صعبة بعض الشيء أثناء التطوير. لحسن الحظ، هناك بعض الأدوات التي يمكن أن تساعدك:

التحديث عند إعادة التحميل

هذا هو المفضّل لدي.

أدوات المطوّرين تعرِض &quot;التحديث عند إعادة التحميل&quot;

يؤدي ذلك إلى تغيير دورة الحياة لتصبح ملائمة للمطوّرين. سيؤدي كل تنقّل إلى ما يلي:

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

وهذا يعني أنّك ستتلقّى آخر الأخبار عند الانتقال إلى أي صفحة (بما في ذلك إعادة تحميل الصفحة) بدون الحاجة إلى إعادة التحميل مرتين أو إغلاق علامة التبويب.

تخطّي الانتظار

أدوات المطوّرين تعرِض خيار &quot;تخطّي الانتظار&quot;

إذا كان لديك عامل في انتظار المعالجة، يمكنك النقر على "تخطّي الانتظار" في DevTools لترقيته على الفور إلى "نشط".

إعادة التحميل باستخدام مفتاح Shift

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

التعامل مع التعديلات

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

لذلك، لإتاحة أكبر عدد ممكن من الأنماط، يمكن رصد دورة التعديل بالكامل:

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

دورة الحياة مستمرة

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