تايب سكريبت و IndexedDB

تايب سكريبت و IndexedDB

تشرح هذه المقالة عن تايب سكريبت و IndexedDB۔

سنشرح تايب سكريبت و IndexedDB مع أمثلة عملية۔

YouTube Video

تايب سكريبت و IndexedDB

IndexedDB هو تخزين NoSQL منخفض المستوى يتيح لك حفظ البيانات المنظمة في المتصفح۔ مع تايب سكريبت، يمكنك تمثيل المخططات بطريقة آمنة نوعياً، مما يقلل الأخطاء ويحسن سهولة الصيانة۔

المصطلحات الأساسية وسير العمل

IndexedDB هو قاعدة بيانات صغيرة داخل المتصفح۔ يدير البيانات باستخدام آليات مثل قواعد البيانات التي لها أسماء وإصدارات، مخازن الكائنات، المعاملات، الفهارس، والمؤشرات۔ لقواعد البيانات إصدارات، وعندما يتم الترقية إلى إصدار جديد، يتم استدعاء onupgradeneeded لتحديث المخطط مثل إنشاء أو تعديل الجداول۔

فتح IndexedDB (النمط الأساسي)

أولاً، نعرض مثالاً لفتح قاعدة بيانات باستخدام IndexedDB وإنشاء مخزن كائنات في onupgradeneeded إذا تطلب الأمر۔

 1// Open an IndexedDB database and create an object store if needed.
 2// This code shows the classic callback-based IndexedDB API wrapped into a Promise.
 3function openDatabase(dbName: string, version: number): Promise<IDBDatabase> {
 4  return new Promise((resolve, reject) => {
 5    const request = indexedDB.open(dbName, version);
 6
 7    request.onerror = () => {
 8      reject(request.error);
 9    };
10
11    request.onupgradeneeded = (event) => {
12      const db = request.result;
13      if (!db.objectStoreNames.contains('todos')) {
14        // Create object store with keyPath 'id'
15        db.createObjectStore('todos', { keyPath: 'id' });
16      }
17    };
18
19    request.onsuccess = () => {
20      resolve(request.result);
21    };
22  });
23}
24
25// Usage example:
26openDatabase('my-db', 1)
27  .then(db => {
28    console.log('DB opened', db.name, db.version);
29    db.close();
30  })
31  .catch(err => console.error('Failed to open DB', err));
  • يفتح هذا الكود قاعدة البيانات ويسجل ما إذا كانت العملية ناجحة أو فاشلة۔
  • إذا لزم الأمر، يتم إنشاء مخزن todos في onupgradeneeded۔

تعريف الأنواع في تايب سكريبت (النماذج)

بعد ذلك، نقوم بتعريف أنواع البيانات باستخدام تايب سكريبت۔ هذا يضمن أمان الأنواع في عمليات CRUD اللاحقة۔

1// Define the TypeScript interface for a Todo item.
2// This helps with type safety in the database operations.
3interface Todo {
4  id: string;          // primary key
5  title: string;
6  completed: boolean;
7  createdAt: number;
8}
  • هنا، نعرّف نوع Todo۔

أمثلة على تنفيذ دوال CRUD بسيطة

بعد ذلك، نعرض عمليات CRUD الأساسية مثل الإضافة، الاسترجاع، التحديث، والحذف من مخزن الكائنات۔ كل دالة تأخذ كائن IDBDatabase وتعيد وعداً (Promise)۔

 1// CRUD utilities for the 'todos' object store.
 2// Each operation uses a transaction and returns a Promise for easier async/await usage.
 3
 4function addTodo(db: IDBDatabase, todo: Todo): Promise<void> {
 5  return new Promise((resolve, reject) => {
 6    const tx = db.transaction('todos', 'readwrite');
 7    const store = tx.objectStore('todos');
 8    const req = store.add(todo);
 9
10    req.onsuccess = () => {
11      console.log('Todo added', todo.id);
12    };
13    req.onerror = () => reject(req.error);
14
15    tx.oncomplete = () => resolve();
16    tx.onerror = () => reject(tx.error);
17  });
18}
  • تضيف هذه الدالة عنصر Todo جديداً إلى مخزن todos في IndexedDB۔ تعيد وعدًا (Promise) للمعالجة غير المتزامنة، ويتم تحقيقه عند اكتمال المعالجة۔
 1function getTodo(db: IDBDatabase, id: string): Promise<Todo | undefined> {
 2  return new Promise((resolve, reject) => {
 3    const tx = db.transaction('todos', 'readonly');
 4    const store = tx.objectStore('todos');
 5    const req = store.get(id);
 6
 7    req.onsuccess = () => resolve(req.result as Todo | undefined);
 8    req.onerror = () => reject(req.error);
 9  });
10}
  • تسترجع هذه الدالة عنصر Todo بالمعرّف المحدد وتعيد الكائن إذا تم العثور عليه۔ إذا لم يتم العثور على بيانات مطابقة، تُعيد undefined۔
 1function updateTodo(db: IDBDatabase, todo: Todo): Promise<void> {
 2  return new Promise((resolve, reject) => {
 3    const tx = db.transaction('todos', 'readwrite');
 4    const store = tx.objectStore('todos');
 5    const req = store.put(todo);
 6
 7    req.onsuccess = () => {
 8      console.log('Todo updated', todo.id);
 9    };
10    req.onerror = () => reject(req.error);
11
12    tx.oncomplete = () => resolve();
13    tx.onerror = () => reject(tx.error);
14  });
15}
  • تقوم هذه الدالة بتحديث بيانات Todo الموجودة۔ عند النجاح، يتم تسجيل معرف عنصر Todo الذي تم تحديثه۔
 1function deleteTodo(db: IDBDatabase, id: string): Promise<void> {
 2  return new Promise((resolve, reject) => {
 3    const tx = db.transaction('todos', 'readwrite');
 4    const store = tx.objectStore('todos');
 5    const req = store.delete(id);
 6
 7    req.onsuccess = () => {
 8      console.log('Todo deleted', id);
 9    };
10    req.onerror = () => reject(req.error);
11
12    tx.oncomplete = () => resolve();
13    tx.onerror = () => reject(tx.error);
14  });
15}
  • تحذف هذه الدالة عنصر Todo بالمعرّف المحدد۔ عند النجاح في المعالجة، يتم تسجيل رقم التعريف المحذوف۔

  • تقوم هذه الدوال بإنهاء أو رفض الوعد (Promise) حسب اكتمال المعاملة أو حدوث أخطاء۔ إضافة المخرجات عبر console.log يسهل تتبع ما يحدث أثناء التنفيذ۔

الفهارس والاستعلامات المركبة

من خلال استخدام الفهارس في IndexedDB، يمكنك البحث بكفاءة في الحقول المحددة۔ هنا، ننشئ فهرسًا لحقل createdAt ونقدم مثالًا لاستعلام نطاق۔

 1// When opening DB, create an index for createdAt.
 2// Then demonstrate a range query using the index.
 3
 4function openDatabaseWithIndex(dbName: string, version: number): Promise<IDBDatabase> {
 5  return new Promise((resolve, reject) => {
 6    const request = indexedDB.open(dbName, version);
 7
 8    request.onupgradeneeded = () => {
 9      const db = request.result;
10      if (!db.objectStoreNames.contains('todos')) {
11        const store = db.createObjectStore('todos', { keyPath: 'id' });
12        // Create an index on createdAt for sorting/filtering
13        store.createIndex('by-createdAt', 'createdAt', { unique: false });
14      } else {
15        const store = request.transaction!.objectStore('todos');
16        if (!store.indexNames.contains('by-createdAt')) {
17          store.createIndex('by-createdAt', 'createdAt', { unique: false });
18        }
19      }
20    };
21
22    request.onerror = () => reject(request.error);
23    request.onsuccess = () => resolve(request.result);
24  });
25}
  • تفتح هذه الدالة قاعدة البيانات وتنشئ أو تتحقق من وجود الفهرس by-createdAt على حقل createdAt۔ هذا يتيح البحث والترتيب بكفاءة حسب تاريخ الإنشاء۔
 1async function getTodosCreatedAfter(db: IDBDatabase, timestamp: number): Promise<Todo[]> {
 2  return new Promise((resolve, reject) => {
 3    const tx = db.transaction('todos', 'readonly');
 4    const store = tx.objectStore('todos');
 5    const index = store.index('by-createdAt');
 6    const range = IDBKeyRange.lowerBound(timestamp, true); // exclusive
 7    const req = index.openCursor(range);
 8
 9    const results: Todo[] = [];
10    req.onsuccess = (event) => {
11      const cursor = (event.target as IDBRequest).result as IDBCursorWithValue | null;
12      if (cursor) {
13        results.push(cursor.value as Todo);
14        cursor.continue();
15      } else {
16        resolve(results);
17      }
18    };
19    req.onerror = () => reject(req.error);
20  });
21}
  • تسترجع هذه الدالة فقط عناصر Todo التي تم إنشاؤها بعد الطابع الزمني المحدد۔ استخدام الفهرس يسمح بالمسح الفعال للبيانات حسب ترتيب تاريخ الإنشاء۔

  • في هذا المثال، يتم إنشاء فهرس by-createdAt أثناء ترقية قاعدة البيانات، ويتم تعداد عناصر Todo التي أنشئت بعد الوقت المحدد باستخدام المؤشر۔

غلاف خفيف الوزن مبني على Promise

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

1// A minimal TypeScript wrapper around IndexedDB to simplify common operations.
2// This class is generic over the store's value type and assumes 'keyPath' is 'id'.
3
4class IDBWrapper<T extends { id: string }> {
5  private dbPromise: Promise<IDBDatabase>;
6
7  constructor(private dbName: string, private version: number, private storeName: string) {
8    this.dbPromise = this.open();
9  }
  • تغلف هذه الفئة عمليات IndexedDB وتوفر طرق CRUD آمنة نوعياً۔ تفترض وجود مخزن كائنات حيث المفتاح هو id۔
 1  private open(): Promise<IDBDatabase> {
 2    return new Promise((resolve, reject) => {
 3      const req = indexedDB.open(this.dbName, this.version);
 4      req.onerror = () => reject(req.error);
 5      req.onupgradeneeded = () => {
 6        const db = req.result;
 7        if (!db.objectStoreNames.contains(this.storeName)) {
 8          db.createObjectStore(this.storeName, { keyPath: 'id' });
 9        }
10      };
11      req.onsuccess = () => resolve(req.result);
12    });
13  }
  • تفتح قاعدة البيانات وتنشئ مخزن كائنات جديد إذا لزم الأمر۔ يتم تهيئة المخزن باستخدام حدث الترقية۔
 1  async add(item: T): Promise<void> {
 2    const db = await this.dbPromise;
 3    await new Promise<void>((resolve, reject) => {
 4      const tx = db.transaction(this.storeName, 'readwrite');
 5      const store = tx.objectStore(this.storeName);
 6      const req = store.add(item);
 7      req.onsuccess = () => {
 8        console.log('added', item.id);
 9      };
10      req.onerror = () => reject(req.error);
11      tx.oncomplete = () => resolve();
12      tx.onerror = () => reject(tx.error);
13    });
14  }
  • يضيف بيانات إلى مخزن IndexedDB۔ بعد الإضافة، يتم تسجيل رقم التعريف في وحدة التحكم۔
 1  async get(id: string): Promise<T | undefined> {
 2    const db = await this.dbPromise;
 3    return new Promise((resolve, reject) => {
 4      const tx = db.transaction(this.storeName, 'readonly');
 5      const store = tx.objectStore(this.storeName);
 6      const req = store.get(id);
 7      req.onsuccess = () => resolve(req.result as T | undefined);
 8      req.onerror = () => reject(req.error);
 9    });
10  }
  • يسترجع البيانات المقابلة لرقم التعريف المحدد۔ undefined تتم إعادتها إذا لم تكن البيانات موجودة۔
 1  async put(item: T): Promise<void> {
 2    const db = await this.dbPromise;
 3    return new Promise((resolve, reject) => {
 4      const tx = db.transaction(this.storeName, 'readwrite');
 5      const store = tx.objectStore(this.storeName);
 6      const req = store.put(item);
 7      req.onsuccess = () => {
 8        console.log('put', item.id);
 9      };
10      req.onerror = () => reject(req.error);
11      tx.oncomplete = () => resolve();
12      tx.onerror = () => reject(tx.error);
13    });
14  }
  • يحدث البيانات الموجودة أو يضيف بيانات جديدة۔ بعد المعالجة، يتم تسجيل رقم التعريف المحدث۔
 1  async delete(id: string): Promise<void> {
 2    const db = await this.dbPromise;
 3    return new Promise((resolve, reject) => {
 4      const tx = db.transaction(this.storeName, 'readwrite');
 5      const store = tx.objectStore(this.storeName);
 6      const req = store.delete(id);
 7      req.onsuccess = () => {
 8        console.log('deleted', id);
 9      };
10      req.onerror = () => reject(req.error);
11      tx.oncomplete = () => resolve();
12      tx.onerror = () => reject(tx.error);
13    });
14  }
  • يحذف البيانات ذات رقم التعريف المحدد۔ في حال النجاح، يتم تسجيل رقم التعريف المحذوف۔
 1  async getAll(): Promise<T[]> {
 2    const db = await this.dbPromise;
 3    return new Promise((resolve, reject) => {
 4      const tx = db.transaction(this.storeName, 'readonly');
 5      const store = tx.objectStore(this.storeName);
 6      const req = store.getAll();
 7      req.onsuccess = () => resolve(req.result as T[]);
 8      req.onerror = () => reject(req.error);
 9    });
10  }
11}
  • يسترجع جميع البيانات في المخزن۔ القيمة المرجعة هي مصفوفة من النوع T۔
 1// Example usage with Todo type:
 2interface Todo {
 3  id: string;
 4  title: string;
 5  completed: boolean;
 6  createdAt: number;
 7}
 8
 9const todoStore = new IDBWrapper<Todo>('my-db', 1, 'todos');
10
11(async () => {
12  const newTodo: Todo = { id: '1', title: 'Learn IndexedDB', completed: false, createdAt: Date.now() };
13  await todoStore.add(newTodo);
14  const fetched = await todoStore.get('1');
15  console.log('fetched', fetched);
16  newTodo.completed = true;
17  await todoStore.put(newTodo);
18  const all = await todoStore.getAll();
19  console.log('all todos', all);
20  await todoStore.delete('1');
21})();
  • هذا الكود مثال فعلي لاستخدام فئة IDBWrapper۔ يوضح هذا سير العمل لإضافة، واسترجاع، وتحديث، وعرض، وحذف بيانات Todo۔

  • يتيح هذا الغلاف التعامل البسيط مع عمليات CRUD الأساسية۔ في بيئة العالم الحقيقي، تحتاج أيضًا إلى التعامل مع معالجة الأخطاء وإدارة المخطط (الفهارس)۔

ترحيل المخططات (ترقية الإصدار)

لتغيير مخطط قاعدة البيانات، قم بزيادة الوسيطة الثانية (الإصدار) عند استدعاء indexedDB.open وحدث المخطط في الحدث onupgradeneeded.۔ يجب أن تصمم النظام بحيث تنتهي المعاملات الحالية ويتم تجنب التغييرات التي قد تدمر البيانات.۔

 1// Example of handling upgrade to version 2: add an index and perhaps migrate data.
 2// onupgradeneeded receives an event where oldVersion and newVersion are accessible.
 3
 4function upgradeToV2(dbName: string): Promise<IDBDatabase> {
 5  return new Promise((resolve, reject) => {
 6    const req = indexedDB.open(dbName, 2);
 7    req.onupgradeneeded = (ev) => {
 8      const db = req.result;
 9      const oldVersion = (ev as IDBVersionChangeEvent).oldVersion;
10      console.log('Upgrading DB from', oldVersion, 'to', db.version);
11      let store: IDBObjectStore;
12      if (!db.objectStoreNames.contains('todos')) {
13        store = db.createObjectStore('todos', { keyPath: 'id' });
14      } else {
15        store = req.transaction!.objectStore('todos');
16      }
17      // Add index if not present
18      if (!store.indexNames.contains('by-completed')) {
19        store.createIndex('by-completed', 'completed', { unique: false });
20      }
21
22      // Optional: data migration logic if necessary can go here,
23      // but heavy migrations often should be done lazily on read.
24    };
25    req.onsuccess = () => resolve(req.result);
26    req.onerror = () => reject(req.error);
27  });
28}
  • المعالجة الثقيلة داخل onupgradeneeded قد تعيق واجهة المستخدم، لذا حافظ عليها في أدنى حد، وفكر في ترحيل مؤجل (معالجة أثناء بدء تشغيل التطبيق) إذا أمكن۔

احتياطات المعاملات (دورة الحياة والأخطاء)

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

 1// Bad pattern: awaiting outside transaction callbacks can cause tx to auto-commit.
 2// Good pattern is to chain requests and resolve on tx.oncomplete as shown earlier.
 3
 4// Example: Do multiple operations inside single tx, avoid awaiting inside.
 5function multiOperation(db: IDBDatabase, items: Todo[]): Promise<void> {
 6  return new Promise((resolve, reject) => {
 7    const tx = db.transaction('todos', 'readwrite');
 8    const store = tx.objectStore('todos');
 9
10    for (const item of items) {
11      const req = store.put(item);
12      req.onerror = () => console.error('put error', req.error);
13      // Do NOT await here; just schedule requests synchronously.
14    }
15
16    tx.oncomplete = () => {
17      console.log('All operations in transaction completed');
18      resolve();
19    };
20    tx.onerror = () => reject(tx.error);
21  });
22}
  • انتبه لمدة بقاء المعاملة؛ استخدم معاملات منفصلة عند الحاجة، أو نظم العمليات بشكل متزامن داخل المعاملة۔

تطبيقات المؤشر والتقطيع

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

 1// Example: fetch first N items using a cursor (ascending by key).
 2function fetchFirstN(db: IDBDatabase, n: number): Promise<Todo[]> {
 3  return new Promise((resolve, reject) => {
 4    const tx = db.transaction('todos', 'readonly');
 5    const store = tx.objectStore('todos');
 6    const req = store.openCursor();
 7    const out: Todo[] = [];
 8    req.onsuccess = (ev) => {
 9      const cursor = (ev.target as IDBRequest).result as IDBCursorWithValue | null;
10      if (cursor && out.length < n) {
11        out.push(cursor.value as Todo);
12        cursor.continue();
13      } else {
14        resolve(out);
15      }
16    };
17    req.onerror = () => reject(req.error);
18  });
19}
  • عبر جلب العناصر واحدة تلو الأخرى باستخدام المؤشرات، يمكن تقليل استخدام الذاكرة۔ عند تنفيذ التقطيع، من الشائع تذكر آخر مفتاح تمت قراءته۔

معالجة الأخطاء والتعامل الاحتياطي

1// Feature detection
2if (!('indexedDB' in window)) {
3  console.warn('IndexedDB is not supported. Falling back to localStorage.');
4  // implement fallback logic...
5}
  • قد لا تتوفر IndexedDB بسبب اختلافات التنفيذ بين المتصفحات أو إعدادات خصوصية المستخدمين، مثل التصفح الخاص.۔ لذلك، تحقق مما إذا كانت indexedDB موجودة، وإذا لم تكن كذلك، قدم بديلاً مثل localStorage

الأداء وأفضل الممارسات

IndexedDB سريع وقوي، لكن الأداء قد يختلف بشكل كبير حسب كيفية تصميمه وطريقة التعامل مع البيانات۔ اعتمادًا على حالة الاستخدام، يمكن تحسين الأداء بالطرق التالية:۔

  • صمم مخزن الكائنات وفقاً للاستخدام الفعلي۔ على سبيل المثال، إذا كان هناك العديد من عمليات القراءة، وفر الفهارس؛ إذا كانت هناك العديد من عمليات الكتابة، اجعل تصميم المفاتيح بسيطاً۔
  • يجب تخزين البيانات الثنائية الكبيرة مثل الصور والصوتيات كـ Blobs، أو إدارتها باستخدام File API أو العمال الخدميين إذا لزم الأمر۔ يمكن أيضًا النظر في الضغط إذا كان ذلك ضرورياً۔
  • اجعل المعاملات قصيرة قدر الإمكان وقم بتنفيذ المعالجة الثقيلة خارج المعاملة لتقليل وقت القفل۔
  • الفهارس يمكن أن تسرّع عمليات البحث، لكنها تبطئ عمليات الإدخال والتحديث، لذا أنشئ فقط الفهارس الضرورية حقًا۔
  • عند التعامل مع العديد من أجزاء البيانات الصغيرة، قد يؤدي استرجاعها جميعاً دفعة واحدة باستخدام getAll() إلى استنفاد الذاكرة۔ يمكنك تقليل استخدام الذاكرة عن طريق تقسيم المعالجة باستخدام المؤشرات۔

الأمان والخصوصية

بيانات IndexedDB معزولة حسب النطاق والبروتوكول وفقًا لسياسة الأصل نفسه۔ صمم النظام على افتراض أن البيانات قد تضيع إذا حذف المستخدم بيانات المتصفح أو استخدم وضع التصفح الخاص۔

الملخص وأنماط التصميم الموصى بها

لاستخدام IndexedDB بشكل فعال مع TypeScript، من المهم إعداد الأنواع والعمليات غير المتزامنة، والانتباه إلى إدارة الإصدارات وتصميم المعاملات، وتغليف العمليات الشائعة لتحسين قابلية الصيانة۔

  • تعريف الأنواع في تايب سكريبت ولف عمليات IndexedDB باستخدام Promise/async/await يحسن الأمان وقابلية قراءة الكود۔
  • يجب أن تستخدم تغييرات المخطط إدارة الإصدارات مع onupgradeneeded، وينبغي تأجيل العمليات الثقيلة إذا أمكن۔
  • صمم المعاملات لتكون قصيرة وتجنب المعالجة غير المتزامنة الثقيلة ضمن المعاملة نفسها۔
  • من خلال إنشاء فئات تغليف (wrapper classes)، يمكنك تقليل العمليات المتكررة مثل معالجة الأخطاء، وتسجيل الأحداث، وتعريف الأنواع۔

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

YouTube Video