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 & 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ằngrequest.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ỉ địnhkeyPath
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ệnonupgradeneeded
. -
keyPath: 'id'
ĐặtkeyPath
thành thuộc tínhid
, 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épid
đượ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ứccreateIndex
để tạo một chỉ mục dựa trên thuộc tínhname
nhằm mục đích tìm kiếm. Bằng cách đặtunique: 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ằngstore.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ằngindex.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ó name
là Alice
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ụngIDBKeyRange.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ớioldName
. Đ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ậtcursor.value
, gọiupdate()
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ó name
là Bob
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ớicursor.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 trongonsuccess
.
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ặcaddUserAsync
bằngPromise
cho phép xử lý trực quan các quá trình bất đồng bộ vớiasync/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.