Stories

Detail Return Return

被前端存儲坑到崩潰?IndexedDB 高效用法幫你少走 90% 彎路 - Stories Detail

IndexedDB 是一種在瀏覽器中提供事務性的鍵值對存儲的低級 API。它允許你在用户的瀏覽器中存儲大量結構化數據,並且可以對其進行高效的搜索、更新和刪除操作。IndexedDB 適用於需要離線存儲和快速訪問大量數據的應用程序,如 Progressive Web Apps (PWAs) 和單頁應用程序 (SPAs)。本文將詳細介紹如何在前端項目中高效使用 IndexedDB。

1. IndexedDB 基本概念

1.1 數據庫

數據庫是存儲對象集合的地方。每個數據庫都有一個名稱和版本號。

1.2 對象存儲空間(Object Store)

對象存儲空間類似於關係型數據庫中的表。每個對象存儲空間可以包含一組對象,並且每個對象都有一個唯一的鍵。

1.3 索引(Index)

索引用於快速查找對象存儲空間中的對象。索引可以基於對象的屬性創建,從而加速查詢操作。

1.4 事務(Transaction)

事務是一組操作的集合,這些操作要麼全部成功,要麼全部失敗。事務可以保證數據的一致性和完整性。

1.5 請求(Request)

請求用於執行數據庫操作,如添加、刪除或獲取數據。請求是異步的,可以通過事件監聽器處理結果。

2. IndexedDB 基本操作

2.1 打開數據庫

const request = indexedDB.open('myDatabase', 1);

request.onupgradeneeded = function(event) {
    const db = event.target.result;
    // 創建對象存儲空間
    if (!db.objectStoreNames.contains('myStore')) {
        const objectStore = db.createObjectStore('myStore', { keyPath: 'id', autoIncrement: true });
        // 創建索引
        objectStore.createIndex('nameIndex', 'name', { unique: false });
    }
};

request.onsuccess = function(event) {
    const db = event.target.result;
    console.log('Database opened successfully');
};

request.onerror = function(event) {
    console.error('Database error:', event.target.errorCode);
};

2.2 添加數據

function addData(db, data) {
    const transaction = db.transaction(['myStore'], 'readwrite');
    const objectStore = transaction.objectStore('myStore');

    const request = objectStore.add(data);

    request.onsuccess = function(event) {
        console.log('Data added successfully');
    };

    request.onerror = function(event) {
        console.error('Error adding data:', event.target.errorCode);
    };
}

// 使用示例
const db = request.result;
addData(db, { name: 'John Doe', age: 30 });

2.3 獲取數據

function getData(db, id) {
    const transaction = db.transaction(['myStore'], 'readonly');
    const objectStore = transaction.objectStore('myStore');

    const request = objectStore.get(id);

    request.onsuccess = function(event) {
        const data = event.target.result;
        console.log('Data retrieved:', data);
    };

    request.onerror = function(event) {
        console.error('Error retrieving data:', event.target.errorCode);
    };
}

// 使用示例
getData(db, 1);

2.4 更新數據

function updateData(db, id, newData) {
    const transaction = db.transaction(['myStore'], 'readwrite');
    const objectStore = transaction.objectStore('myStore');

    const request = objectStore.get(id);

    request.onsuccess = function(event) {
        const data = event.target.result;
        if (data) {
            Object.assign(data, newData);
            const updateRequest = objectStore.put(data);

            updateRequest.onsuccess = function() {
                console.log('Data updated successfully');
            };

            updateRequest.onerror = function(event) {
                console.error('Error updating data:', event.target.errorCode);
            };
        } else {
            console.error('Data not found');
        }
    };

    request.onerror = function(event) {
        console.error('Error retrieving data:', event.target.errorCode);
    };
}

// 使用示例
updateData(db, 1, { age: 31 });

2.5 刪除數據

function deleteData(db, id) {
    const transaction = db.transaction(['myStore'], 'readwrite');
    const objectStore = transaction.objectStore('myStore');

    const request = objectStore.delete(id);

    request.onsuccess = function(event) {
        console.log('Data deleted successfully');
    };

    request.onerror = function(event) {
        console.error('Error deleting data:', event.target.errorCode);
    };
}

// 使用示例
deleteData(db, 1);

3. 使用 Promise 封裝 IndexedDB 操作

為了簡化異步操作,可以使用 Promise 封裝 IndexedDB 的基本操作。

function openDatabase(name, version) {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(name, version);

        request.onupgradeneeded = function(event) {
            const db = event.target.result;
            if (!db.objectStoreNames.contains('myStore')) {
                const objectStore = db.createObjectStore('myStore', { keyPath: 'id', autoIncrement: true });
                objectStore.createIndex('nameIndex', 'name', { unique: false });
            }
        };

        request.onsuccess = function(event) {
            resolve(event.target.result);
        };

        request.onerror = function(event) {
            reject(event.target.error);
        };
    });
}

function addData(db, data) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['myStore'], 'readwrite');
        const objectStore = transaction.objectStore('myStore');

        const request = objectStore.add(data);

        request.onsuccess = function(event) {
            resolve(event.target.result);
        };

        request.onerror = function(event) {
            reject(event.target.error);
        };
    });
}

function getData(db, id) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['myStore'], 'readonly');
        const objectStore = transaction.objectStore('myStore');

        const request = objectStore.get(id);

        request.onsuccess = function(event) {
            resolve(event.target.result);
        };

        request.onerror = function(event) {
            reject(event.target.error);
        };
    });
}

function updateData(db, id, newData) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['myStore'], 'readwrite');
        const objectStore = transaction.objectStore('myStore');

        const request = objectStore.get(id);

        request.onsuccess = function(event) {
            const data = event.target.result;
            if (data) {
                Object.assign(data, newData);
                const updateRequest = objectStore.put(data);

                updateRequest.onsuccess = function() {
                    resolve();
                };

                updateRequest.onerror = function(event) {
                    reject(event.target.error);
                };
            } else {
                reject(new Error('Data not found'));
            }
        };

        request.onerror = function(event) {
            reject(event.target.error);
        };
    });
}

function deleteData(db, id) {
    return new Promise((resolve, reject) => {
        const transaction = db.transaction(['myStore'], 'readwrite');
        const objectStore = transaction.objectStore('myStore');

        const request = objectStore.delete(id);

        request.onsuccess = function(event) {
            resolve();
        };

        request.onerror = function(event) {
            reject(event.target.error);
        };
    });
}

3.1 使用示例

openDatabase('myDatabase', 1)
    .then(db => {
        return addData(db, { name: 'John Doe', age: 30 });
    })
    .then(id => {
        console.log('Data added with ID:', id);
        return getData(db, id);
    })
    .then(data => {
        console.log('Data retrieved:', data);
        return updateData(db, data.id, { age: 31 });
    })
    .then(() => {
        console.log('Data updated successfully');
        return deleteData(db, data.id);
    })
    .then(() => {
        console.log('Data deleted successfully');
    })
    .catch(error => {
        console.error('Error:', error);
    });

4. 使用 idb 庫簡化操作

idb 是一個用於簡化 IndexedDB 操作的庫,提供了更簡潔的 API。

4.1 安裝 idb

npm install idb

4.2 使用 idb 進行操作

import { openDB } from 'idb';

async function openDatabase() {
    const db = await openDB('myDatabase', 1, {
        upgrade(db) {
            if (!db.objectStoreNames.contains('myStore')) {
                const objectStore = db.createObjectStore('myStore', { keyPath: 'id', autoIncrement: true });
                objectStore.createIndex('nameIndex', 'name', { unique: false });
            }
        },
    });
    return db;
}

async function addData(db, data) {
    const tx = db.transaction('myStore', 'readwrite');
    const store = tx.objectStore('myStore');
    await store.add(data);
    await tx.done;
}

async function getData(db, id) {
    const tx = db.transaction('myStore', 'readonly');
    const store = tx.objectStore('myStore');
    return store.get(id);
}

async function updateData(db, id, newData) {
    const tx = db.transaction('myStore', 'readwrite');
    const store = tx.objectStore('myStore');
    const data = await store.get(id);
    if (data) {
        Object.assign(data, newData);
        await store.put(data);
    } else {
        throw new Error('Data not found');
    }
    await tx.done;
}

async function deleteData(db, id) {
    const tx = db.transaction('myStore', 'readwrite');
    const store = tx.objectStore('myStore');
    await store.delete(id);
    await tx.done;
}

// 使用示例
(async () => {
    try {
        const db = await openDatabase();
        const id = await addData(db, { name: 'John Doe', age: 30 });
        console.log('Data added with ID:', id);
        const data = await getData(db, id);
        console.log('Data retrieved:', data);
        await updateData(db, data.id, { age: 31 });
        console.log('Data updated successfully');
        await deleteData(db, data.id);
        console.log('Data deleted successfully');
    } catch (error) {
        console.error('Error:', error);
    }
})();

5. 高效使用 IndexedDB 的最佳實踐

5.1 使用事務

儘量將多個操作放在同一個事務中,以減少事務的開銷並提高性能。

5.2 創建索引

為經常查詢的字段創建索引,可以顯著提高查詢速度。

5.3 數據分片

如果需要存儲大量數據,可以考慮將數據分片存儲在不同的對象存儲空間中。

5.4 異步操作

IndexedDB 是異步的,充分利用異步操作可以避免阻塞主線程,提高應用的響應速度。

5.5 錯誤處理

始終為每個請求添加錯誤處理邏輯,以便在出現問題時能夠及時捕獲並處理。

5.6 數據版本管理

使用版本號來管理數據庫的升級和降級,確保數據的一致性和完整性。

6. 示例:構建一個簡單的待辦事項應用

下面是一個使用 IndexedDB 構建的簡單待辦事項應用的示例。

6.1 HTML 結構

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo App</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        #todo-form {
            margin-bottom: 20px;
        }
        #todo-list {
            list-style-type: none;
            padding: 0;
        }
        .todo-item {
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 10px;
        }
        .todo-item button {
            margin-left: 10px;
        }
    </style>
</head>
<body>
    <h1>Todo List</h1>
    <form id="todo-form">
        <input type="text" id="todo-input" placeholder="Add a new todo" required>
        <button type="submit">Add</button>
    </form>
    <ul id="todo-list"></ul>

    <script src="app.js"></script>
</body>
</html>

6.2 JavaScript 代碼 (app.js)

import { openDB } from 'idb';

let db;

async function openDatabase() {
    db = await openDB('todoDatabase', 1, {
        upgrade(db) {
            if (!db.objectStoreNames.contains('todos')) {
                db.createObjectStore('todos', { keyPath: 'id', autoIncrement: true });
            }
        },
    });
}

async function addTodo(todo) {
    const tx = db.transaction('todos', 'readwrite');
    const store = tx.objectStore('todos');
    await store.add(todo);
    await tx.done;
    renderTodos();
}

async function getTodos() {
    const tx = db.transaction('todos', 'readonly');
    const store = tx.objectStore('todos');
    return store.getAll();
}

async function deleteTodo(id) {
    const tx = db.transaction('todos', 'readwrite');
    const store = tx.objectStore('todos');
    await store.delete(id);
    await tx.done;
    renderTodos();
}

async function renderTodos() {
    const todos = await getTodos();
    const todoList = document.getElementById('todo-list');
    todoList.innerHTML = '';
    todos.forEach(todo => {
        const li = document.createElement('li');
        li.className = 'todo-item';
        li.textContent = todo.text;
        const deleteButton = document.createElement('button');
        deleteButton.textContent = 'Delete';
        deleteButton.addEventListener('click', () => deleteTodo(todo.id));
        li.appendChild(deleteButton);
        todoList.appendChild(li);
    });
}

document.getElementById('todo-form').addEventListener('submit', async (event) => {
    event.preventDefault();
    const input = document.getElementById('todo-input');
    const text = input.value.trim();
    if (text) {
        await addTodo({ text });
        input.value = '';
    }
});

(async () => {
    await openDatabase();
    renderTodos();
})();

6.3 解釋

  1. HTML 結構

    • 包含一個表單用於添加新的待辦事項。
    • 包含一個無序列表用於顯示所有待辦事項。
  2. JavaScript 代碼

    • 使用 idb 庫簡化 IndexedDB 的操作。
    • 打開數據庫並創建 todos 對象存儲空間。
    • 提供 addTodogetTodosdeleteTodo 函數來操作數據。
    • renderTodos 函數用於渲染待辦事項列表。
    • 表單提交事件監聽器用於添加新的待辦事項。
user avatar alibabawenyujishu Avatar zaotalk Avatar linlinma Avatar dirackeeko Avatar aqiongbei Avatar littlelyon Avatar wmbuke Avatar kitty-38 Avatar nznznz Avatar nzbin Avatar yuxl01 Avatar verd Avatar
Favorites 53 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.