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

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

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

الغاية

ويتمثل الغرض من دورة الحياة في ما يلي:

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

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

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

وباختصار:

  • الحدث 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; تعرض &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.
});

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

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