阅读视图

发现新文章,点击刷新页面。

被前端存储坑到崩溃?IndexedDB 高效用法帮你少走 90% 弯路

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 函数用于渲染待办事项列表。
    • 表单提交事件监听器用于添加新的待办事项。

CSS Flex 布局比 float 更值得学

在现代 Web 开发中,布局是构建用户界面的核心环节。长期以来,CSS 的 float 属性曾是实现多列布局的主要手段,但随着 CSS 技术的发展,Flexbox(弹性盒子布局)已成为更强大、更直观、更可靠的布局方案。本文将从多个维度对比 float 与 Flex 布局,并阐明为何 Flex 布局更值得投入时间学习和使用。

1. 历史背景:从 float 到 Flexbox

float 最初设计用于实现文本环绕图片的效果(如杂志排版),并非为页面整体布局而生。然而在 CSS Grid 和 Flexbox 出现之前,开发者不得不“滥用” float 来构建复杂的多列布局,导致代码冗长、语义不清,且需大量“清除浮动”(clearfix)技巧来修复布局塌陷问题。

Flexbox 则是 W3C 专门为一维布局(行或列)设计的现代 CSS 布局模块,于 2012 年左右开始被主流浏览器支持。它从语义、功能和开发体验上彻底解决了传统布局的痛点。

2. 语义与意图更清晰

使用 float 实现布局时,代码往往与视觉效果脱节。例如:

.sidebar {
  float: left;
  width: 200px;
}
.content {
  margin-left: 220px;
}

这段代码意图是“侧边栏在左,内容区在右”,但 float 本身表达的是“元素向左浮动”,而非“创建两列布局”。而 Flex 布局则直接表达布局意图:

.container {
  display: flex;
}
.sidebar {
  width: 200px;
}
.content {
  flex: 1; /* 自动填充剩余空间 */
}

display: flex 明确告诉开发者:这是一个弹性容器,子元素将按弹性规则排列。

3. 对齐与分布更强大

float 几乎无法实现垂直居中、等高列、动态间距等常见需求,而 Flexbox 原生支持:

  • 水平/垂直居中

    .center {
      display: flex;
      justify-content: center; /* 水平居中 */
      align-items: center;     /* 垂直居中 */
    }
    
  • 等高列:Flex 容器中的子项默认拉伸至相同高度,无需 JavaScript 或 hack 技巧。

  • 动态分配空间:通过 flex-growflex-shrinkflex-basis 精细控制子项如何伸缩。

  • 响应式对齐:结合 flex-direction 可轻松实现移动端堆叠、桌面端并排的响应式布局。

4. 无需“清除浮动”

使用 float 时,父容器高度会因子元素脱离文档流而“塌陷”,必须通过 clearfix(如 overflow: hidden 或伪元素)修复:

.clearfix::after {
  content: "";
  display: table;
  clear: both;
}

而 Flex 容器天然包含其子元素,不存在高度塌陷问题,代码更简洁、更可靠。

5. 更好的可维护性与可读性

Flex 布局的代码结构清晰、逻辑直观,便于团队协作和后期维护。相比之下,基于 float 的布局常需嵌套多层 div、添加额外类名,且难以调试。

例如,实现一个三栏布局:

  • float 方案:需为每栏设置宽度、浮动方向,并为父容器清除浮动。
  • Flex 方案
    .container {
      display: flex;
    }
    .left, .right {
      width: 200px;
    }
    .main {
      flex: 1;
    }
    

仅需几行代码,语义明确,无需额外 hack。

6. 浏览器支持已无后顾之忧

虽然 Flexbox 在早期存在浏览器兼容性问题,但如今(截至 2024 年),所有主流浏览器(包括 IE11,部分支持)均良好支持 Flexbox。对于仍需支持老旧浏览器的项目,可结合 Autoprefixer 或渐进增强策略处理。

相比之下,继续使用 float 布局不仅技术落后,还会增加开发成本和出错概率。

7. 学习成本其实更低

许多初学者误以为 Flexbox 复杂,但实际上:

  • 核心属性仅需掌握 5 个:display: flexflex-directionjustify-contentalign-itemsflex
  • 一旦理解“主轴/交叉轴”概念,布局逻辑一通百通。
  • 调试工具(如 Chrome DevTools 的 Flexbox 可视化)极大提升开发效率。

float 虽看似简单,但其副作用(如脱离文档流、高度塌陷、margin 折叠等)反而让初学者陷入更多陷阱。

结语

CSS Flex 布局不是“替代” float 的权宜之计,而是现代 Web 布局的标准方案。它语义清晰、功能强大、易于维护,且已被广泛支持。对于新项目,应毫不犹豫地采用 Flexbox(或更复杂的场景使用 CSS Grid)。而 float 应仅用于其原始用途:实现文本环绕图像等内联浮动效果。

❌