TypeScript و Web Storage
تشرح هذه المقالة TypeScript و Web Storage۔
نشرح TypeScript و Web Storage مع أمثلة عملية۔
YouTube Video
TypeScript و Web Storage
تخزين المتصفح Web Storage هو مخزن مفاتيح/قيم للسلاسل النصية۔ إنه خفيف الوزن ويقدّم واجهة برمجة تطبيقات متزامنة، لكن ضع في الحسبان أنه لا يدعم سوى تخزين السلاسل النصية، ويجب عليك معالجة الاستثناءات مثل تجاوز حصة التخزين۔ عند دمجه مع TypeScript، يمكنك إضافة أمان الأنواع، وتسلسل/إلغاء تسلسل آمنين، وإدارة مفاتيح مركزية، وانتهاء صلاحية وإصدار، ما يفضي إلى تصميم جاهز للإنتاج۔
localStorage و sessionStorage
يمثل localStorage تخزيناً دائماً يبقى بعد إغلاق المتصفح، بينما sessionStorage هو تخزين جلسة لكل علامة تبويب/نافذة يُمسح عند إغلاق علامة التبويب۔ كلاهما يخزّن القيم كأزواج مفتاح-قيمة (كسلاسل نصية)۔
1// Simple usage example: store and retrieve a string
2// This example uses localStorage to persist a username.
3localStorage.setItem('username', 'alice');
4console.log('saved username:', localStorage.getItem('username')); // "alice"
5
6// session storage example
7sessionStorage.setItem('session-id', 'xyz-123');
8console.log('session id:', sessionStorage.getItem('session-id')); // "xyz-123"
- هذا الكود مثال لحفظ السلاسل النصية واسترجاعها۔ بما أن
Web Storageلا يخزّن إلا السلاسل النصية، يجب تحويل الكائنات إلى JSON قبل تخزينها۔
مثال باستخدام تحليل JSON
لتخزين الكائنات في Web Storage واستعادتها، استخدم JSON.stringify / JSON.parse۔
1// Simple write & read with JSON
2// Store a small object as JSON and read it back.
3const user = { id: 1, name: "Alice" };
4localStorage.setItem("app:user", JSON.stringify(user));
5
6const raw = localStorage.getItem("app:user");
7const parsed = raw ? (JSON.parse(raw) as { id: number; name: string }) : null;
8
9console.log("user:", parsed); // user: { id: 1, name: "Alice" }
- هذا الكود مثال عمليّ مصغّر۔ في التطبيقات الواقعية، يجب أيضًا أخذ الاستثناءات بالحسبان مثل فشل التحليل ونفاد حصة التخزين۔
مثال على معالجة الاستثناءات عند تحليل JSON
هنا نوفر غلافًا لجعل عمليات القراءة والكتابة آمنة ومعالجة حالات فشل JSON.parse واستثناءات setItem۔
1// Safe JSON helpers
2// These helpers handle parse errors and memory/storage issues separately.
3export function safeParseJson<T>(raw: string | null): T | null {
4 if (raw == null) return null;
5
6 try {
7 return JSON.parse(raw) as T;
8 } catch (error: any) {
9 if (error instanceof SyntaxError) {
10 console.error("JSON parse error:", error.message);
11 return null;
12 }
13
14 console.error("Unexpected JSON error:", error);
15 return null;
16 }
17}
18
19// Safe setter for JSON values
20export function setJson(storage: Storage, key: string, value: unknown): void {
21 try {
22 const json = JSON.stringify(value);
23 storage.setItem(key, json);
24 } catch (error: any) {
25 if (error?.name === "QuotaExceededError") {
26 console.error("Storage quota exceeded while saving JSON:", error.message);
27 } else if (error instanceof TypeError) {
28 console.error("JSON serialization failed:", error.message);
29 } else {
30 console.error("Unexpected error while setting JSON:", error);
31 }
32 }
33}
34
35// Safe getter for JSON values
36export function getJson<T>(storage: Storage, key: string, fallback?: T): T | null {
37 const parsed = safeParseJson<T>(storage.getItem(key));
38 return parsed ?? (fallback ?? null);
39}- يمكنك إعادة استخدام هذه الأداة عبر التطبيق۔ أضف تغليفاً إضافياً عندما تريد إضافة
try/catch۔
مثال على الحفظ مع TTL (انتهاء الصلاحية)
لأن Web Storage لا يوفّر TTL بحد ذاته، أدِر ذلك بإضافة expiresAt إلى القيمة۔
1// TTL wrapper
2// Store { value, expiresAt } and automatically expire items on read.
3type WithTTL<T> = { value: T; expiresAt: number | null };
4
5export function setWithTTL<T>(storage: Storage, key: string, value: T, ttlMs?: number) {
6 const payload: WithTTL<T> = { value, expiresAt: ttlMs ? Date.now() + ttlMs : null };
7 setJson(storage, key, payload);
8}
9
10export function getWithTTL<T>(storage: Storage, key: string): T | null {
11 const payload = getJson<WithTTL<T>>(storage, key);
12 if (!payload) return null;
13 if (payload.expiresAt && Date.now() > payload.expiresAt) {
14 storage.removeItem(key);
15 return null;
16 }
17 return payload.value;
18}- يعدّ TTL فعالاً للتخزين المؤقت والحفظ التلقائي (المسودات) ويمكن أن يقلّل حالات عدم الاتساق۔
مثال على إدارة مركزية للمفاتيح ومساحات الأسماء (تجنّب التصادم)
توحيد المفاتيح بالشكل prefix + version + name يقلّل التصادمات ويبسّط عمليات الترحيل۔
1// Key factory with namespacing and versioning
2// Create namespaced keys like "myapp:v1:theme" to avoid collisions.
3const APP = "myapp";
4const V = "v1";
5
6const ns = (k: string) => `${APP}:${V}:${k}`;
7
8const Keys = {
9 theme: ns("theme"),
10 user: ns("user"),
11 cart: ns("cart"),
12 draft: ns("draft"),
13};- إن تضمين مساحة اسم في المفتاح يجعل من الأسهل تبديل الإصدارات أو إجراء التنظيف لاحقًا۔
مثال على تجاوز الحصة (QuotaExceededError) واستراتيجيات التراجع
ضع في اعتبارك أن QuotaExceededError قد يحدث عند استدعاء setItem، وصمّم استراتيجية احتياطية لحالة فشل حفظ البيانات۔ على سبيل المثال، عند تجاوز سعة التخزين، يمكنك حذف البيانات القديمة أو الاعتماد على sessionStorage أو ذاكرة مؤقتة داخلية للحفاظ على استقرار التطبيق ككل۔
1// Quota-safe set with fallback to in-memory storage
2// Return true if stored, false otherwise.
3export function trySetJson(storage: Storage, key: string, value: unknown, fallback?: Map<string, string>): boolean {
4 try {
5 storage.setItem(key, JSON.stringify(value));
6 return true;
7 } catch (err) {
8 console.warn("Failed to set item:", key, err);
9 if (fallback) {
10 try {
11 fallback.set(key, JSON.stringify(value));
12 return true;
13 } catch {
14 return false;
15 }
16 }
17 return false;
18 }
19}
20
21// Example fallback usage
22const inMemoryFallback = new Map<string, string>();
23const ok = trySetJson(localStorage, Keys.cart, { items: [] }, inMemoryFallback);
24if (!ok) console.log("Saved to fallback map instead");- اعتمادًا على وجهة الحل الاحتياطي، قد لا تكون استمرارية البيانات مضمونة۔ لذلك، اختر وجهة تخزين مناسبة وفقًا لحالة الاستخدام۔ على سبيل المثال، في وضع التصفح الخاص أو تحت قيود التخزين، يمكنك الحفاظ على الوظائف باستخدام الذاكرة مؤقتًا أو
sessionStorage۔
مثال على المزامنة عبر علامات التبويب (حدث storage) والتنبيهات داخل نفس علامة التبويب
باستخدام window.addEventListener('storage', …)، يمكنك اكتشاف تغييرات التخزين التي حدثت في علامات تبويب أخرى۔ ومع ذلك، لا يُطلق هذا الحدث داخل نفس علامة التبويب۔ لذلك، من أجل إشعارات التغيير داخل نفس علامة التبويب، انشر أحداثك الخاصة باستخدام CustomEvent۔
1// Cross-tab and same-tab notification helpers
2// storage event fires on other tabs; use CustomEvent for same-tab listeners.
3const SAME_TAB_EVENT = "storage:changed";
4
5function notifyChanged(key: string) {
6 window.dispatchEvent(new CustomEvent(SAME_TAB_EVENT, { detail: { key } }));
7}
8
9function setJsonWithNotify(storage: Storage, key: string, value: unknown) {
10 setJson(storage, key, value);
11 notifyChanged(key);
12}
13
14// Listeners
15window.addEventListener("storage", (e) => {
16 if (e.key === Keys.theme) {
17 const theme = safeParseJson<string>(e.newValue);
18 console.log("Theme changed in another tab:", theme);
19 }
20});
21
22window.addEventListener(SAME_TAB_EVENT, (e: Event) => {
23 const detail = (e as CustomEvent).detail as { key: string };
24 console.log("Changed in this tab:", detail.key);
25});- بهذا الكود، يمكنك مزامنة تغييرات التخزين عبر كلٍ من علامات تبويب أخرى وعلامة التبويب الحالية۔
سجل آمن النوع (تحديد صارم للنوع لكل مفتاح)
عرّف خريطة من المفتاح إلى النوع في TypeScript لمنع الأخطاء أثناء الحفظ والاسترجاع۔
1// Typed registry that enforces types per key
2// Registry maps keys to their allowed types.
3type Registry = {
4 [k in typeof Keys.theme]: "light" | "dark";
5} & {
6 [k in typeof Keys.user]: { id: number; name: string };
7};
8
9type KeyOf<R> = Extract<keyof R, string>;
10
11export const TypedStore = {
12 get<K extends KeyOf<Registry>>(key: K, storage: Storage = localStorage): Registry[K] | null {
13 return getJson<Registry[K]>(storage, key);
14 },
15 set<K extends KeyOf<Registry>>(key: K, value: Registry[K], storage: Storage = localStorage): void {
16 setJson(storage, key, value);
17 },
18 remove<K extends KeyOf<Registry>>(key: K, storage: Storage = localStorage): void {
19 storage.removeItem(key);
20 },
21};- {^ i18n_speak 型をキーに関連付けることで、ランタイムでの誤使用をコンパイル時に検出でき、安全性を高めることができます。 ^}
التسلسل/المستعيد (reviver) للأنواع المعقّدة (Date/Map وغيرها)
لاستعادة كائنات مثل Date وMap بشكل صحيح، استفد من replacer وreviver في JSON.stringify۔
1// Serializing Dates with replacer and reviver
2// Custom replacer marks Date objects for correct revival.
3function replacer(_k: string, v: unknown) {
4 if (v instanceof Date) return { __type: "Date", value: v.toISOString() };
5 return v;
6}
7
8function reviver(_k: string, v: any) {
9 if (v && v.__type === "Date") return new Date(v.value);
10 return v;
11}
12
13function setJsonWithDates(storage: Storage, key: string, value: unknown) {
14 storage.setItem(key, JSON.stringify(value, replacer));
15}
16
17function getJsonWithDates<T>(storage: Storage, key: string): T | null {
18 const raw = storage.getItem(key);
19 if (!raw) return null;
20 try { return JSON.parse(raw, reviver) as T; } catch { return null; }
21}- باستخدام هذه الطريقة، تُستعاد كائنات
Dateبشكل صحيح على أنهاDate۔ وبالمثل، يمكن أيضًا وسمMapوSetواستعادتهما۔
مثال على إدارة الإصدارات واستراتيجيات الترحيل
إذا كان من المحتمل تغيير تنسيق التخزين مستقبلاً، فأدرج إصداراً في الحمولة وتهيّأ لعمليات الترحيل۔
1// Versioned payload pattern for migrations
2// Keep { v, data } and migrate on read if necessary.
3type VersionedPayload<T> = { v: number; data: T };
4
5function migrateUserV1toV2(u1: { id: number; name: string }) {
6 return { id: u1.id, profile: { displayName: u1.name } };
7}
8
9function readUserAnyVersion(): { id: number; profile: { displayName: string } } | null {
10 const raw = localStorage.getItem(Keys.user);
11 if (!raw) return null;
12 try {
13 const obj = JSON.parse(raw) as VersionedPayload<any>;
14 if (obj.v === 2) {
15 return obj.data;
16 } else if (obj.v === 1) {
17 const migrated = migrateUserV1toV2(obj.data);
18 localStorage.setItem(Keys.user, JSON.stringify({ v: 2, data: migrated }));
19 return migrated;
20 }
21 return null;
22 } catch (err) {
23 console.error("migration parse error", err);
24 return null;
25 }
26}- بتكديس عمليات ترحيل صغيرة يمكنك الحفاظ على التوافق العكسي۔
مثال على التعامل في SSR (التصيير على جانب الخادم)
في البيئات التي لا تحتوي على window، سيؤدي الوصول المباشر إلى localStorage إلى الانهيار، لذا استخدم حمايات خاصة بالبيئة۔
1// Guard for SSR
2// Return a Storage-compatible object or null when not in browser.
3export const isBrowser = (): boolean => typeof window !== "undefined" && typeof window.localStorage !== "undefined";
4
5export const safeLocalStorage = (): Storage | null => (isBrowser() ? window.localStorage : null);
6
7// Usage
8const ls = safeLocalStorage();
9if (ls) setJson(ls, Keys.theme, "dark");- بالنسبة للكود الذي يجب أن يدعم SSR، تذكّر دائماً التحقق من
typeof window۔
نصائح عملية
- حدّد تكرار الكتابة باستخدام debounce/throttle (لتخفيف الاندفاعات الناتجة عن إجراءات واجهة المستخدم)۔
- أدِر المفاتيح باستخدام مساحة اسم + إصدار، مثل:
app:v1:...۔ - كقاعدة عامة لا تخزّن معلومات حساسة (رموز الوصول وما شابه)۔ إن اضطررت، ففكّر في أعمار قصيرة مع تحقق من الخادم أو WebCrypto۔
- السعة تعتمد على المتصفح (بضعة ميغابايت)، لذا خزّن البيانات الكبيرة في IndexedDB۔
- استخدم
CustomEventللتنبيهات داخل نفس علامة التبويب وstorageلما بين علامات التبويب۔ - في SSR تحقّق دائماً من
typeof window۔
فئة موحّدة لـ 'مخزن آمن النوع'
لنلقِ نظرة على مثال لتنفيذ فئة عامة يدمج العناصر التي تناولناها حتى الآن، بما في ذلك مساحات الأسماء، وسلامة الأنواع، ومدة الصلاحية (TTL)، ومعالجة الاستثناءات۔ في المنتجات الفعلية، فكّر بإضافة اختبارات، وتسجيل السجلات (logging)، وحذف البيانات القديمة اعتمادًا على LRU، والتشفير، وغير ذلك۔
1// Comprehensive TypedStorage store integrating many patterns shown above.
2// - type-safe registry per key
3// - prefix (namespace + version)
4// - trySet with fallback
5// - same-tab notify
6// - TTL optional getter/setter
7type Jsonifiable = string | number | boolean | null | Jsonifiable[] | { [k: string]: Jsonifiable };
8
9interface StoreOptions {
10 storage?: Storage | null; // default: auto-detected localStorage or null
11 prefix?: string; // e.g., "myapp:v1"
12 sameTabEvent?: string | null;
13 fallback?: Map<string, string>; // in-memory fallback
14}- يعرض هذا الكود الإعدادات وتعريفات الأنواع لبناء مخزن مفاتيح/قيم آمن الأنواع وغني بالميزات۔
1export class TypedStorage<Reg extends Record<string, Jsonifiable | object>> {
2 private storage: Storage | null;
3 private prefix: string;
4 private sameTabEvent: string | null;
5 private fallback?: Map<string, string>;
6
7 constructor(private registry: Reg, opts: StoreOptions = {}) {
8 this.storage = opts.storage ?? (typeof window !== "undefined" ? window.localStorage : null);
9 this.prefix = (opts.prefix ?? "app:v1") + ":";
10 this.sameTabEvent = opts.sameTabEvent ?? "storage:changed";
11 this.fallback = opts.fallback;
12 }
13
14 private k(key: keyof Reg & string) { return this.prefix + key; }- يمثل هذا الكود جوهر الفئة
TypedStorageالتي توفر تخزين مفاتيح/قيم مع سلامة الأنواع. وهو يدير المفاتيح المسموح بها وأنواعها بناءً علىregistry، ويولّد مفاتيح تخزين مسبوقة۔ وعلاوة على ذلك، يستخدمlocalStorageوحلًا احتياطيًا داخل الذاكرة، ويتيح لك تعيين اسم حدث لإشعارات التغيير ضمن نفس علامة التبويب۔
1 // Basic get with optional TTL-aware retrieval
2 get<K extends keyof Reg & string>(key: K): Reg[K] | null {
3 const fullKey = this.k(key);
4 try {
5 const raw = this.storage ? this.storage.getItem(fullKey) : this.fallback?.get(fullKey) ?? null;
6 if (!raw) return null;
7 // Check if value is TTL-wrapped
8 const maybe = safeParseJson<{ value: Reg[K]; expiresAt?: number }>(raw);
9 if (maybe && typeof maybe.expiresAt === "number") {
10 if (maybe.expiresAt && Date.now() > maybe.expiresAt) {
11 this.remove(key);
12 return null;
13 }
14 return maybe.value;
15 }
16 return safeParseJson<Reg[K]>(raw);
17 } catch (err) {
18 console.error("TypedStorage.get error", err);
19 return null;
20 }
21 }- تسترجع الدالة
getالقيمة للمفتاح المحدد بطريقة تراعي سلامة الأنواع، ويمكنها اختيارياً التعامل مع القيم ذات مدة الصلاحية (TTL)۔
1 // Basic set; returns success boolean
2 set<K extends keyof Reg & string>(key: K, value: Reg[K]): boolean {
3 const fullKey = this.k(key);
4 const payload = JSON.stringify(value);
5 try {
6 if (this.storage) this.storage.setItem(fullKey, payload);
7 else this.fallback?.set(fullKey, payload);
8 if (this.sameTabEvent) window.dispatchEvent(new CustomEvent(this.sameTabEvent, { detail: { key: fullKey } }));
9 return true;
10 } catch (err) {
11 console.warn("TypedStorage.set primary failed, trying fallback", err);
12 try {
13 if (this.fallback) {
14 this.fallback.set(fullKey, payload);
15 return true;
16 }
17 return false;
18 } catch (e) {
19 console.error("TypedStorage.set fallback failed", e);
20 return false;
21 }
22 }
23 }- تقوم الدالة
setبحفظ قيمة تحت المفتاح المحدد وتعيد قيمة منطقية تشير إلى ما إذا كانت العملية قد نجحت۔
1 // Set with TTL convenience
2 setWithTTL<K extends keyof Reg & string>(key: K, value: Reg[K], ttlMs?: number): boolean {
3 const payload = { value, expiresAt: ttlMs ? Date.now() + ttlMs : null };
4 return this.set(key, payload as unknown as Reg[K]);
5 }- تقوم الدالة
setWithTTLبحفظ قيمة مع مدة الصلاحية (TTL)۔
1 remove<K extends keyof Reg & string>(key: K) {
2 const fullKey = this.k(key);
3 try {
4 if (this.storage) this.storage.removeItem(fullKey);
5 this.fallback?.delete(fullKey);
6 } catch (err) { console.warn("TypedStorage.remove error", err); }
7 }- تقوم الدالة
removeبحذف القيمة للمفتاح المحدد من كلٍ من التخزين والبديل۔
1 clear() {
2 try {
3 if (this.storage) this.storage.clear();
4 this.fallback?.clear();
5 } catch (err) { console.warn("TypedStorage.clear error", err); }
6 }
7}- تقوم الدالة
clearبحذف جميع البيانات المخزنة من كلٍ من التخزين والبديل۔
1// Usage example
2type MyReg = {
3 theme: "light" | "dark";
4 user: { id: number; name: string };
5 draft: string;
6};
7
8const memFallback = new Map<string, string>();
9const store = new TypedStorage<MyReg>({} as MyReg, {
10 prefix: "myapp:v1",
11 sameTabEvent: "storage:changed",
12 fallback: memFallback
13});
14store.set("theme", "dark");
15console.log(store.get("theme")); // "dark"
16store.setWithTTL("draft", "in-progress...", 1000 * 60 * 60); // keep 1 hour
-
يمثل هذا الكود مثالاً على استخدام
TypedStorage، حيث يحفظ ويسترجع قيماً مثل"theme"و"draft"في مخزن مفاتيح/قيم يراعي سلامة الأنواع، كما يدعم مدة الصلاحية (TTL) والبديل۔ يُهيّئ إشعارات ضمن نفس علامة التبويب وحلًا احتياطيًا داخل الذاكرة لتنفيذ عمليات التخزين بأمان۔ -
تُعد الفئة
TypedStorageنقطة انطلاق عملية۔ نفّذ استراتيجيات LRU والتشفير والضغط وبدائل إلى IndexedDB حسب الحاجة۔
الملخص
عند استخدام Web Storage مع TypeScript، ضع دائماً في الاعتبار أربع نقاط: أمان النوع، والقدرة على تحمّل الاستثناءات، والأمان، والمزامنة (تعدد علامات التبويب) من أجل تصميم متين۔ الأغلفة والأدوات التي رأيناها حتى الآن أمثلة على ذلك۔ يمكنك أيضاً الترحيل إلى أنواع تخزين متصفح أخرى مثل IndexedDB عند الحاجة۔
يمكنك متابعة المقالة أعلاه باستخدام Visual Studio Code على قناتنا على YouTube.۔ يرجى التحقق من القناة على YouTube أيضًا.۔