Electron 文件系统与数据存储

读写文件、路径选择、localStorage / IndexedDB / SQLite / JSON 的取舍。

Intermediate
Key Points文件操作只在主进程做,通过 IPC 暴露给页面;数据持久化优先用 userData 目录下的文件或 SQLite。

路径选择

JavaScript
const { app } = require('electron');
const path = require('node:path');

app.getPath('userData');     // 配置、数据库 → 首选
app.getPath('home');
app.getPath('documents');
app.getPath('downloads');
app.getPath('temp');
app.getPath('logs');         // 建议显式 setAppLogsPath()

path.join(app.getPath('userData'), 'settings.json');

userData is a per-app directory, usually preserved on uninstall — right for user data. __dirname points at the code, which after packaging is a read-only asar archive.

读写文件

JavaScript
// main.js
const fs = require('node:fs/promises');
const { ipcMain, app } = require('electron');
const path = require('node:path');

const CONFIG = path.join(app.getPath('userData'), 'settings.json');

ipcMain.handle('config:load', async () => {
  try {
    return JSON.parse(await fs.readFile(CONFIG, 'utf8'));
  } catch (e) {
    if (e.code === 'ENOENT') return {};       // 首次运行,没有文件
    throw e;
  }
});

ipcMain.handle('config:save', async (_e, data) => {
  const tmp = CONFIG + '.tmp';
  await fs.writeFile(tmp, JSON.stringify(data, null, 2), 'utf8');
  await fs.rename(tmp, CONFIG);               // 原子替换,避免写一半崩溃
  return true;
});

Writing .tmp then rename is the standard atomic write: within one filesystem rename is atomic, so you get either the old file or the new one, never a half-written JSON.

大文件与流

JavaScript
const { createReadStream, createWriteStream } = require('node:fs');
const { pipeline } = require('node:stream/promises');

await pipeline(
  createReadStream(srcPath),
  createWriteStream(destPath)
);

fs.readFile loads the whole file into memory and blows up on hundreds of MB. Copying and transcoding must use streams, where memory stays constant.

存储方案对比

OptionCapacityBest for
localStorage约 5MB少量偏好设置
IndexedDB几百 MB结构化数据、离线缓存
JSON 文件几 MB配置、简单状态
SQLiteGB 级大量记录、需要查询
自定义文件格式不限文档、媒体

SQLite

JavaScript
const Database = require('better-sqlite3');
const path = require('node:path');
const { app } = require('electron');

const db = new Database(path.join(app.getPath('userData'), 'app.db'));
db.pragma('journal_mode = WAL');

db.exec(`
  CREATE TABLE IF NOT EXISTS notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    body TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
  )
`);

const insert = db.prepare('INSERT INTO notes (title, body) VALUES (?, ?)');
insert.run('第一条', '内容');

const rows = db.prepare('SELECT * FROM notes ORDER BY id DESC LIMIT 20').all();

better-sqlite3 is synchronous; in the main process a query over hundreds of thousands of rows freezes the UI. For large data use the async sqlite3 API or move queries into a worker.

better-sqlite3 is a native module and must be rebuilt with electron-rebuild against Electron's ABI, or it fails at runtime with a NODE_MODULE_VERSION mismatch — a failure that appears only when launching the app, never during npm install.

安全提醒

JavaScript
// 危险:页面传来的路径直接读
ipcMain.handle('read', (_e, p) => fs.readFile(p, 'utf8'));

// 安全:限制在允许的目录内
ipcMain.handle('read', async (_e, p) => {
  const base = path.join(app.getPath('documents'), 'myapp');
  const target = path.resolve(base, p);
  if (!target.startsWith(base + path.sep)) {
    throw new Error('路径越界');
  }
  return fs.readFile(target, 'utf8');
});

path.resolve collapses ../, so resolve first and then compare prefixes. Without the check, one XSS in the page reads ~/.ssh/id_rsa.

Friends & Partners