7.2 离线能力与本地数据库集成


7.2 离线能力与本地数据库集成

本节摘要:离线能力 = 本地持久层 + 操作队列 + 同步策略三件套。本节讲 SQLite 在主进程的集成模式(为什么放主进程、怎么封装 IPC 数据访问层)、IndexedDB 等浏览器侧方案与本地方案的选型、离线操作队列与冲突处理的三种策略,并用一个"断网可用的外勤填报应用"把三件套串成完整实现。

选型:数据放哪一层

桌面应用存数据的位置有三层可选,各有明确辖区。浏览器层(localStorage、IndexedDB):归渲染进程所有,胜在零集成成本,IndexedDB 容量可观且异步,适合缓存与界面状态;局限是数据跟着某个窗口的存储分区走,主进程与多窗口共享不便,也没法做复杂查询。文件层:JSON 落盘适合小配置(前面几章的窗口状态、应用配置都是),结构化数据一多就力不从心。数据库层(SQLite 或同类嵌入式库):跑在主进程,SQL 查询能力、事务、万级以上数据的稳定性能,是多窗口共享、复杂查询、离线业务数据的正选。

判断口诀:界面状态放浏览器层、应用配置放文件、业务数据进数据库。混着用(业务数据塞 localStorage)是常见的早期债。

SQLite 在主进程的集成模式

SQLite 以原生模块形态集成,运行在主进程(渲染进程的沙箱时代碰不到它,也不该碰)。推荐的分层结构是"主进程数据服务 + IPC 数据访问层 + 页面仓库对象"三段式:

// 主进程:数据服务层(SQLite 的封装示意) const db = openDatabase(); //better-sqlite3 等同步API的初始化封装 // 预建表与索引:启动时执行迁移脚本 db.exec(` CREATE TABLE IF NOT EXISTS visits ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer TEXT NOT NULL, note TEXT, created_at INTEGER NOT NULL, synced INTEGER DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_visits_synced ON visits(synced, created_at); `); // IPC 数据访问层:SQL 只留在主进程,页面永远不拼 SQL ipcMain.handle('visits:create', async (_e, { customer, note }) => { if (typeof customer !== 'string' || !customer.trim()) { throw new Error('客户名不能为空'); // 入口校验(第四章纪律) } const info = db.prepare( 'INSERT INTO visits (customer, note, created_at) VALUES (?, ?, ?)' ).run(customer, note ?? '', Date.now()); // 参数化查询,杜绝注入 return info.lastInsertRowid; }); ipcMain.handle('visits:unsynced', async () => { return db.prepare( 'SELECT * FROM visits WHERE synced = 0 ORDER BY created_at' ).all(); });

两个工程要点。其一,同步 API 的主进程适配:better-sqlite3 这类同步接口在主进程里跑简单查询毫无问题(微秒到毫秒级),但批量导入这类重操作要拆片或移入 Worker(2.2 节的主进程铁律)。其二,SQL 不出主进程:页面永远通过"动词化"的 IPC 通道请求数据(visits:create、visits:unsynced),而不是传 SQL 语句过去执行——后者等于给页面开了数据库全权,通道分级表(4.4 节)里的三级通道。

同步:队列与冲突

离线写入的数据迟早要同步回服务端,设计三件套:待同步标记(synced 字段加索引,上面建表语句里已经埋了)、同步队列(网络恢复或定时触发,批量取未同步记录上报)、冲突策略。冲突处理的三个经典选项:

策略 规则 适用
本地优先 冲突时本地覆盖远端 单人单设备数据
远端优先 冲突时远端覆盖本地 服务端是唯一事实源
字段合并 按字段时间戳逐字段取新 多端编辑同一记录

选型没有标准答案,取决于业务语义。外勤填报这类"一人写自己的数据"场景,本地优先加服务端仲裁就够;协作编辑就必须上字段合并甚至操作变换。

案例:断网可用的外勤填报应用

背景:快消品公司的拜访管理应用,销售在地下商超、偏远门店频繁断网,旧网页版断网即瘫痪,填一半的表单丢失是日常投诉。

操作:三件套落地。数据层:SQLite 进主进程,表结构围绕拜访记录设计,全部带 synced 标记。队列:写入永远先落本地(页面的提交经 IPC 直接入库,界面立即显示"待同步"状态角标);同步服务在主进程定时探测网络,恢复后批量上报,成功则置 synced 并推送结果事件刷新界面。冲突:一人一设备的业务语义,采用本地优先,服务端只做合法性校验不回写。

// 主进程:同步服务的核心循环 async function syncLoop() { if (!(await isOnline())) return; const pending = db.prepare( 'SELECT * FROM visits WHERE synced = 0 LIMIT 50' // 小批量分片 ).all(); if (!pending.length) return; try { const result = await uploadBatch(pending); // 带鉴权的加密上报 db.prepare( `UPDATE visits SET synced = 1 WHERE id IN (${pending.map(() => '?').join(',')})` ).run(...pending.map(r => r.id)); notifyWindows('sync:done', result); // 界面刷新角标 } catch (err) { console.warn('同步失败,下轮重试:', err.message); // 失败静默,指数退避 } } setInterval(syncLoop, 60_000);

结果:断网场景从"瘫痪"变成"无感"——表单秒存本地、角标可见、网络恢复自动补传。上线三个月,因网络问题造成的填报丢失投诉归零。

解读:这个案例里最值钱的设计是**"本地即时反馈、后台补传"的体验反转**——用户不再关心网,系统替他管网。变式:如果记录会被多端编辑(比如主管端也会改状态),冲突策略升级为字段合并,同步协议要多带版本向量;再进一步若要实时协作,就该考虑操作变换或 CRDT 一类的同步数据结构,那是一个独立的大课题。

本节要点回顾

  • 三层辖区:界面状态浏览器层、配置文件层、业务数据 SQLite;
  • 三段式集成:主进程数据服务、IPC 动词化访问层、页面仓库对象,SQL 不出主进程;
  • 同步三件套:待同步标记、批量队列、冲突策略按业务语义选;
  • 同步纪律:小批量分片、失败退避、结果事件回推界面;
  • 体验心法:本地即时反馈、后台静默补传,让用户忘掉网络的存在。

数据扎根了,下一节补上质量长期主义:测试金字塔与生产期的眼睛。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U