الكاش · فصل ٦

مثال حقيقي: الكاش في مذكرات سعيد

build id · cache stamp · freshness · deploy flow · offline fallback
🎯 الهدف: تفهم الـ Service Worker اللي شغال على الموقع ده، وإزاي بيعرف إن الصفحة المحفوظة من deploy قديم.

الموقع اللي إنت بتقرا عليه دلوقتي PWA. الكود في src/pwa/sw.js، والإعدادات في src/lib/pwa.ts. هنفكّكه هنا.

٣ أنواع ملفات، ٣ قرارات

النوعالاستراتيجيةالصلاحية
صفحات المذكرات (HTML)Cache First بصلاحيةلحد أول deploy جديد، وبحد أقصى ٧ أيام
_astro/ (CSS و JS والخطوط)Cache Firstمفيش، لأن الاسم فيه hash. وحد أقصى ٢٠٠ ملف
فهرس البحث والأيقوناتStale-While-Revalidateمفيش، بيتحدث في الخلفية كل مرة

الصفحات: Cache First بس “طازة”

قرار الـ SW لما تفتح صفحة مذكرة
  1. فتحت صفحة ← في الكاش؟
  2. في الكاش؟ ← طازة؟: أيوه
  3. في الكاش؟ ← 📴 صفحة offline: لأ + مفيش نت
  4. طازة؟ ← ⚡ اعرضها فورًا: أيوه
  5. طازة؟ ← النت: لأ
  6. النت ← الجديدة + احفظها: رد
  7. النت ← النسخة القديمة: فشل أو اتأخر

لو الصفحة مش موجودة والنت شغال، بتتجاب من النت وتتحفظ عادي. الرسمة بتوضح الحالة الصعبة بس.

إزاي بيعرف إن الصفحة من deploy قديم؟

فكرة من ٣ حتت: رقم لكل build، مكتوب في sw.js، ومختوم على كل صفحة.

من git push لحد ما الصفحة تتحدث
  1. git push ← GitHub Actions: build
  2. GitHub Actions ← sw.js جديد: يكتب الرقم جوّاه
  3. sw.js جديد ← المتصفح: اتغير؟ يعمل install
  4. المتصفح ← صفحة محفوظة: SW جديد يقارن
  5. صفحة محفوظة ← تتجاب من النت: الختم قديم: تتحدث
src/lib/pwa.ts
// على GitHub Actions = الـ commit hash، وعلى جهازك = الوقتexport const BUILD_ID = process.env.GITHUB_SHA?.slice(0, 12) ?? Date.now().toString(36);

وقت الـ build، src/pages/sw.js.ts بيبدّل __BUILD__ في الكود بالرقم ده. فكل deploy بيطلّع sw.js مختلف، والمتصفح بيلاحظ. وكل صفحة بتتحفظ بختم:

src/pwa/sw.js
async function stamp(res) {  const headers = new Headers(res.headers);  headers.set("x-sw-cached-at", String(Date.now()));  headers.set("x-sw-build", BUILD);  return new Response(await res.blob(), { status: res.status, statusText: res.statusText, headers });}function isFresh(res) {  if (res.headers.get("x-sw-build") !== BUILD) return false; // من deploy قديم  return Date.now() - Number(res.headers.get("x-sw-cached-at")) < PAGE_MAX_AGE;}

ليه stamp بيعمل Response جديد؟

لو السيرفر عمل redirect (مثلًا /docker ← /docker/)، الرد بيبقى redirected: true. المتصفح بيرفض يستخدم رد redirected كرد على navigation جاي من Service Worker. نسخ الـ body في new Response(...) بيعمل رد جديد نضيف، ومعاه بنحط الختم.

🧪 Lab: شوفه بعينك

  1. افتح الموقع، وبعدين DevTools ← Application ← Cache storage. هتلاقي pages-v1 و assets-v1 و static-v1 و shell-....
  2. افتح أي صفحة في pages-v1، وبص على الـ headers: هتلاقي x-sw-build و x-sw-cached-at.
  3. في Service workers علّم على Offline، وافتح صفحة زرتها قبل كده: هتفتح. وافتح صفحة جديدة: هتلاقي صفحة “النت مقطوع” وفيها لستة اللي محفوظ عندك.

خلّصت الفصل لما

🧠 اختبر نفسك
١) نزّلت deploy جديد. الصفحات المحفوظة اتمسحت؟
لأ. بقت قديمة بس، فبتتحدث لما تفتحها والنت شغال. offline بتفضل تفتح.
٢) ليه ملفات _astro مالهاش صلاحية؟
الاسم فيه hash من المحتوى. أي تعديل بيطلّع اسم جديد، فالقديم عمره ما بيتغير.
٣) ليه فهرس البحث SWR مش Cache First؟
اسمه ثابت ومحتواه بيتغير مع كل مذكرة. SWR بيدّيك سرعة ويحدّث في الخلفية.