人人都会AI编程

13.3 离线缓存策略:静态资源缓存、业务数据离线存储

更新时间:2026-07-11

在很多人的印象中,Electron 应用天生“在线”,因为它的壳子里跑的就是一个网页。但现实场景远比这复杂:用户的网络可能不稳定,某些功能需要在无网环境下使用,或者纯粹为了提升秒开体验。良好的离线缓存策略能够让应用在网络抖动时依然流畅运行,甚至在完全断网的情况下提供基本服务。

这一节我们从工程实践出发,分别讨论 静态资源缓存(让界面快速渲染)和 业务数据离线存储(让核心功能不依赖网络)两大部分。所有方案都无需引入第三方云服务,完全基于 Electron 自身能力和 Node.js 生态实现。


13.3.1 静态资源缓存:让界面“秒开”

Electron 应用通常使用 win.loadURLwin.loadFile 加载页面。如果采用 loadURL 加载远程地址,每次冷启动都依赖网络,速度慢且不可控。推荐的现代方案是 本地优先策略,即所有静态资源都随应用包发布,让渲染进程从本地文件系统加载。

方案一:纯本地部署(推荐)

直接把前端构建产物(HTML/JS/CSS/图片)打包进应用的 resources 目录,通过 loadFile 加载入口文件。这是最彻底、最可靠的离线方案。

实现步骤:

  1. 前端项目构建产出 dist 文件夹。
  2. 在 electron-builder 配置中将 dist 目录包含进 app 包内:
// package.json (electron-builder 配置片段)
"build": {
  "files": [
    "main.js",
    "preload.js",
    "dist/**/*"   // 前端静态文件
  ]
}
  1. 主进程加载本地文件:
// 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,数据存储在一个文件中,方便备份和迁移。

集成示例:

  1. 安装依赖:
npm install better-sqlite3
  1. 在主进程中创建数据库连接(需要 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 };
  1. 在 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'),
});
  1. 主进程 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相关的缓存。

实践三:离线同步策略(基础版)

有了本地存储,下一步往往是设计一个同步机制,保证离线修改的数据能在联网后与服务端握手。一种简单实用的方案是 本地修改记录 + 后台同步队列

核心思路:

  1. 为每条业务记录增加 _syncStatus 字段(值如 synced, modified, deleted)。
  2. 用户在离线时进行增删改,所有操作标记为 modifieddeleted(而不是直接删除)。
  3. 监听网络状态变化(navigator.onLine 或 Electron 的 net 模块事件),一旦联网,启动同步进程。
  4. 同步进程查询所有 _syncStatus !== 'synced' 的记录,依次发送到远端 API,成功后更新状态为 synced
  5. 冲突处理可以简单采用“最后写入胜出”(基于时间戳),或提示用户手动合并。

示例:在 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 应用的核心竞争力之一。