JavaScript và IndexedDB

JavaScript và IndexedDB

Trong bài viết này, chúng tôi sẽ giải thích về JavaScript và IndexedDB.

Hướng dẫn này cung cấp giải thích từng bước về JavaScript và IndexedDB, bao gồm mã mẫu thực tế ở mỗi bước để giúp bạn hiểu sâu hơn.

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 và IndexedDB

IndexedDB là một cơ sở dữ liệu lưu trữ khóa-giá trị bất đồng bộ được tích hợp trong trình duyệt. Nó cung cấp các tính năng tương tự như cơ sở dữ liệu quan hệ và cho phép bạn lưu trữ, tìm kiếm một lượng lớn dữ liệu có cấu trúc ở phía máy khách. Nó đặc biệt hữu ích cho các ứng dụng có khả năng hoạt động ngoại tuyến và các PWAs (Progressive Web Apps).

Các tính năng của IndexedDB

  • Hoạt động theo cách bất đồng bộ và dựa trên sự kiện.
  • Các đối tượng JavaScript có thể được lưu trữ trong object stores.
  • Có thể tìm kiếm bằng truy vấn hoặc chỉ mục.
  • Nó có dung lượng lưu trữ lớn (hàng trăm MB hoặc hơn), cho phép bạn lưu trữ nhiều dữ liệu hơn nhiều so với cookies hoặc localStorage.

Mở và Tạo cơ sở dữ liệu

Để sử dụng IndexedDB, trước tiên bạn cần mở một cơ sở dữ liệu. Nếu cơ sở dữ liệu chưa tồn tại, nó sẽ được tạo tự động.

1const request = indexedDB.open('MyDatabase', 1); // Specify DB name and version
  • Phương thức open mở cơ sở dữ liệu một cách bất đồng bộ và kích hoạt ba sự kiện sau.

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};
  • Sự kiện onsuccess được kích hoạt khi cơ sở dữ liệu được mở thành công. Các thao tác tiếp theo nên được thực hiện bằng request.result, giá trị này sẽ khả dụng tại thời điểm này.

onerror

1// Fired when database fails to open
2request.onerror = (event) => {
3    console.error('Failed to open database:', event.target.error);
4};
  • Sự kiện onerror được kích hoạt khi cơ sở dữ liệu không thể mở. Đăng nhập lỗi và xử lý lỗi nên được thực hiện tại đây.

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};
  • Sự kiện onupgradeneeded được kích hoạt khi cơ sở dữ liệu được tạo mới hoặc khi phiên bản được chỉ định cao hơn phiên bản hiện tại. Việc tạo bảng (object stores) và xác định lược đồ nên được thực hiện vào thời điểm này.

Tạo một Object Store

Đầu tiên, hãy tạo một 'object store' (tương đương với bảng) trong sự kiện 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};

Tại đây, các cài đặt sau được áp dụng:.

  • createObjectStore createObjectStore là phương thức dùng để tạo một object store mới trong cơ sở dữ liệu. Bạn có thể lưu trữ dữ liệu vào object store và xác định cách quản lý dữ liệu bằng cách chỉ định keyPath và các tùy chọn khác. Quá trình này phải được thực hiện bên trong sự kiện onupgradeneeded.

  • keyPath: 'id' Đặt keyPath thành thuộc tính id, thuộc tính này xác định duy nhất mỗi bản ghi như một khóa chính. Điều này cho phép id được sử dụng tự động khi thêm, tìm kiếm hoặc cập nhật dữ liệu.

  • createIndex Sử dụng phương thức createIndex để tạo một chỉ mục dựa trên thuộc tính name nhằm mục đích tìm kiếm. Bằng cách đặt unique: false, cho phép nhiều bản ghi có cùng giá trị name.

Xử lý kết nối cơ sở dữ liệu thành công

Gán các xử lý cần thực hiện khi kết nối cơ sở dữ liệu thành công cho sự kiện onsuccess. Trong quá trình này, bạn lấy thể hiện của cơ sở dữ liệu và chuẩn bị để đọc và ghi dữ liệu.

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};
  • Quy trình này sẽ được thực hiện khi kết nối đến cơ sở dữ liệu IndexedDB hoàn tất thành công.

  • event.target.result chứa thể hiện cơ sở dữ liệu đã mở (IDBDatabase), được dùng để bắt đầu giao dịch và truy cập các object store.

  • Các thao tác đọc ghi thực tế như thêm, lấy, cập nhật, xóa dữ liệu được thực hiện bằng đối tượng db.

Tại thời điểm này, cơ sở dữ liệu đã sẵn sàng nên bạn có thể bắt đầu các giao dịch một cách an toàn.

Thêm dữ liệu

Cách thêm dữ liệu mới vào IndexedDB như sau.

 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};
  • Tạo một giao dịch với db.transaction() và chỉ định object store sẽ thao tác cùng với chế độ (trong trường hợp này là readwrite).
  • Thêm dữ liệu mới bằng phương thức store.add().
  • Các giao dịch sẽ được tự động cam kết, nhưng nếu bạn muốn nhóm nhiều thao tác, bạn có thể quản lý chúng bằng các sự kiện kết thúc giao dịch.

Lấy dữ liệu (Tìm kiếm theo khóa chính)

Đây là cách để lấy dữ liệu cụ thể bằng khóa chính.

 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};
  • Tạo một giao dịch chỉ đọc với db.transaction().
  • Dùng store.get(id) để lấy dữ liệu tương ứng với khóa chính được chỉ định.
  • onsuccess được gọi khi việc lấy dữ liệu thành công và kết quả sẽ được hiển thị nếu có. Nếu không có kết quả, nó sẽ được coi là 'không có dữ liệu tương ứng.'.

Sử dụng tìm kiếm chỉ mục

Nếu bạn muốn tìm kiếm theo thuộc tính khác ngoài khóa chính, hãy sử dụng chỉ mục mà bạn đã tạo trước đó.

 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};
  • Truy cập chỉ mục name đã được tạo trước đó bằng store.index('name').
  • index.get(value) sẽ lấy bản ghi đầu tiên có giá trị phù hợp. Nếu có nhiều bản ghi cùng giá trị, bạn có thể lấy tất cả bằng index.getAll(value).

Cập nhật dữ liệu

Để cập nhật dữ liệu hiện có (ghi đè), hãy sử dụng phương thức put().

Nếu có một bản ghi trùng khóa chính, nó sẽ được cập nhật; nếu không, một bản ghi mới sẽ được thêm vào.

 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() là phương thức tiện lợi vừa hỗ trợ cập nhật vừa hỗ trợ thêm mới.
  • Nếu đã tồn tại dữ liệu có cùng khóa chính, nó sẽ bị ghi đè.
  • Nếu muốn kiểm tra sự tồn tại trước khi cập nhật, bạn có thể dùng get() để xác nhận trước.

Xóa dữ liệu

Để xóa dữ liệu tương ứng với khóa chính được chỉ định, hãy dùng phương thức 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};
  • Dùng store.delete(id) để xóa dữ liệu có khóa chính phù hợp.
  • Lưu ý rằng ngay cả khi dữ liệu không tồn tại, sẽ không có lỗi nào xảy ra và thao tác này vẫn được coi là thành công.
  • Việc triển khai xử lý lỗi sẽ giúp mã của bạn mạnh mẽ hơn.

Lấy tất cả dữ liệu

getAll()

Để lấy tất cả bản ghi trong object store, hãy dùng phương thức 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() sẽ lấy tất cả bản ghi trong object store đã chỉ định dưới dạng một mảng.
  • Ngay cả khi lấy một lượng lớn dữ liệu cùng lúc, nó vẫn được xử lý hiệu quả.
  • Kết quả sẽ được lưu trữ dưới dạng mảng trong request.result.
  • Luôn thêm xử lý lỗi để có thể xử lý các trường hợp thất bại.

openCursor()

openCursor() là phương thức để duyệt lần lượt các bản ghi trong object store hoặc chỉ mục. Nó hữu ích khi bạn muốn xử lý dữ liệu từng bản ghi một thay vì lấy tất cả cùng lúc.

 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};
  • Bằng cách sử dụng openCursor(), bạn bắt đầu con trỏ và lấy từng bản ghi một trong object store.
  • Dùng cursor.value để lấy đối tượng dữ liệu của bản ghi hiện tại.
  • Di chuyển con trỏ đến bản ghi tiếp theo với cursor.continue().
  • Khi cursor === null, tất cả các bản ghi đã được duyệt qua.

Ví dụ về quy trình cập nhật sử dụng openCursor()

Ví dụ, quá trình thay đổi tên của người dùng có nameAlice thành Alicia sẽ như sau:.

 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) Bằng cách sử dụng IDBKeyRange.only, bạn có thể chỉ định riêng các bản ghi có khóa trùng khớp chính xác với oldName. Điều này hữu ích khi bạn muốn truy cập trực tiếp một giá trị cụ thể.

  • cursor.update() Sau khi cập nhật cursor.value, gọi update() sẽ ghi đè lên bản ghi tương ứng.

  • Xử lý nhiều trường hợp trùng khớp Bằng cách gọi cursor.continue(), bạn có thể di chuyển con trỏ sang bản ghi trùng khớp tiếp theo. Điều này cho phép bạn xử lý nhiều bản ghi trùng khớp với cùng một khóa hoặc điều kiện theo thứ tự.

  • Xử lý lỗi Bằng cách xuất nhật ký trong onerror khi một quá trình thất bại, bạn sẽ dễ dàng điều tra nguyên nhân và khắc phục sự cố trong quá trình vận hành.

Ví dụ về quy trình xóa sử dụng openCursor()

Ví dụ, quá trình xóa tất cả người dùng có nameBob sẽ như sau:.

 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() Với cursor.delete(), bản ghi tại vị trí con trỏ hiện tại sẽ bị xóa. Vì kết quả được trả về bất đồng bộ, bạn có thể kiểm tra quá trình trong onsuccess.

Lưu ý về giao dịch và xử lý bất đồng bộ

IndexedDB là bất đồng bộ và dựa trên sự kiện. Tất cả các thao tác cần được xử lý bằng các sự kiện onsuccess hoặc onerror. Khi bạn muốn nhóm nhiều quá trình lại với nhau, việc đóng gói chúng trong một Promise sẽ rất tiện lợi.

 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}
  • Đóng gói các hàm như openDatabase hoặc addUserAsync bằng Promise cho phép xử lý trực quan các quá trình bất đồng bộ với async/await.
  • Điều này giúp tránh tình trạng callback hell và làm cho mã nguồn dễ đọc hơn.

Tóm tắt

IndexedDB là một tính năng rất mạnh mẽ khi bạn muốn thực hiện quản lý dữ liệu nâng cao trên phía trình duyệt. Ban đầu, bạn có thể thấy bối rối với việc xử lý bất đồng bộ dựa trên sự kiện, nhưng khi đã hiểu cấu trúc, bạn có thể thực hiện các thao tác dữ liệu toàn diện ở phía khách hàng.

Đặc biệt, ghi nhớ những điểm sau đây sẽ giúp bạn sử dụng nó thuận lợi hơn:.

  • Thực hiện thiết lập ban đầu bằng onupgradeneeded.
  • Chú ý đến chế độ đọc/ghi của giao dịch.
  • Chỉ mục cho phép tìm kiếm hiệu quả.
  • Dữ liệu có thể được lưu trữ dưới dạng đối tượng, rất tương thích với JSON.

Khi thành thạo IndexedDB, việc quản lý dữ liệu cho PWA và các ứng dụng ngoại tuyến sẽ trở nên dễ dàng hơn nhiều.

Bạn có thể làm theo bài viết trên bằng cách sử dụng Visual Studio Code trên kênh YouTube của chúng tôi. Vui lòng ghé thăm kênh YouTube.

YouTube Video