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

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

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

الهدف

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

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

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

أول عامل خدمات

وباختصار:

  • الحدث 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 بعد. يمكنك ظهور مشغّل الخدمات الجديد وهو ينتظر في علامة التبويب "التطبيق" (Application) في "أدوات مطوّري البرامج"، وذلك باتّباع الخطوات التالية:

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

حتى في حال فتح علامة تبويب واحدة فقط للعرض التوضيحي، لا تكفي إعادة تحميل الصفحة للسماح بتولي الإصدار الجديد. ويرجع ذلك إلى كيفية عمل عمليات التنقل في المتصفح. عند التنقّل، لا تتم إزالة الصفحة الحالية إلا بعد تلقّي عناوين الاستجابة، وقد تظل الصفحة الحالية ظاهرة إذا كانت الاستجابة تتضمّن عنوان 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; تعرض &quot;تخطّي الانتظار&quot;

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

إعادة التحميل باستخدام 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.
});

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

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