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

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

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

الهدف

الهدف من دورة الحياة هو:

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

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

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

وباختصار:

  • حدث 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() ضمن مشغّل الخدمات بعد تفعيله.

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

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

تحديث مشغّل الخدمات

وباختصار:

  • يتم تشغيل عملية التحديث في حال حدوث أيٍّ مما يلي:
    • التنقل إلى صفحة داخل النطاق.
    • أحداث صالحة، مثل 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

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

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

أدوات مطوّري البرامج تعرض مشغّل خدمات جديد ينتظر

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

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

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

تفعيل

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

في العرض التوضيحي أعلاه، أحتفظ بقائمة بذاكرات التخزين المؤقت التي أتوقع وجودها هناك، وأتخلص من أي ذاكرة تخزين مؤقت أخرى في حدث 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 للنص البرمجي لمشغِّل الخدمات

إذا كنت قد قرأت مشاركتي عن أفضل ممارسات التخزين المؤقت، ننصحك بمنح عنوان 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 من ذاكرة التخزين المؤقت الخاصة به. لقد وضعت نفسك في موقف تحتاج فيه إلى تحديث مشغّل الخدمات لكي تتمكّن من تحديث مشغّل الخدمات. Ew.

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

تسهيل التطوير

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

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

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

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

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

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

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

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

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

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

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

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

جارٍ التعامل مع التحديثات

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

وبالتالي، لتفعيل أكبر عدد ممكن من الأنماط، يمكن ملاحظة دورة التحديث بأكملها:

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.
});

تسير دورة الحياة إلى الأبد

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