IndexedDB لتخزين البيانات دون اتصال
IndexedDB هي قاعدة بيانات متصفح قوية تسمح لك بتخزين كميات كبيرة من البيانات المنظمة للاستخدام دون اتصال. على عكس localStorage، يمكن لـ IndexedDB تخزين كائنات معقدة، والتعامل مع مجموعات بيانات كبيرة، وتدعم الفهارس والمعاملات.
أساسيات IndexedDB
IndexedDB هو نظام قاعدة بيانات غير متزامن ومعاملاتي يخزن أزواج المفاتيح والقيم. إنه مثالي لتطبيقات الويب التقدمية التي تحتاج إلى تخزين كميات كبيرة من البيانات دون اتصال.
المفاهيم الأساسية:
- قاعدة البيانات: حاوية لمخازن الكائنات
- مخزن الكائنات: مشابه للجدول في قواعد بيانات SQL
- الفهرس: يسمح بالاستعلام بواسطة حقول غير المفتاح الأساسي
- المعاملة: تضمن سلامة البيانات أثناء العمليات
- المؤشر: آلية للتكرار عبر سجلات متعددة
فتح قاعدة البيانات
الخطوة الأولى هي فتح (أو إنشاء) قاعدة بيانات:
// فتح قاعدة البيانات
const dbName = 'myPWADatabase';
const dbVersion = 1;
const request = indexedDB.open(dbName, dbVersion);
request.onerror = (event) => {
console.error('خطأ في قاعدة البيانات:', event.target.error);
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log('تم فتح قاعدة البيانات بنجاح');
// استخدام db للعمليات
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// إنشاء مخزن الكائنات إذا لم يكن موجودًا
if (!db.objectStoreNames.contains('users')) {
const objectStore = db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true
});
// إنشاء الفهارس
objectStore.createIndex('email', 'email', { unique: true });
objectStore.createIndex('name', 'name', { unique: false });
console.log('تم إنشاء مخزن الكائنات');
}
};
عمليات CRUD
قم بإجراء عمليات الإنشاء والقراءة والتحديث والحذف باستخدام المعاملات:
// دالة مساعدة للحصول على قاعدة البيانات
function getDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('myPWADatabase', 1);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// إنشاء - إضافة سجل
async function addUser(user) {
const db = await getDatabase();
const transaction = db.transaction(['users'], 'readwrite');
const objectStore = transaction.objectStore('users');
return new Promise((resolve, reject) => {
const request = objectStore.add(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// قراءة - الحصول على سجل بالمفتاح الأساسي
async function getUser(id) {
const db = await getDatabase();
const transaction = db.transaction(['users'], 'readonly');
const objectStore = transaction.objectStore('users');
return new Promise((resolve, reject) => {
const request = objectStore.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// قراءة - الحصول على جميع السجلات
async function getAllUsers() {
const db = await getDatabase();
const transaction = db.transaction(['users'], 'readonly');
const objectStore = transaction.objectStore('users');
return new Promise((resolve, reject) => {
const request = objectStore.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// تحديث - تحديث سجل
async function updateUser(user) {
const db = await getDatabase();
const transaction = db.transaction(['users'], 'readwrite');
const objectStore = transaction.objectStore('users');
return new Promise((resolve, reject) => {
const request = objectStore.put(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// حذف - حذف سجل
async function deleteUser(id) {
const db = await getDatabase();
const transaction = db.transaction(['users'], 'readwrite');
const objectStore = transaction.objectStore('users');
return new Promise((resolve, reject) => {
const request = objectStore.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// أمثلة على الاستخدام
async function demonstrateCRUD() {
try {
// إنشاء
const userId = await addUser({
name: 'محمد أحمد',
email: 'mohammed@example.com',
age: 30
});
console.log('تمت إضافة المستخدم بالمعرف:', userId);
// قراءة
const user = await getUser(userId);
console.log('تم استرجاع المستخدم:', user);
// تحديث
user.age = 31;
await updateUser(user);
console.log('تم تحديث المستخدم');
// حذف
await deleteUser(userId);
console.log('تم حذف المستخدم');
} catch (error) {
console.error('فشلت عملية CRUD:', error);
}
}
استخدام الفهارس للاستعلامات
تسمح لك الفهارس بالاستعلام عن البيانات بواسطة حقول غير المفتاح الأساسي:
// الاستعلام بالبريد الإلكتروني باستخدام الفهرس
async function getUserByEmail(email) {
const db = await getDatabase();
const transaction = db.transaction(['users'], 'readonly');
const objectStore = transaction.objectStore('users');
const index = objectStore.index('email');
return new Promise((resolve, reject) => {
const request = index.get(email);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// الحصول على جميع المستخدمين باسم محدد
async function getUsersByName(name) {
const db = await getDatabase();
const transaction = db.transaction(['users'], 'readonly');
const objectStore = transaction.objectStore('users');
const index = objectStore.index('name');
return new Promise((resolve, reject) => {
const request = index.getAll(name);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
العمل مع المؤشرات
تسمح لك المؤشرات بالتكرار عبر السجلات بكفاءة:
async function iterateUsers() {
const db = await getDatabase();
const transaction = db.transaction(['users'], 'readonly');
const objectStore = transaction.objectStore('users');
return new Promise((resolve, reject) => {
const users = [];
const request = objectStore.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
users.push(cursor.value);
console.log('مستخدم:', cursor.value);
cursor.continue(); // الانتقال إلى السجل التالي
} else {
// لا توجد سجلات أخرى
resolve(users);
}
};
request.onerror = () => reject(request.error);
});
}
متى تستخدم المؤشرات:
- التكرار عبر مجموعات بيانات كبيرة
- تصفية السجلات بمنطق مخصص
- معالجة السجلات واحدًا تلو الآخر
- تنفيذ الترقيم
مكتبات غلاف IndexedDB
بينما IndexedDB الأصلية قوية، يمكن أن تكون مطولة. فكر في استخدام مكتبات الغلاف:
// استخدام مكتبة idb (https://github.com/jakearchibald/idb)
import { openDB } from 'idb';
const db = await openDB('myPWADatabase', 1, {
upgrade(db) {
const store = db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('email', 'email', { unique: true });
}
});
// عمليات CRUD تصبح أبسط بكثير
await db.add('users', { name: 'محمد', email: 'mohammed@example.com' });
const user = await db.get('users', 1);
await db.put('users', { id: 1, name: 'فاطمة', email: 'fatima@example.com' });
await db.delete('users', 1);
const allUsers = await db.getAll('users');
مزامنة البيانات دون اتصال
قم بتنفيذ استراتيجية مزامنة للحفاظ على بيانات محلية وخادم متزامنة:
class DataSync {
constructor(apiUrl, storeName) {
this.apiUrl = apiUrl;
this.storeName = storeName;
}
async syncFromServer() {
try {
// جلب البيانات من الخادم
const response = await fetch(this.apiUrl);
const serverData = await response.json();
// التخزين في IndexedDB
const db = await getDatabase();
const transaction = db.transaction([this.storeName], 'readwrite');
const objectStore = transaction.objectStore(this.storeName);
// مسح البيانات الموجودة
await objectStore.clear();
// إضافة بيانات الخادم
for (const item of serverData) {
await objectStore.add(item);
}
console.log('تمت مزامنة البيانات من الخادم');
return true;
} catch (error) {
console.error('فشلت المزامنة:', error);
return false;
}
}
async syncToServer() {
try {
// الحصول على التغييرات المحلية
const db = await getDatabase();
const transaction = db.transaction([this.storeName], 'readonly');
const objectStore = transaction.objectStore(this.storeName);
const localData = await objectStore.getAll();
// إرسال إلى الخادم
const response = await fetch(this.apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(localData)
});
if (response.ok) {
console.log('تمت مزامنة البيانات المحلية مع الخادم');
return true;
}
} catch (error) {
console.error('فشلت المزامنة مع الخادم:', error);
return false;
}
}
async autoSync() {
if (navigator.onLine) {
await this.syncFromServer();
await this.syncToServer();
}
}
}
// الاستخدام
const userSync = new DataSync('/api/users', 'users');
// المزامنة عند الاتصال
window.addEventListener('online', () => {
userSync.autoSync();
});
// مزامنة أولية
if (navigator.onLine) {
userSync.syncFromServer();
}
حدود التخزين: لدى IndexedDB حدود تخزين تختلف حسب المتصفح. تعامل دائمًا مع أخطاء تجاوز الحصة بلطف وأخبر المستخدمين عندما ينخفض التخزين.
التحقق من حصة التخزين
async function checkStorageQuota() {
if (navigator.storage && navigator.storage.estimate) {
const estimate = await navigator.storage.estimate();
const percentUsed = (estimate.usage / estimate.quota) * 100;
console.log(`التخزين المستخدم: ${estimate.usage} بايت`);
console.log(`حصة التخزين: ${estimate.quota} بايت`);
console.log(`النسبة المستخدمة: ${percentUsed.toFixed(2)}%`);
if (percentUsed > 80) {
console.warn('التخزين ينخفض!');
}
}
}
تمرين:
- أنشئ قاعدة بيانات IndexedDB لتطبيق قائمة مهام
- نفذ عمليات CRUD لعناصر المهام
- أضف فهرسًا للبحث في المهام حسب الحالة (مكتملة/قيد الانتظار)
- نفذ مزامنة البيانات بين IndexedDB وواجهة برمجة تطبيقات وهمية
- تعامل مع أخطاء تجاوز حصة التخزين