Redis والتخزين المؤقت المتقدم

التخزين المؤقت من جانب العميل

18 دقيقة الدرس 19 من 30

التخزين المؤقت من جانب العميل

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

أنواع تخزين المتصفح

توفر المتصفحات واجهات برمجة تطبيقات تخزين متعددة بخصائص مختلفة لحالات استخدام مختلفة.

خيارات التخزين: ذاكرة المتصفح المؤقتة (ذاكرة HTTP المؤقتة)، localStorage (10 ميجابايت، دائم)، sessionStorage (5 ميجابايت، نطاق علامة التبويب)، IndexedDB (غير محدود، بيانات منظمة)، Service Workers (دعم عدم الاتصال).

ذاكرة المتصفح المؤقتة

يخزن المتصفح الموارد تلقائيًا مؤقتًا بناءً على رؤوس ذاكرة HTTP المؤقتة. هذه هي طريقة التخزين المؤقت الأكثر كفاءة للأصول الثابتة.

// يحدد الخادم رؤوس الذاكرة المؤقتة\nres.setHeader('Cache-Control', 'public, max-age=31536000, immutable');\nres.setHeader('ETag', 'v1.2.3');\n\n// يقوم المتصفح تلقائيًا بـ:\n// 1. تخزين المورد مؤقتًا\n// 2. التقديم من الذاكرة المؤقتة في الطلبات اللاحقة\n// 3. التحقق مع الخادم عندما تنتهي صلاحية الذاكرة المؤقتة\n\n// فرض التحقق من الذاكرة المؤقتة:\nfetch('/api/data', {\n  cache: 'no-cache'  // التحقق دائمًا مع الخادم\n});\n\n// فرض تجاوز الذاكرة المؤقتة:\nfetch('/api/data', {\n  cache: 'reload'  // تخطي الذاكرة المؤقتة تمامًا\n});

واجهة برمجة تطبيقات localStorage

يوفر localStorage تخزينًا دائمًا لأزواج المفتاح والقيمة يستمر بعد إعادة تشغيل المتصفح. يتم تخزين البيانات كسلاسل نصية.

// تخزين البيانات\nlocalStorage.setItem('username', 'john_doe');\n\n// تخزين الكائنات (تسلسل JSON)\nconst user = { id: 123, name: 'John' };\nlocalStorage.setItem('user', JSON.stringify(user));\n\n// استرجاع البيانات\nconst username = localStorage.getItem('username');\nconst user = JSON.parse(localStorage.getItem('user'));\n\n// إزالة عنصر\nlocalStorage.removeItem('username');\n\n// مسح الكل\nlocalStorage.clear();\n\n// التحقق من وجود المفتاح\nif (localStorage.getItem('token')) {\n  console.log('المستخدم مصادق عليه');\n}
نصيحة: احرص دائمًا على تغليف JSON.parse() في try-catch لمعالجة البيانات التالفة بسلاسة.

فئة مساعدة لـ localStorage

أنشئ فئة أداة مساعدة لتبسيط عمليات localStorage مع دعم انتهاء الصلاحية.

class LocalCache {\n  static set(key, value, ttlMinutes = null) {\n    const item = {\n      value,\n      timestamp: Date.now(),\n      ttl: ttlMinutes ? ttlMinutes * 60 * 1000 : null\n    };\n    localStorage.setItem(key, JSON.stringify(item));\n  }\n  \n  static get(key) {\n    const itemStr = localStorage.getItem(key);\n    if (!itemStr) return null;\n    \n    try {\n      const item = JSON.parse(itemStr);\n      \n      // التحقق من انتهاء الصلاحية\n      if (item.ttl) {\n        const age = Date.now() - item.timestamp;\n        if (age > item.ttl) {\n          localStorage.removeItem(key);\n          return null;\n        }\n      }\n      \n      return item.value;\n    } catch (e) {\n      return null;\n    }\n  }\n  \n  static remove(key) {\n    localStorage.removeItem(key);\n  }\n  \n  static clear() {\n    localStorage.clear();\n  }\n}\n\n// الاستخدام\nLocalCache.set('products', productList, 30); // 30 دقيقة\nconst products = LocalCache.get('products');

واجهة برمجة تطبيقات sessionStorage

sessionStorage مشابه لـ localStorage ولكن يتم مسح البيانات عند إغلاق علامة التبويب. مثالي للبيانات المؤقتة.

// تخزين الجلسة - يُمسح عند إغلاق علامة التبويب\nsessionStorage.setItem('cartId', 'abc123');\nsessionStorage.setItem('searchQuery', 'laptops');\n\n// الاسترجاع\nconst cartId = sessionStorage.getItem('cartId');\n\n// حالة الاستخدام: نماذج متعددة الخطوات\nfunction saveFormStep(step, data) {\n  sessionStorage.setItem(`form_step_${step}`, JSON.stringify(data));\n}\n\nfunction getFormStep(step) {\n  const data = sessionStorage.getItem(`form_step_${step}`);\n  return data ? JSON.parse(data) : null;\n}\n\n// المسح عند اكتمال النموذج\nfunction clearFormData() {\n  for (let i = 1; i <= 5; i++) {\n    sessionStorage.removeItem(`form_step_${i}`);\n  }\n}
localStorage مقابل sessionStorage: استخدم localStorage للبيانات الدائمة (تفضيلات المستخدم، بيانات API المخزنة مؤقتًا). استخدم sessionStorage للبيانات المؤقتة (حالة النموذج، تقدم المعالج).

التخزين المؤقت في IndexedDB

IndexedDB هي قاعدة بيانات NoSQL قوية في المتصفح، مثالية لتخزين كميات كبيرة من البيانات المنظمة.

// فتح قاعدة البيانات\nfunction openDB() {\n  return new Promise((resolve, reject) => {\n    const request = indexedDB.open('AppCache', 1);\n    \n    request.onerror = () => reject(request.error);\n    request.onsuccess = () => resolve(request.result);\n    \n    request.onupgradeneeded = (event) => {\n      const db = event.target.result;\n      \n      // إنشاء مخزن الكائنات\n      if (!db.objectStoreNames.contains('apiCache')) {\n        const store = db.createObjectStore('apiCache', { keyPath: 'url' });\n        store.createIndex('timestamp', 'timestamp');\n      }\n    };\n  });\n}\n\n// تخزين استجابة API\nasync function cacheAPIResponse(url, data) {\n  const db = await openDB();\n  const tx = db.transaction('apiCache', 'readwrite');\n  const store = tx.objectStore('apiCache');\n  \n  await store.put({\n    url,\n    data,\n    timestamp: Date.now()\n  });\n}\n\n// استرجاع الاستجابة المخزنة مؤقتًا\nasync function getCachedAPIResponse(url, maxAge = 300000) {\n  const db = await openDB();\n  const tx = db.transaction('apiCache', 'readonly');\n  const store = tx.objectStore('apiCache');\n  \n  const cached = await store.get(url);\n  \n  if (!cached) return null;\n  \n  // فحص العمر\n  if (Date.now() - cached.timestamp > maxAge) {\n    return null; // منتهي الصلاحية\n  }\n  \n  return cached.data;\n}

التخزين المؤقت في Service Worker

تعترض Service Workers طلبات الشبكة وتنفذ استراتيجيات تخزين مؤقت متقدمة. ضرورية لتطبيقات الويب التقدمية.

// service-worker.js\nconst CACHE_VERSION = 'v1';\nconst CACHE_NAME = `app-cache-${CACHE_VERSION}`;\n\nconst STATIC_ASSETS = [\n  '/',\n  '/styles.css',\n  '/app.js',\n  '/logo.png'\n];\n\n// حدث التثبيت - تخزين الأصول الثابتة مؤقتًا\nself.addEventListener('install', (event) => {\n  event.waitUntil(\n    caches.open(CACHE_NAME)\n      .then(cache => cache.addAll(STATIC_ASSETS))\n  );\n});\n\n// حدث التنشيط - تنظيف الذاكرات المؤقتة القديمة\nself.addEventListener('activate', (event) => {\n  event.waitUntil(\n    caches.keys().then(keys => {\n      return Promise.all(\n        keys\n          .filter(key => key !== CACHE_NAME)\n          .map(key => caches.delete(key))\n      );\n    })\n  );\n});

استراتيجية الذاكرة المؤقتة أولاً

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

// استراتيجية الذاكرة المؤقتة أولاً\nself.addEventListener('fetch', (event) => {\n  event.respondWith(\n    caches.match(event.request)\n      .then(cachedResponse => {\n        if (cachedResponse) {\n          return cachedResponse; // التقديم من الذاكرة المؤقتة\n        }\n        \n        // ليس في الذاكرة المؤقتة، جلب من الشبكة\n        return fetch(event.request)\n          .then(response => {\n            // تخزين الاستجابة مؤقتًا\n            const responseClone = response.clone();\n            caches.open(CACHE_NAME)\n              .then(cache => {\n                cache.put(event.request, responseClone);\n              });\n            \n            return response;\n          });\n      })\n  );\n});

استراتيجية الشبكة أولاً

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

// استراتيجية الشبكة أولاً\nself.addEventListener('fetch', (event) => {\n  event.respondWith(\n    fetch(event.request)\n      .then(response => {\n        // تحديث الذاكرة المؤقتة بأحدث إصدار\n        const responseClone = response.clone();\n        caches.open(CACHE_NAME)\n          .then(cache => {\n            cache.put(event.request, responseClone);\n          });\n        \n        return response;\n      })\n      .catch(() => {\n        // فشلت الشبكة، جرّب الذاكرة المؤقتة\n        return caches.match(event.request);\n      })\n  );\n});
نصيحة: استخدم الذاكرة المؤقتة أولاً للأصول التي لا تتغير أبدًا (ملفات ذات إصدارات)، والشبكة أولاً للمحتوى المحدث بشكل متكرر (استجابات API).

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

قدم المحتوى المخزن مؤقتًا على الفور بينما تجلب بيانات جديدة في الخلفية. أفضل تجربة للمستخدم.

// استراتيجية قديم أثناء إعادة التحقق\nself.addEventListener('fetch', (event) => {\n  event.respondWith(\n    caches.match(event.request)\n      .then(cachedResponse => {\n        const fetchPromise = fetch(event.request)\n          .then(response => {\n            // تحديث الذاكرة المؤقتة في الخلفية\n            const responseClone = response.clone();\n            caches.open(CACHE_NAME)\n              .then(cache => {\n                cache.put(event.request, responseClone);\n              });\n            return response;\n          });\n        \n        // إرجاع المخزن مؤقتًا على الفور، جلب في الخلفية\n        return cachedResponse || fetchPromise;\n      })\n  );\n});

إدارة البيانات القديمة

نفّذ استراتيجيات لمعالجة البيانات المخزنة مؤقتًا القديمة والحفاظ على واجهة المستخدم متزامنة.

// مساعد إبطال الذاكرة المؤقتة\nclass CacheInvalidator {\n  static async invalidate(pattern) {\n    const cacheNames = await caches.keys();\n    \n    for (const cacheName of cacheNames) {\n      const cache = await caches.open(cacheName);\n      const requests = await cache.keys();\n      \n      for (const request of requests) {\n        if (request.url.includes(pattern)) {\n          await cache.delete(request);\n        }\n      }\n    }\n  }\n  \n  static async clearAll() {\n    const cacheNames = await caches.keys();\n    await Promise.all(\n      cacheNames.map(name => caches.delete(name))\n    );\n  }\n}\n\n// إبطال بيانات المستخدم عند تسجيل الخروج\nfunction logout() {\n  CacheInvalidator.invalidate('/api/user');\n  localStorage.clear();\n  sessionStorage.clear();\n}

الذاكرة المؤقتة من جانب العميل مع انتهاء الصلاحية

تنفيذ كامل مع انتهاء صلاحية الذاكرة المؤقتة التلقائي والتحديث.

class APICache {\n  constructor(ttl = 300000) { // 5 دقائق افتراضي\n    this.ttl = ttl;\n  }\n  \n  getCacheKey(url, params) {\n    return url + (params ? `?${JSON.stringify(params)}` : '');\n  }\n  \n  async fetch(url, params = null) {\n    const cacheKey = this.getCacheKey(url, params);\n    \n    // تحقق من localStorage أولاً\n    const cached = LocalCache.get(cacheKey);\n    if (cached) {\n      return cached;\n    }\n    \n    // جلب من الشبكة\n    const queryString = params ? '?' + new URLSearchParams(params) : '';\n    const response = await fetch(url + queryString);\n    const data = await response.json();\n    \n    // تخزين النتيجة مؤقتًا\n    LocalCache.set(cacheKey, data, this.ttl / 60000);\n    \n    return data;\n  }\n  \n  invalidate(url) {\n    const pattern = this.getCacheKey(url);\n    // إزالة جميع المفاتيح المطابقة للنمط\n    Object.keys(localStorage)\n      .filter(key => key.startsWith(pattern))\n      .forEach(key => localStorage.removeItem(key));\n  }\n}\n\n// الاستخدام\nconst apiCache = new APICache(600000); // 10 دقائق\n\n// الاستدعاء الأول - جلب من الشبكة\nconst users = await apiCache.fetch('/api/users');\n\n// الاستدعاء الثاني - فوري من الذاكرة المؤقتة\nconst usersAgain = await apiCache.fetch('/api/users');\n\n// الإبطال عند التحديثات\nawait updateUser(userId, data);\napiCache.invalidate('/api/users');
تحذير: لا تخزن أبدًا بيانات حساسة (كلمات مرور، رموز) في localStorage أو IndexedDB. استخدم ملفات تعريف ارتباط آمنة وhttpOnly لرموز المصادقة.
تمرين: ابنِ تطبيق قارئ أخبار مع تخزين مؤقت من جانب العميل. خزّن قوائم المقالات في localStorage بانتهاء صلاحية 10 دقائق. خزّن المقالات الكاملة في IndexedDB. نفّذ Service Worker باستراتيجية قديم أثناء إعادة التحقق. أضف إبطال الذاكرة المؤقتة عندما يقوم المستخدمون بالتحديث يدويًا. اعرض طوابع زمنية للذاكرة المؤقتة في واجهة المستخدم حتى يعرف المستخدمون حداثة البيانات.