JavaScript بعمق
تخزين المتصفح والحالة: localStorage والكوكيز وIndexedDB وأكثر
دليل عملي لتخزين البيانات في المتصفح — localStorage وsessionStorage، والكوكيز وأعلامها، ومتى تستخدم كلًّا، وIndexedDB للبيانات المنظّمة الأكبر، وواجهة Cache، والأمان وحدود الحجم، وأنماط إبقاء حالة التطبيق — مع تمارين عملية وحلولها.
صفحات الويب عديمة الحالة افتراضيًّا — أعد التحميل ويُعاد ضبط كل شيء. لتذكّر تفضيل سمة، أو إبقاء مستخدم مسجَّلًا، أو تخزين بيانات مؤقّتًا، أو بناء شيء قادر على العمل دون اتصال، تحتاج تخزين المتصفح. هناك عدّة خيارات يسهل الخلط بينها؛ واختيار الخطأ يعني بيانات تختفي مبكرًا، أو تتسرّب عبر التبويبات، أو تنفخ طلب خادم. هذا المقال يعرض كل آلية، لماذا هي، ومقايضاتها. (يبني على الكائنات وJSON من أساسيات JavaScript.)
طابِق الأداة مع البيانات. localStorage للقيم الصغيرة الدائمة غير الحسّاسة. sessionStorage للحالة المؤقّتة لكل تبويب. الكوكيز فقط حين يحتاج الخادم القيمة عند كل طلب. IndexedDB للبيانات الكبيرة أو المنظّمة. اللجوء للخطأ هو أصل معظم علل التخزين.
localStorage: مفتاح-قيمة دائم بسيط
الحصان العامل. مخزن مفتاح-قيمة نصّيّ متزامن يدوم عبر إعادات التحميل وإعادات تشغيل المتصفح، محصور بالأصل:
localStorage.setItem("theme", "dark");
localStorage.getItem("theme"); // "dark"
localStorage.removeItem("theme");
localStorage.clear(); // امحُ كل شيء لهذا الأصل
يخزّن سلاسل فقط، فالكائنات يجب تسلسلها بـ JSON:
localStorage.setItem("user", JSON.stringify({ name: "Ada", age: 36 }));
const user = JSON.parse(localStorage.getItem("user")); // عودةً إلى كائن
الحدود والتحذيرات: نحو 5–10 ميغابايت لكل أصل، وهو متزامن (القراءات/الكتابات الكبيرة تحجب الخيط الرئيسي)، ويقرؤه أي JavaScript على الصفحة — فـلا تخزّن أسرارًا أو رموزًا هناك أبدًا. مثاليّ لـ: السمة، اللغة، تفضيلات الواجهة، البيانات المخزَّنة غير الحسّاسة.
sessionStorage: نفس الواجهة، محصور بالتبويب
واجهة مطابقة لـ localStorage، لكن البيانات تعيش فقط لـ جلسة التبويب — تُمسَح عند إغلاق التبويب ولا تُشارَك مع تبويبات أخرى:
sessionStorage.setItem("step", "3"); // تقدّم نموذج متعدّد الخطوات
sessionStorage.getItem("step");
استخدمه للحالة المؤقّتة لكل تبويب: الخطوة الحالية لمعالج، موضع التمرير، مسوّدة لا تريدها تنجو من إغلاق تبويب. الفرق المفتاحي عن localStorage هو العمر والنطاق — جلسة مقابل دائم، تبويب واحد مقابل كل تبويبات الأصل.
الكوكيز: حين يحتاجها الخادم
الكوكيز تسبق واجهات التخزين ولها سمة مميِّزة واحدة: تُرسَل إلى الخادم مع كل طلب لذلك الأصل. هذا يجعلها صحيحة لما يجب أن يراه الخادم (معرّفات الجلسة، المصادقة) — وخاطئة للبيانات الخاصّة بالعميل فقط، لأنها تنفخ كل طلب:
document.cookie = "lang=en; path=/; max-age=31536000; SameSite=Lax";
الكوكيز صغيرة (~4 كيلوبايت) وواجهة document.cookie الخام أخرق. الأهمّ هي أعلام الأمان، التي يضبطها الخادم عادةً:
HttpOnly— غير مرئيّة لـ JavaScript (تمنع سرقة XSS)؛ كوكيز المصادقة ينبغي أن تملكها دائمًا.Secure— تُرسَل فقط عبر HTTPS.SameSite—Strict/Lax/None، تتحكّم في الإرسال عبر المواقع (حماية CSRF).
قاعدة عملية: إن احتاجت JavaScript القيمة، استخدم التخزين؛ وإن احتاجها الخادم عند كل طلب (جلسات، رموز مصادقة يضبطها)، استخدم كوكي — مثاليًّا HttpOnly.
IndexedDB: كبير، منظّم، غير متزامن
حين تتجاوز بضعة مفاتيح-قيم — تخزين استجابات API، مخزن مستندات دون اتصال، مئات السجلّات — هناك IndexedDB: قاعدة بيانات غير متزامنة معامِلاتية في المتصفح تحمل كائنات منظّمة (لا سلاسل فقط) وتتوسّع إلى مئات الميغابايتات.
const db = await new Promise((resolve, reject) => {
const req = indexedDB.open("app", 1);
req.onupgradeneeded = () => req.result.createObjectStore("notes", { keyPath: "id" });
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
const tx = db.transaction("notes", "readwrite");
tx.objectStore("notes").put({ id: 1, text: "Hello" });
الواجهة الخام مطوَّلة وقائمة على الأحداث، فمعظم الفرق تستخدم غلافًا صغيرًا مثل idb أو Dexie يحوّلها إلى وعود نظيفة. الجأ إلى IndexedDB حين تكون البيانات كبيرة أو منظّمة أو مُستعلَمة أو مطلوبة دون اتصال — إنه أساس التطبيقات القادرة على العمل دون اتصال.
واجهة Cache (وكلمة عن العمل دون اتصال)
لتخزين استجابات الشبكة (الأصول، نتائج API)، واجهة Cache — يقودها عادةً عامل خدمة (service worker) — تخزّن أزواج Request/Response وهي ما يجعل تطبيقات الويب التقدّمية تعمل دون اتصال:
const cache = await caches.open("v1");
await cache.add("/styles.css"); // اجلب وخزّن
const res = await cache.match("/styles.css"); // قدّم من المخزن
هذه أداة متخصّصة للعمل دون اتصال أولًا وللأداء؛ تلجأ إليها عند بناء PWA، لا للحالة اليومية.
الاختيار: خريطة سريعة
| الحاجة | استخدم |
|---|---|
| قيمة صغيرة، تدوم عبر الزيارات، JS فقط | localStorage |
| مؤقّت، هذا التبويب فقط | sessionStorage |
| الخادم يجب أن يقرأها كل طلب (مصادقة، جلسة) | كوكي (HttpOnly) |
| بيانات كبيرة/منظّمة/قابلة للاستعلام/دون اتصال | IndexedDB |
| تخزين استجابات الشبكة للعمل دون اتصال | واجهة Cache + عامل خدمة |
الأخطاء الشائعة
- تخزين الرموز/الأسرار في localStorage — أي سكربت على الصفحة يقرؤها (تعرّض XSS).
- نسيان
JSON.stringify/parseوتخزين[object Object]بالخطأ. - استخدام الكوكيز لبيانات خاصّة بالعميل، فنفخ كل طلب إلى الخادم.
- توقّع أن يكون
localStorageلكل تبويب — إنه مشترك عبر كل تبويبات الأصل (استخدمsessionStorageلكل تبويب). - عدم معالجة فشل
JSON.parseعلى بيانات مخزَّنة فاسدة/قديمة — لُفّها في try/catch. - معاملة
localStorageكأنه لا محدود — إنه ~5–10 ميغابايت ومتزامن؛ استخدم IndexedDB للبيانات الكبيرة. - افتراض أن البيانات المخزَّنة موجودة دائمًا — المستخدمون يمحونها، والوضع الخاص قد يعطّلها؛ برمِج دفاعيًّا.
تمارين
جرّب كلًّا منها قبل فتح الحل.
تمرين 1 — أبقِ سمة
احفظ اختيار سمة كي ينجو من إعادات التحميل، واقرأه عائدًا بقيمة افتراضية.
إظهار الحل
function setTheme(theme) { localStorage.setItem("theme", theme); }
function getTheme() { return localStorage.getItem("theme") ?? "light"; }
localStorage يدوم عبر إعادات التحميل؛ و?? "light" يوفّر افتراضيًّا أول مرّة، حين لا شيء مخزَّن بعد.
تمرين 2 — خزّن واقرأ كائنًا
احفظ كائن إعدادات واقرأه عائدًا ككائن قابل للاستخدام، بأمان.
إظهار الحل
function saveSettings(obj) {
localStorage.setItem("settings", JSON.stringify(obj));
}
function loadSettings() {
try {
return JSON.parse(localStorage.getItem("settings")) ?? {};
} catch {
return {}; // بيانات فاسدة/قديمة → افتراضيّ آمن
}
}
التخزين يحمل سلاسل، فتحوّلها نصًّا عند الحفظ وتحلّلها عند التحميل — ملفوفًا في try/catch لأن البيانات المخزَّنة قد تكون فاسدة أو من نسخة أقدم.
تمرين 3 — اختر المخزن الصحيح
رمز مصادقة مستخدم، يضبطه الخادم، يجب أن يرافق كل طلب API ويبقى مخفيًّا عن JavaScript. أي آلية، ولماذا؟
إظهار الحل
كوكي بأعلام HttpOnly (وSecure وSameSite). يُرسَل تلقائيًّا مع كل طلب للأصل (وهذا ما يحتاجه الخادم)، وHttpOnly يجعله غير مقروء من JavaScript، حاميًا إيّاه من سرقة XSS — وهذا بالضبط سبب كون localStorage الخيار الخطأ للرموز.
النموذج الذهني الذي تحتفظ به
تخزين المتصفح عن مطابقة العمر والنطاق والحجم مع البيانات. localStorage بسيط، دائم، ~5–10 ميغابايت، نصّيّ فقط (حوّل كائناتك بـ JSON)، مقروء بـ JS — رائع للتفضيلات، فظيع للأسرار. sessionStorage نفسه لكنه لكل تبويب ومؤقّت. الكوكيز موجودة لسبب واحد — إرسالها إلى الخادم كل طلب — فاستخدمها للمصادقة/الجلسات (بـ HttpOnly/Secure/SameSite) ولا شيء آخر. للبيانات الكبيرة أو المنظّمة أو دون اتصال، ارتقِ إلى IndexedDB (عبر غلاف مثل idb/Dexie)، وإلى واجهة Cache + عمّال الخدمة للاستجابات دون اتصال. برمِج دفاعيًّا — التخزين قد يكون فارغًا أو فاسدًا أو معطَّلًا — ولا تضع أبدًا رمزًا حيث يقرؤه سكربت.