indexDB学习笔记——结合TodoList实战(附源码)

196 阅读7分钟

一、常规用法

1.特性

  • 大容量存储

支持存储GB级数据(通常为浏览器可用磁盘的50%),远超LocalStorage的5MB限制。

适合离线应用缓存、音视频资源本地存储等场景。

  • 异步事务模型

所有操作通过异步API执行,避免阻塞主线程。

支持读写事务(readwrite)和只读事务(readonly),保证数据操作的原子性。(数据库事务的定义

  • 高效索引查询

支持单字段/多字段索引,通过createIndex创建索引提升查询效率。

支持范围查询、模糊匹配(通过游标遍历)。

  • 复杂数据结构支持

可存储JavaScript对象、Blob、File等数据类型,通过键值对管理数据。

  • 版本控制与迁移

通过onupgradeneeded事件实现数据库版本升级,支持数据结构变更(如新增对象仓库)。

2.创建,打开数据库

// 初始化数据库
initDB() {
  return new Promise((resolve, reject) => {
    // 打开数据库
    const request = indexedDB.open(this.dbName, this.dbVersion);

    // 数据库首次创建或版本更新时调用
    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      
      // 创建对象仓库(类似于关系型数据库中的表)
      if (!db.objectStoreNames.contains('todos')) {
        const store = db.createObjectStore('todos', {
          keyPath: 'id',
          autoIncrement: true
        });
        
        // 创建单独的索引
        store.createIndex('timestamp', 'timestamp', { unique: false });
        store.createIndex('completed', 'completed', { unique: false });
        
        // 创建复合索引
        store.createIndex('completed_timestamp', ['completed', 'timestamp'], { unique: false });
      }
    };

    // 数据库打开成功
    request.onsuccess = (event) => {
      this.db = event.target.result;
      console.log('数据库连接成功');
      resolve();
    };

    // 数据库打开失败
    request.onerror = (event) => {
      console.error('数据库连接失败:', event.target.error);
      reject(event.target.error);
    };
  });
}

2.1 使用IndexDB.open打开indexDB数据库

  • 需要指定数据库名称和版本号
  • 返回一个**IDBRequest**对象
const request = indexedDB.open(this.dbName, this.dbVersion);

2.2 创建store,类似于SQL的table表(数据一条条存放在store)。

一般在IndexDB的onupgradeneeded成功回调中创建。

注意做好判断,不要重复创建

onupgradeneeded当IndexDB第一次打开或者版本号升级时候触发的回调

创建store可以设置

  • 主键,自增
  • 索引
request.onupgradeneeded = (event) => {
    const db = event.target.result;
    if (!db.objectStoreNames.contains('todos')) {
      const store = db.createObjectStore('todos', {
          keyPath: 'id', // 定义主键 id
          autoIncrement: true // 主键自增
      });
    }
};

2.3创建store索引

传入三个参数

  • 索引名称
  • 索引对应的对象的key
  • 配置options
store.createIndex(indeName, keyPath, { unique: false });

3.查get(key),getAll()

IndexDB的**增删改查操作都需要通过事务**来完成,并且是异步的,不会阻塞主线程。

  1. 创建**readonly**的事务

IDBRequest.transaction([storeName], 'readonly'|' readwrite ')

// transaction
const transaction = this.db.transaction(['todos'], 'readonly');
const store = transaction.objectStore('todos');
  1. 事务绑定到todos这个store上
  2. **store.getAll()**查询所有数据
const request = store.getAll();

4.store.get(key),根据主键查询对应的数据

const getRequest = store.get(id);

3.新增add(object)

1.使用**readwrite**事务

2.调用**store.add(Object)**。内部先克隆在新增,不必担心引用共享的问题。

// 添加待办事项
async addTodo(text) {
  const todo = {
    text,
    completed: false,
    timestamp: Date.now()
  };

  return new Promise((resolve, reject) => {
    // 1.创建读写事务
    const transaction = this.db.transaction(['todos'], 'readwrite');
    const store = transaction.objectStore('todos');
    // 2.add数据
    const request = store.add(todo);
    // 监听成功和失败
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

4.删除delete(key)

  1. 创建readwrite读写事务
  2. store.delete(key) 根据主键删除对应的数据
// 删除待办事项
async deleteTodo(id) {
  return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['todos'], 'readwrite');
      const store = transaction.objectStore('todos');
      const request = store.delete(id);
  
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
  });
}

5.修改put(value, key?)

  1. 创建**readwrite**读写事务
  2. store.put(value, key?)。key可以不传,name直接value中需要含有主键,才能更新对应的数据
// 更新待办事项状态
async updateTodo(id, changes) {
  return new Promise((resolve, reject) => {
    const transaction = this.db.transaction(['todos'], 'readwrite');
    const store = transaction.objectStore('todos');
    
    // 先获取原有数据
    const getRequest = store.get(id);
    
    getRequest.onsuccess = () => {
      const todo = { ...getRequest.result, ...changes };
      const updateRequest = store.put(todo);
      
      updateRequest.onsuccess = () => resolve(updateRequest.result);
      updateRequest.onerror = () => reject(updateRequest.error);
    };
    
    getRequest.onerror = () => reject(getRequest.error);
  });
}

二、高级特性

1.游标Cursor

类似于指针

  1. 创建游标

IDBObjectStore.openCursor()。store对象可以创建游标

下文的索引对象也可以创建游标。IDBIndex.openCursor()

  • event.target.value,event.target.key 获取指针指向当前对象的key和value
  • continue() 移动游标到下一位
const todos = [];
const transaction = this.db.transaction(['todos'], 'readonly');
const store = transaction.objectStore('todos');

// 打开游标
const request = store.openCursor(range);

request.onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    todos.push(cursor.value);
    cursor.continue(); // 移动到下一条记录
  } else {
    resolve(todos); // 遍历完成
  }
};

request.onerror = () => reject(request.error);

2.索引的使用

  1. 创建索引
  2. 创建范围
// 范围查询
const range = IDBKeyRange.bound(startDate, endDate);

// 精确匹配
const range = IDBKeyRange.only(value);

// 大于等于
const range = IDBKeyRange.lowerBound(value);

// 小于等于
const range = IDBKeyRange.upperBound(value);
  1. 结合游标查询数据

案例:

// 创建事务
const transaction = this.db.transaction(['todos'], 'readonly');
const store = transaction.objectStore('todos');

// 使用 completed 索引
const index = store.index('completed');
const range = IDBKeyRange.only(completed);
// 使用游标遍历 索引检索出来的数据
const request = index.openCursor(range);
request.onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    todos.push(cursor.value);
    cursor.continue();
  } else {
    resolve(todos);
  }
};

三、源码

1.index.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>IndexDB 待办事项</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <h1>IndexDB 待办事项</h1>
        <div class="todo-input">
            <input type="text" id="todoInput" placeholder="输入待办事项...">
            <button onclick="addTodo()">添加</button>
        </div>
        <ul id="todoList"></ul>
    </div>
    <script src="db.js"></script>
    <script src="app.js"></script>
</body>
</html> 

2.样式style.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: Arial, sans-serif;
    line-height: 1.6;
    background-color: #f5f5f5;
}

.container {
    max-width: 600px;
    margin: 2rem auto;
    padding: 1rem;
}

h1 {
    text-align: center;
    color: #333;
    margin-bottom: 2rem;
}

.todo-input {
    display: flex;
    gap: 1rem;
    margin-bottom: 2rem;
}

input[type="text"] {
    flex: 1;
    padding: 0.5rem;
    font-size: 1rem;
    border: 1px solid #ddd;
    border-radius: 4px;
}

button {
    padding: 0.5rem 1rem;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

button:hover {
    background-color: #45a049;
}

#todoList {
    list-style: none;
}

.todo-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem;
    background-color: white;
    margin-bottom: 0.5rem;
    border-radius: 4px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

.todo-item.completed {
    background-color: #f8f8f8;
    text-decoration: line-through;
    color: #888;
}

.delete-btn {
    background-color: #ff4444;
}

.delete-btn:hover {
    background-color: #cc0000;
} 

3.app.js 用户交互

// DOM 元素
const todoInput = document.getElementById('todoInput')
const todoList = document.getElementById('todoList')

// 页面加载完成后显示所有代办事项
document.addEventListener('DOMContentLoaded', () => {
  setTimeout(async () => {
    await loadTodos()
  }, 100)
})

// 加载所有代办事项
async function loadTodos() {
  try {
    const todos = await todoDB.getAllTodos()
    renderTodos(todos)
  } catch (error) {
    console.error('加载待办事项失败:', error)
  }
}

// 渲染代办事项列表
function renderTodos(todos) {
  todoList.innerHTML = ''
  todos.forEach((todo) => {
    const li = document.createElement('li')
    li.className = `todo-item ${todo.completed ? 'completed' : ''}`

    // 创建待办事项文本
    const span = document.createElement('span')
    span.textContent = todo.text

    // 创建操作按钮容器
    const actions = document.createElement('div')

    // 创建完成按钮
    const completeBtn = document.createElement('button')
    completeBtn.textContent = todo.completed ? '取消完成' : '完成'
    completeBtn.onclick = () => toggleTodo(todo.id, todo.completed ? 0 : 1)

    // 创建删除按钮
    const deleteBtn = document.createElement('button')
    deleteBtn.textContent = '删除'
    deleteBtn.className = 'delete-btn'
    deleteBtn.onclick = () => deleteTodo(todo.id)

    // 组装元素
    actions.appendChild(completeBtn)
    actions.appendChild(deleteBtn)
    li.appendChild(span)
    li.appendChild(actions)
    todoList.appendChild(li)
  })
}

// 添加待办事项
async function addTodo() {
  const text = todoInput.value.trim()
  if (!text) return

  try {
    await todoDB.addTodo(text)
    todoInput.value = ''
    await loadTodos()
  } catch (error) {
    console.error('添加待办事项失败:', error)
  }
}

// 切换待办事项状态
async function toggleTodo(id, completed) {
  try {
    await todoDB.updateTodo(id, { completed })
    await loadTodos()
  } catch (error) {
    console.error('更新待办事项状态失败:', error)
  }
}

// 删除待办事项
async function deleteTodo(id) {
  try {
    await todoDB.deleteTodo(id)
    await loadTodos()
  } catch (error) {
    console.error('删除待办事项失败:', error)
  }
}

todoInput.addEventListener('keypress', (e) => {
  if (e.key === 'Enter') {
    addTodo()
  }
})

// 点击查看当天已完成的
const checkTodayBtn = document.getElementById('checkTodayBtn')
checkTodayBtn.addEventListener('click', async () => {
  const start = new Date(2025, 3, 8, 0, 0, 0, 0)
  const end = new Date(2025, 3, 8, 23, 59, 59, 0)
  alert(JSON.stringify(await todoDB.getTodosByDateRange(start, end)))
})

// 点击已完成或者未完成
const checkCompleteBtn = document.getElementById('checkCompleteBtn')
checkCompleteBtn.addEventListener('click', async (e) => {
  const data = e.target.dataset.complete
  const res = await todoDB.getTodosByStatus(1)
  alert(JSON.stringify(res))
})

// 点击已完成或者未完成
const deleteBatchBtn = document.getElementById('deleteBatch')
deleteBatchBtn.addEventListener('click', async (e) => {
  // 获取全部id
  const ids = (await todoDB.getAllTodos()).map((item) => item.id)
  const res = await todoDB.deleteTodos(ids)
  loadTodos()
})

4.db.js indexDB数据库操作

// IndexDB 数据库操作类
class TodoDB {
  constructor() {
    this.dbName = 'TodoDB'
    this.dbVersion = 1
    this.db = null

    // 初始化数据库
    this.initDB()
  }

  // 初始化数据库
  initDB() {
    return new Promise((resolve, reject) => {
      // 打开数据库
      const request = indexedDB.open(this.dbName, this.dbVersion)
      // 数据库首次创建或者版本更新时调用
      request.onupgradeneeded = (event) => {
        const db = event.target.result

        // 创建对象仓库(类似于关系型数据库中的表)
        if (!db.objectStoreNames.contains('todos')) {
          const store = db.createObjectStore('todos', {
            keyPath: 'id',
            autoIncrement: true
          })

          // 创建单独索引
          store.createIndex('timestamp', 'timestamp', { unique: false })
          store.createIndex('completed', 'completed', { unique: false })

          // 创建复合索引
          store.createIndex('completed_timestamp', ['timestamp', 'completed'], {
            unique: false
          })
        }
      }
      request.onsuccess = (event) => {
        this.db = event.target.result
        console.log('数据库连接成功')
        resolve()
      }

      request.onerror = (event) => {
        console.error('数据库连接失败:', event.target.error)
        reject(event.target.error)
      }
    })
  }

  // 添加待办事项
  async addTodo(text) {
    const todo = {
      text,
      completed: 0,
      timestamp: Date.now()
    }

    return new Promise((resolve, reject) => {
      // 开启事务
      const transaction = this.db.transaction(['todos'], 'readwrite')
      // 获取对象的存储空间
      const store = transaction.objectStore('todos')
      const request = store.add(todo)

      request.onsuccess = () => {
        resolve(request.result)
      }

      request.onerror = () => {
        reject(request.error)
      }
    })
  }

  // 获取所有代办事项
  async getAllTodos() {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['todos'], 'readonly')
      const store = transaction.objectStore('todos')
      const request = store.getAll()

      request.onsuccess = () => resolve(request.result)
      request.onerror = () => reject(request.error)
    })
  }

  // 更新待办事项
  async updateTodo(id, changes) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['todos'], 'readwrite')
      const store = transaction.objectStore('todos')

      // 先获取原有数据

      const r = store.get(1744354409809)
      r.onsuccess = (e) => {
        console.log(r.result)
      }

      const getRequest = store.get(id)

      getRequest.onsuccess = () => {
        const todo = { ...getRequest.result, ...changes }
        const updateRequest = store.put(todo)

        updateRequest.onsuccess = () => resolve(updateRequest.result)
        updateRequest.onerror = () => reject(updateRequest.error)
      }

      getRequest.onerror = () => reject(getRequest.error)
    })
  }

  // 删除待办事项
  async deleteTodo(id) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['todos'], 'readwrite')
      const store = transaction.objectStore('todos')
      const request = store.delete(id)

      request.onsuccess = () => resolve()
      request.onerror = () => reject(request.error)
    })
  }

  // 使用游标获取指定时间范围的代办事项
  getTodosByDateRange(startDate, endDate) {
    return new Promise((resolve, reject) => {
      const todos = []
      const transaction = this.db.transaction(['todos'], 'readonly')
      console.log(transaction)
      const store = transaction.objectStore('todos')
      console.log(store)
      const index = store.index('timestamp')
      console.log(index)

      // 创建一个范围
      const range = IDBKeyRange.bound(startDate.getTime(), endDate.getTime())
      console.log(range)
      // 打开游标
      const request = index.openCursor(range)

      request.onsuccess = (event) => {
        const cursor = event.target.result
        console.log('cursor', cursor)
        if (cursor) {
          todos.push(cursor.value)
          cursor.continue() // 移动到下一条记录
        } else {
          resolve(todos) // 遍历完成
        }
      }
    })
  }

  // 使用复合索引获取已完成、未完成的待办事项,并按时间排序
  async getTodosByStatus(completed) {
    return new Promise((resolve, reject) => {
      const todos = []
      const transaction = this.db.transaction(['todos'], 'readonly')
      const store = transaction.objectStore('todos')

      // 使用 completed 索引而不是复合索引
      const index = store.index('completed')
      const range = IDBKeyRange.only(1)

      // 使用游标遍历
      const request = index.openCursor(range)

      request.onsuccess = (event) => {
        const cursor = event.target.result
        if (cursor) {
          todos.push(cursor.value)
          cursor.continue()
        } else {
          // 在内存中按时间戳排序
          todos.sort((a, b) => b.timestamp - a.timestamp)
          resolve(todos)
        }
      }

      request.onerror = () => reject(request.error)
    })
  }

  // 批量删除待办事项
  deleteTodos(ids) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['todos'], 'readwrite')
      const store = transaction.objectStore('todos')
      let completed = 0
      let errors = []

      // 监听事务完成
      transaction.oncomplete = () => {
        if (errors.length > 0) {
          reject(errors)
        } else {
          resolve()
        }
      }

      // 在同一事务中执行所有删除操作
      ids.forEach((id) => {
        const request = store.delete(id)
        request.onsuccess = () => completed++
        request.onerror = () => errors.push(request.error)
      })
    })
  }
}

// 创建数据库实例
const todoDB = new TodoDB()