JavaScript ו-IndexedDB

JavaScript ו-IndexedDB

במאמר זה נסביר על JavaScript ו-IndexedDB.

מדריך זה מסביר שלב אחר שלב על JavaScript ו־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>

JavaScript ו-IndexedDB

IndexedDB היא מסד נתונים אסינכרוני של מפתחות-ערכים המובנה בדפדפנים. הוא מספק תכונות הדומות למסדי נתונים יחסיים ומאפשר לאחסן ולחפש כמויות גדולות של נתונים ממוסדים בצד הלקוח. הוא שימושי במיוחד עבור יישומים שיכולים לפעול במצב לא מקוון ו-PWA (אפליקציות רשת מתקדמות).

מאפייני IndexedDB

  • פועל באופן אסינכרוני ומבוסס ארועים.
  • אובייקטים של JavaScript יכולים להישמר בתוך מחסני אובייקטים.
  • ניתן לבצע חיפוש באמצעות שאילתות או אינדקסים.
  • יש לו קיבולת אחסון גדולה (מאות מגה-בייט ואף יותר), מה שמאפשר לאחסן הרבה יותר נתונים מאשר בעוגיות או 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, מותרות מספר רשומות עם אותו name.

טיפול בחיבורי מסד נתונים מוצלחים

שייך תהליכים לביצוע כאשר מסד הנתונים נפתח בהצלחה לאירוע 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) ולשלוף את כל הרשומות אחת-אחת.
  • השתמש ב־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.
  • כך נמנעים מ-Callback Hell והקוד הופך לקריא יותר.

סיכום

IndexedDB היא תכונה עוצמתית במיוחד כאשר רוצים לנהל נתונים מתקדמים בצד הדפדפן. בהתחלה ייתכן שתהיה מבולבל מעיבוד אסינכרוני מבוסס אירועים, אך כשתבין את המבנה, תוכל לבצע פעולות נתונים מלאות בצד הלקוח.

במיוחד, שמירה על הנקודות הבאות תסייע לך להשתמש בו בצורה חלקה יותר:.

  • בצע הגדרה ראשונית באמצעות onupgradeneeded.
  • שים לב למצב קריאה/כתיבה של הטרנזקציות.
  • אינדקסים מאפשרים חיפוש יעיל.
  • הנתונים יכולים להישמר כאובייקטים, מה שהופך אותם לתואמים מאוד ל-JSON.

על ידי שליטה ב-IndexedDB, ניהול נתונים עבור אפליקציות PWA ואפליקציות לא מקוונות הופך לפשוט הרבה יותר.

תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.

YouTube Video