عامل الخدمة في تايب سكريبت

عامل الخدمة في تايب سكريبت

تشرح هذه المقالة عمال الخدمة في تايب سكريبت۔

سنشرح عمال الخدمة في تايب سكريبت، مع أمثلة عملية۔

YouTube Video

عامل الخدمة في تايب سكريبت

عامل الخدمة هو "وكيل طلبات" يجلس بين المتصفح والشبكة۔ يمكّن اعتراض الجلب، التحكم في التخزين المؤقت، دعم العمل دون اتصال، والمعالجة في الخلفية (المزامنة والإشعارات)۔ استخدام تايب سكريبت يوفر أمان الأنواع ويزيد من سهولة الصيانة۔

إعداد تايب سكريبت

tsconfig.json (تمكين أنواع WebWorker)

دعنا نلقي نظرة على مثال لتمكين نوع WebWorker في tsconfig.json۔

 1{
 2  "compilerOptions": {
 3    "target": "ES2020",
 4    "module": "ES2020",
 5    "lib": ["ES2020", "WebWorker"],
 6    "moduleResolution": "Bundler",
 7    "strict": true,
 8    "noEmitOnError": true,
 9    "outDir": "out",
10    "skipLibCheck": true
11  },
12  "include": ["sw.ts"]
13}
  • بإضافة WebWorker إلى مصفوفة lib، يمكنك استخدام أنواع مثل ServiceWorkerGlobalScope۔
  • DOM و WebWorker لهما أنواع مختلفة، لذلك من الشائع فصل إعدادات tsconfig.json لمتصفح الويب (التطبيق الرئيسي) وService Worker
  • يتم في النهاية إخراج ملفات Service Worker إلى مسار يتطابق مع النطاق (غالبًا جذر الموقع /sw.js
  • لأسباب أمنية، لا يمكن تشغيل عمال الخدمة إلا عبر HTTPS (أو على localhost

كود التسجيل في جهة المتصفح

register-sw.ts

1// register-sw.ts
2async function registerServiceWorker() {
3  if (!('serviceWorker' in navigator)) return;
4
5  try {
6    const registration = await navigator.serviceWorker.register(
7      '/sw.js', { scope: '/' }
8    );
  • تقوم هذه العملية بتسجيل Service Worker۔ scope يشير إلى مجموعة المسارات التي يمكن أن يتحكم بها Service Worker۔ على سبيل المثال، إذا وضعت /sw.js مباشرة في الجذر وحددت scope إلى دليل الجذر (/)، يمكنك التحكم بجميع موارد الموقع بأكمله۔ من ناحية أخرى، إذا حددت دليلاً معيناً مثل /app/، فسيتم التحكم فقط في المحتويات الموجودة تحت ذلك الدليل۔
1    // If there's a waiting worker, notify the user.
2    if (registration.waiting) {
3      promptUserToUpdate(registration);
4    }
  • waiting يشير إلى الحالة التي تم فيها تثبيت Service Worker جديد وينتظر التفعيل۔ في هذه المرحلة، لا تزال الصفحات الحالية تحت سيطرة Service Worker القديم، ولذلك من المعتاد طلب تأكيد من المستخدم، وبعد الموافقة يتم استدعاء skipWaiting() لتفعيل Service Worker الجديد فوراً۔ يتيح لك هذا تطبيق العمليات الحديثة دون الحاجة لانتظار إعادة تحميل الصفحة التالية۔
 1    // When a new SW is installing, monitor its state changes
 2    registration.addEventListener('updatefound', () => {
 3      const newWorker = registration.installing;
 4      if (!newWorker) return;
 5      newWorker.addEventListener('statechange', () => {
 6        if (newWorker.state === 'installed' &&
 7            navigator.serviceWorker.controller) {
 8          // New content available, prompt the user
 9          promptUserToUpdate(registration);
10        }
11      });
12    });
  • يتم تفعيل الحدث updatefound عند بدء تثبيت Service Worker جديد۔ عندما يحدث هذا الحدث، يتم تعيين عامل جديد في registration.installing، لذا من خلال مراقبة تغييرات الحالة statechange، يمكنك معرفة متى اكتمل التثبيت (installed)۔ علاوة على ذلك، إذا كان navigator.serviceWorker.controller موجودًا، فهذا يعني أن خدمة عامل قديم يتحكم بالفعل في الصفحة، لذلك هذه فرصة لإبلاغ المستخدم بوجود إصدار جديد.۔
1    // When the active worker changes (e.g., after skipWaiting), reload if desired
2    navigator.serviceWorker.addEventListener('controllerchange', () => {
3      // Optionally reload to let the new SW take over
4      window.location.reload();
5    });
6  } catch (err) {
7    console.error('Service Worker registration failed: ', err);
8  }
9}
  • يتم تفعيل حدث controllerchange في اللحظة التي يبدأ فيها Service Worker الجديد بالتحكم في الصفحة الحالية۔ إعادة تحميل الصفحة في هذه اللحظة ستنفذ فورًا استراتيجيات التخزين المؤقت والمعالجات الجديدة۔ مع ذلك، يمكن أن تؤثر إعادة التحميل التلقائية سلباً على تجربة المستخدم، ولذلك يفضل إعادة التحميل بعد الحصول على موافقة المستخدم۔
1function promptUserToUpdate(reg: ServiceWorkerRegistration) {
2  // Show UI to user. If user accepts:
3  if (reg.waiting) {
4    reg.waiting.postMessage({ type: 'SKIP_WAITING' });
5  }
6}
7
8registerServiceWorker();
  • من خلال استلام Service Worker رسالة postMessage({ type: 'SKIP_WAITING' }) من العميل ثم استدعاء self.skipWaiting()، يمكنك تحفيز التحديث مباشرة۔

إعلان النطاق في sw.ts

بعد ذلك، دعنا ننظر إلى مثال نموذجي لعامل خدمة ينفذ تخزين app shell في الذاكرة۔

عند استخدام عمال الخدمة في تايب سكريبت، من المفيد تعيين النوع الصحيح لـ self۔

1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
  • في TypeScript، يتم التعامل مع self بشكل افتراضي على أنه any، لذا من دون تحديد نوع إضافي، لن تحصل على اكتمال الأنواع أو فحص الأنواع لواجهات برمجة التطبيقات الخاصة بـ Service Worker مثل skipWaiting() أو clients
  • تحديد ServiceWorkerGlobalScope يتيح إكمال تلقائي، ويمنع سوء الاستخدام، ويسمح بتطوير أكثر أمانًا منفصلًا عن نصوص DOM العادية۔

عامل خدمة أساسي (تثبيت/تفعيل/جلب)

يوضح إدارة إصدار التخزين المؤقت بطريقة بسيطة، والتخزين المسبق عند التثبيت، وحذف التخزينات المؤقتة القديمة عند التفعيل، واستراتيجيات التخزين عند الجلب (الأولوية للتخزين المؤقت للملفات الثابتة، والأولوية للشبكة لـ APIs)۔

sw.ts (إعداد أساسي + هيكل التخزين المؤقت)

 1const CACHE_NAME = 'app-shell-v1';
 2const STATIC_ASSETS = [
 3  '/',
 4  '/index.html',
 5  '/styles.css',
 6  '/main.js',
 7  '/fallback.png'
 8];
 9
10self.addEventListener('install', (event: ExtendableEvent) => {
11  // Pre-cache application shell
12  event.waitUntil(
13    caches.open(CACHE_NAME)
14      .then(cache => cache.addAll(STATIC_ASSETS))
15      // Activate immediately (optional: coordinate with client)
16      .then(() => self.skipWaiting())
17  );
18});
  • خلال حدث install، يتم تخزين الموارد الثابتة للتطبيق (App Shell) مسبقًا في ذاكرة الكاش.۔ من خلال استدعاء self.skipWaiting()، يتم تفعيل Service Worker الجديد على الفور، مما يجعل آخر ذاكرة تخزين مؤقتة متاحة دون انتظار الوصول التالي.۔
 1self.addEventListener('activate', (event: ExtendableEvent) => {
 2  // Clean up old caches and take control of clients immediately
 3  event.waitUntil(
 4    caches.keys().then(keys =>
 5      Promise.all(keys
 6        .filter(key => key !== CACHE_NAME)
 7        .map(key => caches.delete(key)))
 8    ).then(() => self.clients.claim())
 9  );
10});
  • في حدث activate، يتم حذف الإصدارات القديمة من الكاش، ويتم تحديث Service Worker باستمرار.۔ علاوة على ذلك، من خلال استدعاء self.clients.claim()، يمكن لـ Service Worker الجديد التحكم في جميع العملاء دون الحاجة إلى إعادة تحميل الصفحة.۔
 1self.addEventListener('fetch', (event: FetchEvent) => {
 2  const request = event.request;
 3  const url = new URL(request.url);
 4
 5  // Navigation requests (SPA) -> network-first with fallback to cached index.html
 6  if (request.mode === 'navigate') {
 7    event.respondWith(
 8      fetch(request).catch(() => caches.match('/index.html') as Promise<Response>)
 9    );
10    return;
11  }
12
13  // Simple API routing: network-first for /api/
14  if (url.pathname.startsWith('/api/')) {
15    event.respondWith(networkFirst(request));
16    return;
17  }
18
19  // Static assets: cache-first
20  event.respondWith(cacheFirst(request));
21});
  • في fetch, يمكنك اعتراض الطلبات والتحكم في الاستجابة۔ يمكنك تنفيذ استراتيجيات مثل "الأولوية للتخزين المؤقت" أو "الأولوية للشبكة"، وهي مفيدة لدعم العمل دون اتصال وتحسين الأداء۔
1self.addEventListener('message', (event: ExtendableMessageEvent) => {
2  const data = (event as any).data;
3  if (!data) return;
4
5  if (data.type === 'SKIP_WAITING') {
6    // Force the waiting service worker to become active
7    self.skipWaiting();
8  }
9});
  • عند استلام SKIP_WAITING، يؤدي استدعاء self.skipWaiting() إلى تفعيل Service Worker المنتظر على الفور۔ وبالتالي، سيطبق الإصدار الجديد من الطلب التالي دون الحاجة لإعادة تحميل الصفحة۔

نظرة عامة على استراتيجيات التخزين المؤقت العملية

cache-first

"الأولوية للتخزين المؤقت (Cache-first)" تتحقق أولاً من التخزين المؤقت وتعيد الاستجابة فور توفرها۔ إذا لم تكن متوفرة، يتم الجلب من الشبكة وحفظ النتيجة في التخزين المؤقت۔ يناسب هذا الملفات الثابتة۔

 1async function cacheFirst(request: Request): Promise<Response> {
 2  const cache = await caches.open(CACHE_NAME);
 3  const cached = await cache.match(request);
 4  if (cached) {
 5    return cached;
 6  }
 7  const response = await fetch(request);
 8  if (response && response.ok) {
 9    cache.put(request, response.clone());
10  }
11  return response;
12}
  • يوضح هذا الكود تطبيق إستراتيجية الأولوية للتخزين المؤقت۔ إذا كان هناك نسخة في التخزين المؤقت يُعيدها، وإذا لم تكن موجودة يجلبها من الشبكة ويحفظها في التخزين المؤقت۔ مناسب للموارد الثابتة التي نادرًا ما تتغير، مثل الصور أو ملفات CSS۔

network-first

"الأولوية للشبكة (Network-first)" تحاول الجلب من الشبكة أولًا وتلجأ للتخزين المؤقت عند الفشل۔ هذه الإستراتيجية مناسبة لـ APIs حيث freshness ضروري۔

 1async function networkFirst(request: Request): Promise<Response> {
 2  const cache = await caches.open(CACHE_NAME);
 3  try {
 4    const response = await fetch(request);
 5    if (response && response.ok) {
 6      cache.put(request, response.clone());
 7    }
 8    return response;
 9  } catch (err) {
10    const cached = await cache.match(request);
11    if (cached) return cached;
12    return new Response(JSON.stringify({ error: 'offline' }), {
13      status: 503,
14      headers: { 'Content-Type': 'application/json' }
15    });
16  }
17}
  • يوضح هذا الكود تطبيق إستراتيجية الأولوية للشبكة۔ إذا تم الحصول على استجابة من الشبكة، يتم حفظها في التخزين المؤقت، وإذا فشلت تُعاد النسخة المخزنة۔ هي مناسبة للموارد التي تحتاج إلى بيانات محدثة، مثل مقالات الأخبار أو ردود APIs۔

stale-while-revalidate

stale-while-revalidate تُعيد الكاش أولاً وتقوم في الوقت نفسه بتحديثه من الشبكة في الخلفية.۔ يوازن هذا بين سرعة الاستجابة وحداثة البيانات۔

 1async function staleWhileRevalidate(request: Request, cacheName = CACHE_NAME): Promise<Response> {
 2  const cache = await caches.open(cacheName);
 3  const cachedResponse = await cache.match(request);
 4  const networkFetch = fetch(request).then(networkResponse => {
 5    if (networkResponse && networkResponse.ok) {
 6      cache.put(request, networkResponse.clone());
 7    }
 8    return networkResponse;
 9  }).catch(() => undefined);
10
11  // Return cached immediately if exists, otherwise wait network
12  return cachedResponse || (await networkFetch) || new Response('offline', { status: 503 });
13}
  • يعيد هذا الكود نتيجة التخزين المؤقت مباشرة إذا كانت متاحة، بينما يجلب بيانات جديدة من الشبكة في الخلفية لتحديث التخزين المؤقت۔ يوفر استجابات سريعة للمستخدمين ويستخدم المحتوى المحدث للوصول القادم، مما يجعله مناسبًا للواجهات أو توصيل البيانات الخفيفة۔

تحسين سير التحديث (إشعار التحديث وإعادة التحميل الآمن)

تحديثات Service Worker ليست فورية؛ سيظل الإصدار الجديد في وضع الانتظار حتى يتم إغلاق علامات التبويب المفتوحة الحالية۔

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

قم بإخطار العميل من جهة Service Worker عندما يكون الإصدار الجديد جاهزاً.

 1// In sw.ts: after 'activate' or when new version is ready, broadcast a message
 2async function notifyClientsUpdated() {
 3  const all = await self.clients.matchAll({ type: 'window' });
 4  for (const client of all) {
 5    client.postMessage({ type: 'SW_UPDATED' });
 6  }
 7}
 8
 9// e.g., call this at the end of 'activate'
10self.addEventListener('activate', (event) => {
11  event.waitUntil((async () => {
12    if ('navigationPreload' in self.registration) {
13      await self.registration.navigationPreload.enable();
14    }
15    // cache cleanup
16    const cacheNames = await caches.keys();
17    await Promise.all(
18      cacheNames.map((name) => {
19        if (name !== CACHE_NAME) {
20          return caches.delete(name);
21        }
22      })
23    );
24
25    await self.clients.claim();
26    await notifyClientsUpdated();
27  })());
28});
  • في هذا الكود، يتم استدعاء notifyClientsUpdated في نهاية حدث activate لإخطار جميع العملاء المتصلين بأن الإصدار الجديد جاهز۔ clients.claim() هي طريقة تجلب فوراً الصفحات المفتوحة حالياً (العملاء) تحت سيطرة Service Worker المفعّل حديثاً۔ عادةً ما يبدأ Service Worker بالتحكم في الصفحة فقط عند التحميل التالي، ولكن باستخدام clients.claim() يمكنك التحكم بالصفحة على الفور دون إعادة تحميل۔

عرض واجهة مستخدم إشعار التحديث لدى العميل، وإعادة التحميل بناءً على إجراء المستخدم

1// in app startup
2navigator.serviceWorker.addEventListener('message', (e) => {
3  if (e.data?.type === 'SW_UPDATED') {
4    // Show a non-intrusive toast or banner: "New version available"
5    // When user clicks "Reload", call:
6    window.location.reload();
7  }
8});
  • يتلقى العميل SW_UPDATED من خلال حدث message ويعرض إشعار التحديث في واجهة المستخدم۔ عندما يختار المستخدم إعادة التحميل، يتم تنفيذ window.location.reload()، ويتم تحديث ملفات HTML وCSS وغيرها من الموارد القديمة على الصفحة إلى أحدث إصدار.۔ هذا يضمن أن الكاش وسيطرة Service Worker التي تم تفعيلها بواسطة clients.claim() تنعكس على الصفحة بالكامل.۔

البديل عند العمل دون اتصال

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

مثال على التنفيذ

 1// sw.ts
 2const CACHE_NAME = 'app-cache-v1';
 3
 4// Cache offline.html during install
 5self.addEventListener('install', (event) => {
 6  event.waitUntil((async () => {
 7    const cache = await caches.open(CACHE_NAME);
 8    await cache.addAll(['/offline.html']);
 9  })());
10});
11
12// Handle fetch requests
13self.addEventListener('fetch', (event) => {
14  const request = event.request;
15
16  // Navigation requests (e.g., page transitions)
17  if (request.mode === 'navigate') {
18    event.respondWith((async () => {
19      try {
20        // Try to fetch from the network as usual
21        return await fetch(request);
22      } catch (err) {
23        // On failure, return offline fallback page
24        const cache = await caches.open(CACHE_NAME);
25        return await cache.match('/offline.html') as Response;
26      }
27    })());
28  }
29});
  • قم بتخزين /offline.html مسبقًا خلال حدث install حتى يمكنك إعادة صفحة أساسية عندما تكون الشبكة غير متوفرة۔
  • في حدث fetch، يمكنك مراقبة طلبات التنقل باستخدام request.mode === 'navigate' واستهداف انتقالات الصفحات بشكل خاص.۔
  • الرجوع إلى /offline.html عند فشل الشبكة، للتأكد من ظهورها حتى بدون اتصال۔

المراسلات بين العميل وService Worker

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

مثال على كود

  1. تعريف أنواع الرسائل
1type SwToClient =
2  | { type: 'SW_READY' }
3  | { type: 'SW_UPDATED' }
4  | { type: 'CACHE_CLEARED' }
5  | { type: 'PING'; ts: number };
6
7type ClientToSw =
8  | { type: 'CLEAR_CACHE' }
9  | { type: 'PING'; ts: number };
  • SwToClient هو نوع الرسالة المرسلة من عامل الخدمة إلى العميل۔
  • ClientToSw هو نوع الرسالة المرسلة من العميل إلى عامل الخدمة۔
  • يتيح لك ذلك توضيح أنواع الأحداث التي يمكن تبادلها من خلال الاتصال الثنائي الاتجاه۔
  1. المعالجة من جهة عامل الخدمة
 1self.addEventListener('message', (event) => {
 2  const data = event.data as ClientToSw;
 3  if (data?.type === 'CLEAR_CACHE') {
 4    event.waitUntil((async () => {
 5      const keys = await caches.keys();
 6      await Promise.all(keys.map((k) => caches.delete(k)));
 7      await broadcast({ type: 'CACHE_CLEARED' });
 8    })());
 9  } else if (data?.type === 'PING') {
10    event.source?.postMessage({ type: 'PING', ts: data.ts } as SwToClient);
11  }
12});
  • يتلقى عامل الخدمة الرسائل من العميل ويقوم بالمعالجة بناءً على نوع الرسالة۔
  • بالنسبة لـ CLEAR_CACHE، يتم حذف التخزين المؤقت ثم إخطار جميع العملاء بـ CACHE_CLEARED۔
  • بالنسبة لـ PING، يرد على العميل الأصلي برسالة PING تتضمن الطابع الزمني۔
  1. إخطار جميع العملاء من عامل الخدمة
1async function broadcast(msg: SwToClient) {
2  const clients = await self.clients.matchAll({ includeUncontrolled: true });
3  for (const c of clients) c.postMessage(msg);
4}
  • استخدم clients.matchAll لجلب جميع تبويبات النوافذ۔
  • من خلال إرسال postMessage لكلٍ منهم، يمكنك بث الرسائل۔
  • يمكن استخدام ذلك لإشعارات التحديث (مثل SW_UPDATED) وإشعارات الأخطاء۔
  1. المعالجة من جهة العميل
1navigator.serviceWorker.controller?.postMessage({
2  type: 'PING',
3  ts: Date.now()
4} as ClientToSw);
  • من خلال إرسال PING من العميل واستلام الرد من Service Worker، يمكنك التأكد من أن الاتصال ثنائي الاتجاه يعمل بشكل صحيح.۔ هذا يسهل اختبار حالات الاتصال ومعالجة الرسائل.۔
 1navigator.serviceWorker.addEventListener('message', (e) => {
 2  const msg = e.data as SwToClient;
 3  switch (msg.type) {
 4    case 'SW_READY':
 5      console.log('Service Worker is ready');
 6      // Example: hide loading spinner or enable offline UI
 7      break;
 8    case 'SW_UPDATED':
 9      console.log('A new version of the Service Worker is available');
10      // Example: show update notification or reload prompt
11      const shouldReload = confirm('A new version is available. Reload now?');
12      if (shouldReload) {
13        window.location.reload();
14      }
15      break;
16    case 'CACHE_CLEARED':
17      console.log('Cache cleared');
18      // Example: show confirmation message to user
19      alert('Cache has been successfully cleared.');
20      break;
21    case 'PING':
22      console.log(`Received PING response, ts=${msg.ts}`);
23      break;
24  }
25});
  • {^ i18n_speak クライアント側では Service Worker から送信されるメッセージを受信し、種類に応じて処理を分岐します。SW_READY は初期化完了、SW_UPDATED は新バージョン検出、CACHE_CLEARED はキャッシュ削除完了、PING は通信確認を示します。各メッセージに応じて、UI の更新やリロード、通知表示などを行います。^}

فوائد المراسلة باستخدام الأنواع

  • استخدام الرسائل المعرفة بأنواع يجعل من الواضح أي الرسائل يمكن إرسالها واستلامها كما أن الإكمال التلقائي والتحقق من الأنواع يحسن الأمان۔
  • postMessage تتيح تواصل فردي, وbroadcast تتيح تواصل من طرف إلى عدة أطراف۔
  • يمكنك بسهولة تنفيذ ميزات أساسية مثل إشعارات التحديث (SW_UPDATED)، إدارة التخزين المؤقت (CACHE_CLEARED)، والتحقق من الحالة (PING

الملخص

  • استخدام تايب سكريبت يضيف أمان أنواع لاستدعاءات عامل الخدمة والمراسلة، مما يعزز الكفاءة وسهولة الصيانة بشكل كبير۔
  • فهم أحداث دورة حياة install, activate, وfetch، واختيار استراتيجية التخزين المؤقت المناسبة (مثل Cache-first أو Network-first) لكل حالة يؤدي إلى تجربة مستخدم أفضل۔
  • بالنسبة للتشغيل، من الضروري فهم إدارة إصدار التخزين المؤقت وتدفقات التحديث (updatefound, waiting, SKIP_WAITING, وما إلى ذلك)۔
  • من خلال اعتماد رسائل نمطية في الاتصال بين العميل وService Worker، يمكنك منع سوء التنفيذ وإرساء نظام سهل التوسعة والصيانة على المدى الطويل۔

يمكنك متابعة المقالة أعلاه باستخدام Visual Studio Code على قناتنا على YouTube.۔ يرجى التحقق من القناة على YouTube أيضًا.۔

YouTube Video