2. 本地数据库选型与初始化:IndexedDB、SQLite、WebSQL对比,建表与版本迁移
2.1 离线场景下的本地数据库需求
在离线优先架构中,前端应用必须能够在没有网络连接时持久化结构化数据。不同于 localStorage 的简单键值对,复杂查询、关联数据和增量同步要求使用具备查询引擎、事务支持和 schema 管理的本地数据库。浏览器端可选的方案主要有三种:IndexedDB、SQLite(通过 WebAssembly 移植)和已被 W3C 废弃但仍存留于旧版浏览器的 WebSQL。
核心选型维度:存储模型、查询能力、容量上限、浏览器兼容性、事务支持、异步/同步 API、版本迁移机制。
2.2 三大本地数据库核心对比
| 对比维度 | IndexedDB | SQLite (Wasm) | WebSQL |
|---|---|---|---|
| 存储模型 | 对象仓库 / 键值对 | 关系型表 / 行 | 关系型表 / 行 |
| 查询语言 | 游标 + 索引 API | 标准 SQL (DDL + DML) | 标准 SQL (DDL + DML) |
| 事务支持 | 读写事务 (自动提交) | 完整 ACID | 完整 ACID |
| 容量上限 | 磁盘剩余空间(通常 >50MB) | Wasm 内存 + 持久化文件 | 5MB(可申请增加) |
| 异步/同步 | 完全异步 (Promise/事件) | 同步 API(包装后可异步) | 异步回调 |
| 浏览器支持 | 所有现代浏览器 | 需加载 Wasm 文件 | 仅旧版 WebKit (已废弃) |
| 版本迁移 | onupgradeneeded + version | 用户态版本表 + 迁移脚本 | transaction + version 参数 |
| 维护状态 | 活跃标准 | 活跃社区维护 | 已废弃,不再更新 |
2.3 数据库初始化与建表实现
2.3.1 IndexedDB
通过 indexedDB.open() 指定数据库名称和版本号;onupgradeneeded 事件中创建对象仓库(表)和索引。版本号必须为无符号整数。
function openIndexedDB(name, version) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('email_idx', 'email', { unique: true });
}
if (!db.objectStoreNames.contains('projects')) {
db.createObjectStore('projects', { keyPath: 'projectId', autoIncrement: true });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 调用:openIndexedDB('offline_app', 1)
2.3.2 SQLite (通过 sql.js)
sql.js 将 SQLite 编译为 WebAssembly,在内存中运行数据库实例。建表使用标准 CREATE TABLE 语句,持久化需手动保存到文件或 IndexedDB。
import initSqlJs from 'sql.js';
async function initSQLite() {
const SQL = await initSqlJs({ locateFile: file => `https://sql.js.org/dist/${file}` });
const db = new SQL.Database();
db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
created_at TEXT DEFAULT (datetime('now'))
)`);
db.run(`CREATE TABLE IF NOT EXISTS projects (
project_id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
owner_id INTEGER,
FOREIGN KEY (owner_id) REFERENCES users(id)
)`);
return db;
}
2.3.3 WebSQL(已废弃,仅用于兼容旧版)
WebSQL 通过 openDatabase() 创建数据库,使用 SQL 字符串执行建表。该规范已从 W3C 移除,不可用于新项目。
const db = openDatabase(
'offline_app', // 数据库名
'1.0', // 版本号
'离线应用数据库', // 描述
5 * 1024 * 1024 // 预估大小 (5MB)
);
db.transaction(tx => {
tx.executeSql(
'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)'
);
tx.executeSql(
'CREATE TABLE IF NOT EXISTS projects (project_id INTEGER, title TEXT)'
);
});
WebSQL 仅在 iOS Safari < 13 和部分旧版 Android WebView 中可用。任何新项目应优先使用 IndexedDB 或 SQLite (Wasm)。
2.4 版本迁移策略
数据库 schema 随应用迭代必然变化。版本迁移的核心原则:向前兼容、增量变更、数据保留。三种数据库的迁移机制差异显著。
IndexedDB 版本迁移
版本号递增触发 onupgradeneeded,在事件中对比 event.oldVersion 和 event.newVersion 执行增量 DDL。
function migrateDB(oldVersion, newVersion, db) {
// 版本 1 -> 2:新增 projects 仓库
if (oldVersion < 2) {
const store = db.createObjectStore('projects', { keyPath: 'id' });
store.createIndex('owner_idx', 'ownerId', { unique: false });
}
// 版本 2 -> 3:给 users 增加 age 索引
if (oldVersion < 3) {
const tx = db.transaction(['users'], 'versionchange');
const store = tx.objectStore('users');
store.createIndex('age_idx', 'age', { unique: false });
}
}
SQLite 用户态迁移
推荐维护一张 _schema_version 表,通过版本号控制迁移脚本的执行顺序。
function migrateSQLite(db) {
// 查询当前版本
const versionRow = db.exec("SELECT value FROM _meta WHERE key='schema_version'");
const currentVersion = versionRow.length ? Number(versionRow[0].values[0][0]) : 0;
if (currentVersion < 1) {
db.run('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
db.run("INSERT INTO _meta (key, value) VALUES ('schema_version', '1')");
}
if (currentVersion < 2) {
db.run('ALTER TABLE users ADD COLUMN email TEXT');
db.run("UPDATE _meta SET value='2' WHERE key='schema_version'");
}
}
迁移脚本应保持幂等性(使用 IF NOT EXISTS / 先检查再执行),并包裹在事务中以保证原子性。IndexedDB 的 onupgradeneeded 自带事务,SQLite 需手动 begin/commit。
2.5 数据库选型与初始化流程
下图从离线应用的数据需求出发,梳理三大数据库的选型依据,并串联从初始化到版本迁移的完整链路。
2.6 常见错误与调试
- IndexedDB 版本号未递增:打开数据库时 version 必须大于当前版本,否则不会触发 onupgradeneeded,schema 变更不生效。
- SQLite Wasm 加载失败:locateFile 路径错误或跨域问题导致 .wasm 文件无法获取。检查网络面板和服务器 MIME 类型。
- WebSQL 大小超限:旧版 Safari 对 WebSQL 有 5MB 硬限制,超出后事务静默失败。通过
db.changeVersion()申请扩容。 - 迁移脚本非幂等:重复执行 CREATE TABLE 会抛出 "table already exists" 错误。始终使用
IF NOT EXISTS或先查询 schema 状态。 - IndexedDB 事务提前关闭:在 onupgradeneeded 中执行异步操作(如 fetch)会导致事务自动提交。迁移逻辑必须同步执行。
调试 IndexedDB 可使用 Chrome DevTools → Application → IndexedDB 面板直接查看对象仓库内容和索引;SQLite 可通过
db.export() 导出为 Uint8Array 后下载分析。2.7 性能与稳定性优化建议
- 批量写入使用事务:IndexedDB 每次事务提交有开销,将多条写入合并到同一个事务中可提升 3-5 倍吞吐。
- SQLite 启用 WAL 模式:
PRAGMA journal_mode=WAL;显著提升并发读性能,减少写入锁冲突。 - 延迟初始化:将数据库打开操作推迟到首次实际读写时,避免启动时阻塞 UI 渲染。
- 索引覆盖查询:为高频查询字段创建索引,避免全表扫描。IndexedDB 的索引是 B+ 树结构,复合索引需注意 keyPath 顺序。
- 定期 VACUUM:SQLite 在大量删除后会留下空闲页,执行
VACUUM;回收空间;IndexedDB 无需手动碎片整理。
选型建议:如果团队前端技术栈以 React/Vue 为主且查询复杂度较低,优先选择 IndexedDB + 封装库(如 Dexie.js);若需要复杂联表查询或已存在 SQL 资产,选择 SQLite (Wasm) 并在 Worker 中运行以避免阻塞主线程。