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

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

في هذا المقال، سنشرح جافاسكريبت و IndexedDB۔

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

YouTube Video

javascript-indexed-db.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <script>
111        // Override console.log to display messages in the #output element
112        (function () {
113            // Override console.log
114            const originalLog = console.log;
115            console.log = function (...args) {
116                originalLog.apply(console, args);
117                const message = document.createElement('div');
118                message.textContent = args
119                    .map(arg => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg)))
120                    .join(" ");
121                output.appendChild(message);
122            };
123
124            // Override console.error
125            const originalError = console.error;
126            console.error = function (...args) {
127                originalError.apply(console, args);
128                const message = document.createElement('div');
129                message.textContent = args
130                    .map(arg => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg)))
131                    .join(" ");
132                message.style.color = 'red'; // Color error messages red
133                output.appendChild(message);
134            };
135        })();
136
137        document.getElementById('executeBtn').addEventListener('click', () => {
138            // Prevent multiple loads
139            if (document.getElementById('externalScript')) return;
140
141            const script = document.createElement('script');
142            script.src = 'javascript-indexed-db.js';
143            script.id = 'externalScript';
144            //script.onload = () => console.log('javascript-indexed-db.js loaded and executed.');
145            //script.onerror = () => console.log('Failed to load javascript-indexed-db.js.');
146            document.body.appendChild(script);
147        });
148    </script>
149</body>
150</html>

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

IndexedDB هي قاعدة بيانات مفتاح-قيمة غير متزامنة مدمجة في المتصفحات۔ تقدم ميزات مشابهة لقواعد البيانات العلائقية وتسمح بتخزين والبحث ضمن كميات كبيرة من البيانات المنظمة على جانب العميل۔ وهي مفيدة بشكل خاص للتطبيقات التي تدعم العمل دون اتصال وتطبيقات الويب التقدمية (PWA)۔

ميزات IndexedDB

  • تعمل بطريقة غير متزامنة وقائمة على الأحداث۔
  • يمكن تخزين كائنات جافاسكريبت في متاجر الكائنات۔
  • البحث ممكن باستخدام الاستعلامات أو الفهارس۔
  • لديها سعة تخزين كبيرة (مئات الميجابايت وأكثر)، ما يسمح لك بتخزين بيانات أكثر بكثير من الكوكيز أو التخزين المحلي (localStorage)۔

فتح وإنشاء قاعدة بيانات

لاستخدام IndexedDB، يجب أولاً فتح قاعدة بيانات۔ إذا لم تكن موجودة، سيتم إنشاؤها تلقائيًا۔

1const request = indexedDB.open('MyDatabase', 1); // Specify DB name and version
  • دالة open تفتح قاعدة البيانات بشكل غير متزامن وتقوم بتشغيل الأحداث الثلاثة التالية۔

onsuccess

1// Fired when database is successfully opened
2request.onsuccess = (event) => {
3    const db = event.target.result; // Database instance
4    console.log('Database opened successfully:', db.name);
5};
  • يتم تشغيل حدث onsuccess عند فتح قاعدة البيانات بنجاح۔ يجب تنفيذ العمليات التالية باستخدام request.result، والذي يصبح متاحًا عند هذه النقطة۔

onerror

1// Fired when database fails to open
2request.onerror = (event) => {
3    console.error('Failed to open database:', event.target.error);
4};
  • يتم تشغيل حدث onerror عندما يفشل فتح قاعدة البيانات۔ يجب تنفيذ تسجيل ومعالجة الأخطاء هنا۔

onupgradeneeded

 1// Fired when database is newly created or upgraded
 2request.onupgradeneeded = (event) => {
 3    const db = event.target.result;
 4    console.log('Database upgrade needed (or newly created):', db.name);
 5
 6    // Example: Create an object store (like a table) if it doesn’t exist
 7    if (!db.objectStoreNames.contains('users')) {
 8        db.createObjectStore('users', { keyPath: 'id' });
 9        console.log('Object store "users" created');
10    }
11};
  • يتم تشغيل onupgradeneeded عندما يتم إنشاء قاعدة بيانات جديدة أو عند طلب إصدار أعلى من الإصدار الحالي۔ يجب إنشاء الجداول (متاجر الكائنات) وتعريف المخطط في هذا التوقيت۔

إنشاء متجر كائنات

أولاً، أنشئ 'متجر كائنات' (ما يعادل جدولاً) داخل حدث onupgradeneeded۔

1request.onupgradeneeded = function (event) {
2    const db = event.target.result;
3    console.log("onupgradeneeded triggered. Database version:", db.version);
4    if (!db.objectStoreNames.contains('users')) {
5        console.log("Creating object store: users");
6        const store = db.createObjectStore('users', { keyPath: 'id' });
7        store.createIndex('name', 'name', { unique: false });
8    }
9};

هنا يتم تطبيق الإعدادات التالية:۔

  • createObjectStore createObjectStore هي دالة لإنشاء متجر كائنات جديد داخل قاعدة البيانات۔ يمكنك تخزين السجلات في متجر الكائنات وتحديد طريقة تنظيم البيانات عبر تحديد keyPath وخيارات أخرى۔ يجب تنفيذ هذه العملية داخل حدث onupgradeneeded۔

  • keyPath: 'id' قم بتعيين keyPath إلى الخاصية id، والتي تحدد كل سجل بشكل فريد كمفتاح أساسي۔ يسمح ذلك باستخدام id تلقائيًا عند إضافة أو البحث أو تحديث البيانات۔

  • createIndex استخدم دالة createIndex لإنشاء فهرس استنادًا إلى خاصية name بغرض البحث۔ من خلال تعيين unique: false، يُسمح بوجود سجلات متعددة بنفس الاسم۔

معالجة اتصالات قاعدة البيانات الناجحة

قم بتعيين العمليات التي تنفذ عند نجاح الاتصال بقاعدة البيانات إلى حدث onsuccess۔ خلال هذه العملية، تحصل على كائن قاعدة البيانات وتستعد لعمليات القراءة والكتابة للبيانات۔

1request.onsuccess = function (event) {
2    const db = event.target.result;
3    console.log('Database opened successfully');
4    // Use db for reading and writing in subsequent operations
5};
  • يتم تنفيذ هذه العملية عندما يكتمل الاتصال بقاعدة بيانات IndexedDB بنجاح۔

  • event.target.result يحتوي على نسخة قاعدة البيانات المفتوحة (كائن IDBDatabase)، الذي يستخدم لبدء العمليات والوصول إلى متاجر الكائنات۔

  • يتم تنفيذ عمليات القراءة والكتابة الفعلية مثل الإضافة والاسترجاع والتحديث والحذف باستخدام كائن db۔

في هذه النقطة، تكون قاعدة البيانات جاهزة ويمكنك بدء العمليات بأمان۔

إضافة بيانات

هكذا يمكنك إضافة بيانات جديدة إلى IndexedDB۔

 1function addUser(db, user) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.add(user);
 5
 6    request.onsuccess = () => {
 7        console.log('User added:', user);
 8    };
 9
10    request.onerror = () => {
11        console.error('Add failed:', request.error);
12    };
13}
14
15// Example: Add a user
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    addUser(db, { id: 1, name: 'Alice' });
19    addUser(db, { id: 2, name: 'Bob' });
20    addUser(db, { id: 3, name: 'John' });
21};
  • أنشئ معاملة باستخدام db.transaction() وحدد متجر الكائنات ووضع العملية (في هذه الحالة readwrite
  • أضف البيانات الجديدة باستخدام دالة store.add()۔
  • تتم معالجة المعاملات تلقائيًا، ولكن إذا كنت تريد تجميع عمليات متعددة، يمكنك إدارتها باستخدام أحداث نهاية المعاملة۔

استرجاع البيانات (بحث بالمفتاح الأساسي)

هنا كيفية استرجاع بيانات محددة باستخدام المفتاح الأساسي۔

 1function getUserById(db, id) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.get(id);
 5
 6    request.onsuccess = () => {
 7        if (request.result) {
 8            console.log('User found:', request.result);
 9        } else {
10            console.log('User not found');
11        }
12    };
13
14    request.onerror = () => {
15        console.error('Error retrieving user:', request.error);
16    };
17}
18
19// Example: Get a user by id
20request.onsuccess = function (event) {
21    const db = event.target.result;
22    getUserById(db, 1);
23};
  • أنشئ معاملة للقراءة فقط باستخدام db.transaction()۔
  • استخدم store.get(id) لاسترجاع البيانات التي تتطابق مع المفتاح الأساسي المحدد۔
  • يتم استدعاء onsuccess عند نجاح الاسترجاع، ويتم عرض النتيجة إذا كانت متوفرة۔ إذا لم تكن هناك نتيجة، يتم اعتبار أنه 'لا توجد بيانات مطابقة.'۔

استخدام البحث عبر الفهرس

إذا أردت البحث باستخدام خاصية غير المفتاح الأساسي، استخدم الفهرس الذي أنشأته مسبقًا۔

 1function getUserByName(db, name) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name');
 5    const request = index.get(name);
 6
 7    request.onsuccess = () => {
 8        if (request.result) {
 9            console.log('User by name:', request.result);
10        } else {
11            console.log('No user found with that name');
12        }
13    };
14
15    request.onerror = () => {
16        console.error('Error retrieving user by name:', request.error);
17    };
18}
19
20// Example: Get a user by id
21request.onsuccess = function (event) {
22    const db = event.target.result;
23    getUserByName(db, 'Alice');
24};
  • قم بالوصول إلى فهرس name الذي أنشئ مسبقًا عبر store.index('name')۔
  • index.get(value) يسترجع أول سجل يطابق القيمة المحددة۔ إذا كان هناك سجلات متعددة بنفس القيمة، يمكنك استرجاعها جميعًا باستخدام index.getAll(value)۔

تحديث البيانات

لتحديث البيانات الموجودة (استبدالها)، استخدم دالة put()۔

إذا كان هناك سجل يطابق المفتاح الأساسي، سيتم تحديثه؛ وإذا لم يوجد، سيتم إضافة سجل جديد۔

 1function updateUser(db, updatedUser) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.put(updatedUser);
 5
 6    request.onsuccess = () => {
 7        console.log('User updated:', updatedUser);
 8    };
 9
10    request.onerror = () => {
11        console.error('Update failed:', request.error);
12    };
13}
14
15// Example: Update user
16request.onsuccess = async (event) => {
17    const db = event.target.result;
18
19    // Test : update existing user
20    updateUser(db, { id: 3, name: 'John Updated' });
21
22    // Test : insert new user
23    updateUser(db, { id: 4, name: 'Charlie' });
24};
  • put() طريقة عملية تدعم كل من التحديث والإضافة۔
  • إذا كان هناك بيانات بنفس المفتاح الأساسي، سيتم الكتابة فوقها۔
  • إذا أردت التأكد من وجود البيانات قبل التحديث، يمكنك استخدام get() للتحقق مسبقًا۔

حذف البيانات

لحذف بيانات بناءً على مفتاح أساسي محدد، استخدم دالة delete()۔

 1function deleteUser(db, id) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.delete(id);
 5
 6    request.onsuccess = () => {
 7        console.log(`User with id=${id} deleted successfully`);
 8    };
 9
10    request.onerror = () => {
11        console.error(`Failed to delete user with id=${id}:`, request.error);
12    };
13}
14
15// Example: Delete a user by id
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    deleteUser(db, 4);
19};
  • استخدم store.delete(id) لحذف البيانات التي تتطابق مع المفتاح الأساسي المحدد۔
  • يرجى ملاحظة أنه حتى إذا لم توجد البيانات، فلن يحدث خطأ وسيعتبر الحذف ناجحًا۔
  • تنفيذ معالجة الأخطاء سيجعل الكود أكثر قوة وموثوقية۔

استرجاع جميع البيانات

getAll()

لاسترجاع جميع السجلات في متجر الكائنات، استخدم دالة getAll()۔

 1function getAllUsers(db) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.getAll();
 5
 6    request.onsuccess = () => {
 7        console.log('All users:', request.result);
 8    };
 9
10    request.onerror = () => {
11        console.error('Failed to retrieve users:', request.error);
12    };
13}
14
15// Example: Get all users
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    getAllUsers(db);
19};
  • getAll() تسترجع جميع السجلات في متجر الكائنات المحدد على شكل مصفوفة۔
  • حتى عند استرجاع كميات كبيرة من البيانات دفعة واحدة، تتم معالجتها بكفاءة۔
  • يتم تخزين النتائج كمصفوفة في request.result۔
  • قم دائمًا بإضافة معالجة للأخطاء لتتمكن من التعامل مع حالات الفشل۔

openCursor()

openCursor() دالة للتنقل تسلسليًا بين السجلات في متجر الكائنات أو الفهرس۔ هي مفيدة عندما ترغب بمعالجة البيانات عنصراً تلو الآخر بدلاً من استرجاع كميات ضخمة دفعة واحدة۔

 1function getAllUsersWithCursor(db) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.openCursor();
 5
 6    request.onsuccess = () => {
 7        const cursor = request.result;
 8        if (cursor) {
 9            console.log('User:', cursor.value); // Process the current record
10            cursor.continue(); // Move to the next record
11        } else {
12            console.log('All users have been processed.');
13        }
14    };
15
16    request.onerror = () => {
17        console.error('Failed to open cursor:', request.error);
18    };
19}
20
21// Example: Get all users
22request.onsuccess = function (event) {
23    const db = event.target.result;
24    getAllUsersWithCursor(db);
25};
  • عند استخدام openCursor()، تبدأ مؤشرًا وتسترجع كافة السجلات في متجر الكائنات واحدًا تلو الآخر۔
  • استخدم cursor.value للحصول على كائن البيانات للسجل الحالي۔
  • حرك المؤشر للسجل التالي باستخدام cursor.continue()۔
  • عندما تكون cursor === null، فهذا يعني أنه تم استعراض جميع السجلات۔

مثال على عملية التحديث باستخدام openCursor()

على سبيل المثال، عملية تغيير اسم المستخدم الذي اسمه Alice إلى Alicia ستكون كما يلي:۔

 1function updateUserName(db, oldName, newName) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name'); // Use the 'name' index
 5    const request = index.openCursor(IDBKeyRange.only(oldName));
 6
 7    request.onsuccess = () => {
 8        const cursor = request.result;
 9        if (cursor) {
10            const user = cursor.value;
11            user.name = newName; // Update the name
12            const updateRequest = cursor.update(user);
13
14            updateRequest.onsuccess = () => {
15                console.log('Updated user:', user);
16            };
17
18            updateRequest.onerror = () => {
19                console.error('Failed to update user:', updateRequest.error);
20            };
21
22            cursor.continue();
23        } else {
24            console.log('All matching users have been updated.');
25        }
26    };
27
28    request.onerror = () => {
29        console.error('Cursor error:', request.error);
30    };
31}
32
33// Example: Update user name
34request.onsuccess = function (event) {
35    const db = event.target.result;
36    updateUserName(db, 'Alice', 'Alicia');
37};
  • IDBKeyRange.only(oldName) من خلال استخدام IDBKeyRange.only، يمكنك استهداف السجلات التي يتطابق مفتاحها تمامًا مع oldName فقط۔ هذا مفيد عندما تريد الوصول إلى قيمة معينة بشكل مباشر۔

  • cursor.update() بعد تحديث cursor.value، سيؤدي استدعاء update() إلى الكتابة فوق السجل المقابل۔

  • معالجة المطابقات المتعددة من خلال استدعاء cursor.continue()، يمكنك نقل المؤشر إلى السجل المطابق التالي۔ يسمح لك هذا بمعالجة عدة سجلات تطابق نفس المفتاح أو الشرط بالتتابع۔

  • معالجة الأخطاء من خلال إخراج السجلات في onerror عند فشل العملية، يصبح من الأسهل البحث عن الأسباب وحل المشكلات أثناء التشغيل۔

مثال على عملية الحذف باستخدام openCursor()

على سبيل المثال، عملية حذف جميع المستخدمين الذين اسمهم Bob ستكون كما يلي:۔

 1function deleteUsersByName(db, targetName) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name');
 5    const request = index.openCursor(IDBKeyRange.only(targetName));
 6
 7    request.onsuccess = () => {
 8        const cursor = request.result;
 9        if (cursor) {
10            const deleteRequest = cursor.delete();
11
12            deleteRequest.onsuccess = () => {
13                console.log('Deleted user:', cursor.value);
14            };
15
16            deleteRequest.onerror = () => {
17                console.error('Failed to delete user:', deleteRequest.error);
18            };
19
20            cursor.continue();
21        } else {
22            console.log('All matching users have been deleted.');
23        }
24    };
25
26    request.onerror = () => {
27        console.error('Cursor error:', request.error);
28    };
29}
30
31// Example: Delete user by name
32request.onsuccess = function (event) {
33    const db = event.target.result;
34    deleteUsersByName(db, 'Bob');
35};
  • cursor.delete() باستخدام cursor.delete()، يتم حذف السجل في موضع المؤشر الحالي۔ نظرًا لأن النتيجة تُسترجع بشكل غير متزامن، يمكنك التحقق من العملية في onsuccess۔

ملاحظات حول المعاملات والمعالجة غير المتزامنة

IndexedDB غير متزامن ويعتمد على الأحداث۔ يجب التعامل مع جميع العمليات من خلال أحداث onsuccess أو onerror۔ عندما تريد تجميع عدة عمليات معًا، من الملائم تغليفها في Promise۔

 1function openDatabase() {
 2    return new Promise((resolve, reject) => {
 3        const request = indexedDB.open('MyDatabase', 1);
 4
 5        request.onupgradeneeded = function (event) {
 6            const db = event.target.result;
 7
 8            // Initialization process (creating object stores and indexes, etc.)
 9            const store = db.createObjectStore('users', { keyPath: 'id' });
10            store.createIndex('name', 'name', { unique: false });
11        };
12
13        request.onsuccess = function (event) {
14            const db = event.target.result;
15            resolve(db);
16        };
17
18        request.onerror = function () {
19            reject(request.error);
20        };
21    });
22}
23
24function addUserAsync(db, user) {
25    return new Promise((resolve, reject) => {
26        const transaction = db.transaction('users', 'readwrite');
27        const store = transaction.objectStore('users');
28        const request = store.add(user);
29
30        request.onsuccess = () => resolve();
31        request.onerror = () => reject(request.error);
32    });
33}
34
35async function main() {
36    try {
37        const db = await openDatabase();
38        await addUserAsync(db, { id: 1, name: 'Alice' });
39        console.log('User added successfully');
40    } catch (error) {
41        console.error('Error:', error);
42    }
43}
  • تغليف وظائف مثل openDatabase أو addUserAsync بـ Promise يجعل من الممكن التعامل مع العمليات غير المتزامنة بشكل بديهي باستخدام async/await۔
  • هذا يتجنب جحيم الاستدعاءات المتداخلة ويجعل الشفرة أكثر قابلية للقراءة۔

الملخص

IndexedDB ميزة قوية جدًا عندما تريد إجراء إدارة بيانات متقدمة على جانب المتصفح۔ في البداية، قد تشعر بالحيرة من المعالجة غير المتزامنة المعتمدة على الأحداث، ولكن بمجرد أن تفهم البنية، يمكنك تنفيذ عمليات بيانات كاملة على جانب العميل۔

على وجه الخصوص، ستساعدك مراعاة النقاط التالية على استخدامه بشكل أكثر سلاسة:۔

  • قم بتنفيذ الإعداد الأولي باستخدام onupgradeneeded۔
  • انتبه إلى وضع القراءة/الكتابة للمعاملات۔
  • الفهارس تتيح البحث بكفاءة۔
  • يمكن تخزين البيانات ككائنات، مما يجعلها متوافقة جدًا مع JSON۔

بإتقانك لـIndexedDB، تصبح إدارة البيانات لتطبيقات PWA والتطبيقات غير المتصلة أسهل بكثير۔

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

YouTube Video