在很多人的印象中,Electron 应用天生“在线”,因为它的壳子里跑的就是一个网页。但现实场景远比这复杂:用户的网络可能不稳定,某些功能需要在无网环境下使用,或者纯粹为了提升秒开体验。良好的离线缓存策略能够让应用在网络抖动时依然流畅运行,甚至在完全断网的情况下提供基本服务。
这一节我们从工程实践出发,分别讨论 静态资源缓存(让界面快速渲染)和 业务数据离线存储(让核心功能不依赖网络)两大部分。所有方案都无需引入第三方云服务,完全基于 Electron 自身能力和 Node.js 生态实现。
13.3.1 静态资源缓存:让界面“秒开”
Electron 应用通常使用 win.loadURL 或 win.loadFile 加载页面。如果采用 loadURL 加载远程地址,每次冷启动都依赖网络,速度慢且不可控。推荐的现代方案是 本地优先策略,即所有静态资源都随应用包发布,让渲染进程从本地文件系统加载。
方案一:纯本地部署(推荐)
直接把前端构建产物(HTML/JS/CSS/图片)打包进应用的 resources 目录,通过 loadFile 加载入口文件。这是最彻底、最可靠的离线方案。
实现步骤:
- 前端项目构建产出
dist文件夹。 - 在 electron-builder 配置中将
dist目录包含进 app 包内:
// package.json (electron-builder 配置片段)
"build": {
"files": [
"main.js",
"preload.js",
"dist/**/*" // 前端静态文件
]
}
- 主进程加载本地文件:
// main.js
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
// 加载本地 index.html
win.loadFile(path.join(__dirname, 'dist', 'index.html'));
}
这样做的好处显而易见:离线启动零延迟,完全不受网络影响;不需要维护远程服务器或 CDN。VS Code、Figma 等大部分 Electron 应用都采用此模式。
方案二:本地资源 + Service Worker 缓存(混合场景)
如果你的应用某些页面或资源仍需从远程加载(例如实时更新的文档模板、动态配置),可以利用 Service Worker 实现“网络优先,缓存回退”或“缓存优先,后台更新”。
适用场景: 界面壳资源本地化,但内部 WebView 或子页面依赖远程资源。
实现要点:
在渲染进程中注册 Service Worker,拦截网络请求:
// renderer.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
// sw.js (Service Worker)
const CACHE_NAME = 'my-app-cache-v1';
const urlsToCache = [
'/', // 本地入口
'https://cdn.example.com/main.css',
'https://cdn.example.com/app.js'
];
// 安装阶段缓存关键资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache))
);
});
// 拦截请求:缓存优先,网络回退
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
注意: Electron 的渲染进程本质上就是 Chromium,对 Service Worker 的支持与浏览器一致,但你需要确保 webPreferences 中没有禁用缓存功能。另外,Service Worker 仅在通过 HTTP/HTTPS 加载(包括 localhost)时才能注册,若使用 loadFile 协议(file://),则无法使用 Service Worker。因此如果要利用 Service Worker,需改用 loadURL 加载本地启动的服务(下一节会提到本地服务器方案),或者仅在应用内置的 webview 中使用。
对于大多数普通桌面应用,方案一(纯本地)是最简单、最稳定的选择,强烈推荐。
13.3.2 业务数据离线存储:让应用在断网时也能用
静态资源只是“壳”,真正决定应用离线能力的是业务数据。用户编辑的文档、设置偏好、未同步的消息……这些都需要在本地可靠地保存,并在网络恢复后按需同步。Electron 环境下,你有多种存储方案可以选择,各有擅场。
存储方案对比
| 方案 | 容量限制 | 复杂查询 | 适用场景 |
|------|---------|----------|----------|
| localStorage / sessionStorage | 5-10 MB | 无 | 少量键值对(用户偏好) |
| IndexedDB | 浏览器配额(通常可用几百MB) | 索引,游标 | 结构化数据,前端直接操作 |
| better-sqlite3 (SQLite) | 无限制(磁盘容量) | 完整 SQL 支持 | 大量复杂数据,需事务/全文搜索 |
| lowdb / nedb | 无限制 | 简单查询 | 原型或小型项目,JSON 文件存储 |
| 自定义 JSON/文件存储 | 无限制 | 需自己实现 | 日志、导出数据等 |
对于 Electron 应用,我们通常使用 SQLite 或 IndexedDB 作为业务数据的主力存储,辅以 localStorage 存放无关紧要的界面状态。
实践一:使用 SQLite(推荐,用于核心业务)
SQLite 是无服务器的嵌入式数据库,与 Electron 是天然绝配。它的库以二进制 Node.js 原生模块形式存在(如 better-sqlite3),性能极高,支持标准 SQL,数据存储在一个文件中,方便备份和迁移。
集成示例:
- 安装依赖:
npm install better-sqlite3
- 在主进程中创建数据库连接(需要
nodeIntegration或通过 preload 暴露接口,推荐在主进程操作以保证安全):
// db.js (在主进程中运行)
const Database = require('better-sqlite3');
const path = require('path');
const { app } = require('electron');
const dbPath = path.join(app.getPath('userData'), 'app-data.db');
const db = new Database(dbPath);
// 启用 WAL 模式提升并发读取性能
db.pragma('journal_mode = WAL');
// 初始化表
db.exec(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// 暴露操作方法(通过 IPC 调用)
function addNote(title, content) {
const stmt = db.prepare('INSERT INTO notes (title, content) VALUES (?, ?)');
return stmt.run(title, content);
}
function getAllNotes() {
return db.prepare('SELECT * FROM notes ORDER BY updated_at DESC').all();
}
module.exports = { addNote, getAllNotes };
- 在 preload 中暴露安全接口:
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('dbAPI', {
addNote: (title, content) => ipcRenderer.invoke('db:addNote', title, content),
getNotes: () => ipcRenderer.invoke('db:getNotes'),
});
- 主进程 IPC 处理:
// main.js
const { ipcMain } = require('electron');
const { addNote, getAllNotes } = require('./db');
ipcMain.handle('db:addNote', (event, title, content) => {
return addNote(title, content);
});
ipcMain.handle('db:getNotes', () => {
return getAllNotes();
});
这样,无论是否有网络,笔记数据都安全地保存在用户本地。即便应用关闭重启,数据依然存在。
实践二:使用 IndexedDB(用于纯前端数据)
如果不想引入原生模块,或者数据量不大,IndexedDB 也是一个轻便的选择。它运行在渲染进程,可以直接使用,无需 IPC,特别适合网页背景的开发者。
简单示例(使用 idb 包装库):
// renderer.js
import { openDB } from 'idb';
const dbPromise = openDB('MyAppDB', 1, {
upgrade(db) {
db.createObjectStore('settings', { keyPath: 'key' });
},
});
// 保存用户设置
async function saveSetting(key, value) {
const db = await dbPromise;
await db.put('settings', { key, value });
}
// 读取设置
async function getSetting(key) {
const db = await dbPromise;
const item = await db.get('settings', key);
return item?.value;
}
这种方式的优势是完全在渲染线程内完成,不阻塞主进程,适合做一些UI相关的缓存。
实践三:离线同步策略(基础版)
有了本地存储,下一步往往是设计一个同步机制,保证离线修改的数据能在联网后与服务端握手。一种简单实用的方案是 本地修改记录 + 后台同步队列。
核心思路:
- 为每条业务记录增加
_syncStatus字段(值如synced,modified,deleted)。 - 用户在离线时进行增删改,所有操作标记为
modified或deleted(而不是直接删除)。 - 监听网络状态变化(
navigator.onLine或 Electron 的net模块事件),一旦联网,启动同步进程。 - 同步进程查询所有
_syncStatus !== 'synced'的记录,依次发送到远端 API,成功后更新状态为synced。 - 冲突处理可以简单采用“最后写入胜出”(基于时间戳),或提示用户手动合并。
示例:在 SQLite 基础上实现同步标记
// 离线编辑时标记
function updateNote(id, title, content) {
db.prepare(`
UPDATE notes
SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP, _syncStatus = 'modified'
WHERE id = ?
`).run(title, content, id);
}
// 后台同步函数
async function syncIfOnline() {
if (!navigator.onLine) return;
const pendingNotes = db.prepare(
"SELECT * FROM notes WHERE _syncStatus != 'synced'"
).all();
for (let note of pendingNotes) {
try {
await fetch('https://api.example.com/notes/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(note),
});
// 标记为已同步
db.prepare("UPDATE notes SET _syncStatus = 'synced' WHERE id = ?").run(note.id);
} catch (err) {
console.error('同步失败,将在下次联网时重试', err);
break; // 失败中断,等待下次触发
}
}
}
结合 Electron 的网络状态 API(net.isOnline(),或在主进程监听 online 事件),可以做到自动触发同步。
13.3.3 最佳实践与常见陷阱
1. 用户数据目录规范
永远使用 app.getPath('userData') 作为用户专属数据的存储根目录,而不是项目安装目录。这个目录在不同操作系统下都是标准的用户应用数据缓存位置,即使应用更新或卸载,用户数据也可保留。
2. 数据库迁移
随着版本迭代,数据库结构会变化。推荐使用版本化迁移脚本,例如在 better-sqlite3 的初始化中执行 db.pragma('user_version = X') 并检查版本,不匹配时执行迁移 SQL。
3. 避免滥用 localStorage
localStorage 性能较差且容量有限,只适合保存少量临时状态。对于结构化数据,请换用 IndexedDB 或 SQLite,否则随着数据量增加,UI 会变得卡顿。
4. 离线缓存大小控制
虽然本地存储几乎无限制,但也要考虑用户磁盘空间。特别是日志、缓存文件等,需要设定上限和清理策略,避免 App 占用空间过大遭到用户删除。
5. 安全性
所有本地数据都应视为用户私有,不要在未加密的情况下存储敏感信息(如密码)。Electron 提供了 safeStorage API 可以对敏感数据进行操作系统级别的加密存储。
通过合理的静态资源打包和业务数据离线方案,你的 Electron 应用可以摆脱对网络的强依赖,成为一个真正可靠的“本地优先”软件。用户在地下室、飞机上或网络高峰期,都能获得流畅的使用体验,而这正是桌面应用相较于纯 Web 应用的核心竞争力之一。