Data persistence is a common requirement in Electron applications, including user settings, application data, cache, etc. This article will detail various data persistence solutions in Electron.
Local Storage Solutions
1. localStorage
localStorage is the simplest storage solution, suitable for storing small amounts of data.
javascript// renderer.js // Store data localStorage.setItem('userSettings', JSON.stringify({ theme: 'dark', language: 'zh-CN', fontSize: 14 })) // Read data const settings = JSON.parse(localStorage.getItem('userSettings')) console.log(settings) // Delete data localStorage.removeItem('userSettings') // Clear all data localStorage.clear()
Advantages:
- Simple to use
- Synchronous API
- Data persistence
Disadvantages:
- Limited storage capacity (usually 5-10MB)
- Can only store strings
- Not suitable for large amounts of data
2. sessionStorage
sessionStorage is similar to localStorage, but data is cleared when the page session ends.
javascript// renderer.js // Store temporary data sessionStorage.setItem('tempData', JSON.stringify({ token: 'abc123', timestamp: Date.now() })) // Read data const tempData = JSON.parse(sessionStorage.getItem('tempData'))
Use Cases:
- Temporary session data
- Passing data between pages
- Data that doesn't need persistence
3. IndexedDB
IndexedDB is a powerful database provided by browsers, suitable for storing large amounts of structured data.
javascript// renderer.js // Open database const request = indexedDB.open('MyAppDB', 1) request.onerror = (event) => { console.error('Database error:', event.target.error) } request.onsuccess = (event) => { const db = event.target.result console.log('Database opened successfully') } request.onupgradeneeded = (event) => { const db = event.target.result // Create object store const objectStore = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true }) // Create indexes objectStore.createIndex('name', 'name', { unique: false }) objectStore.createIndex('email', 'email', { unique: true }) } // Add data function addUser(db, userData) { const transaction = db.transaction(['users'], 'readwrite') const objectStore = transaction.objectStore('users') const request = objectStore.add(userData) request.onsuccess = () => { console.log('User added successfully') } request.onerror = () => { console.error('Error adding user:', request.error) } } // Query data function getUser(db, userId) { const transaction = db.transaction(['users'], 'readonly') const objectStore = transaction.objectStore('users') const request = objectStore.get(userId) request.onsuccess = () => { const user = request.result console.log('User:', user) } } // Update data function updateUser(db, userData) { const transaction = db.transaction(['users'], 'readwrite') const objectStore = transaction.objectStore('users') const request = objectStore.put(userData) request.onsuccess = () => { console.log('User updated successfully') } } // Delete data function deleteUser(db, userId) { const transaction = db.transaction(['users'], 'readwrite') const objectStore = transaction.objectStore('users') const request = objectStore.delete(userId) request.onsuccess = () => { console.log('User deleted successfully') } }
Advantages:
- Large storage capacity (usually hundreds of MB)
- Supports transactions
- Supports indexed queries
- Asynchronous operations, doesn't block UI
Disadvantages:
- Relatively complex API
- Asynchronous operations require callbacks or Promises
4. Using Dexie.js to Simplify IndexedDB
Dexie.js is a wrapper for IndexedDB that provides a simpler API.
javascript// renderer.js import Dexie from 'dexie' // Create database const db = new Dexie('MyAppDB') // Define table structure db.version(1).stores({ users: '++id, name, email', posts: '++id, title, userId', comments: '++id, content, postId' }) // Add data async function addUser(name, email) { try { const id = await db.users.add({ name, email }) console.log('User added with ID:', id) return id } catch (error) { console.error('Error adding user:', error) } } // Query data async function getAllUsers() { try { const users = await db.users.toArray() return users } catch (error) { console.error('Error fetching users:', error) } } // Conditional query async function getUserByEmail(email) { try { const user = await db.users.where('email').equals(email).first() return user } catch (error) { console.error('Error fetching user:', error) } } // Update data async function updateUser(id, updates) { try { await db.users.update(id, updates) console.log('User updated successfully') } catch (error) { console.error('Error updating user:', error) } } // Delete data async function deleteUser(id) { try { await db.users.delete(id) console.log('User deleted successfully') } catch (error) { console.error('Error deleting user:', error) } }
File System Storage
1. Using Node.js fs Module
In the main process, you can use Node.js's fs module for file operations.
javascript// main.js const fs = require('fs').promises const path = require('path') const { app } = require('electron') // Get user data directory const userDataPath = app.getPath('userData') const dataFilePath = path.join(userDataPath, 'data.json') // Save data async function saveData(data) { try { await fs.writeFile(dataFilePath, JSON.stringify(data, null, 2), 'utf-8') console.log('Data saved successfully') } catch (error) { console.error('Error saving data:', error) } } // Read data async function loadData() { try { const data = await fs.readFile(dataFilePath, 'utf-8') return JSON.parse(data) } catch (error) { if (error.code === 'ENOENT') { // File doesn't exist, return default data return {} } console.error('Error loading data:', error) return null } } // Delete data async function deleteData() { try { await fs.unlink(dataFilePath) console.log('Data deleted successfully') } catch (error) { console.error('Error deleting data:', error) } }
2. Using electron-store
electron-store is a simple data storage library designed specifically for Electron.
bashnpm install electron-store
javascript// main.js const Store = require('electron-store') const store = new Store({ defaults: { settings: { theme: 'light', language: 'en', fontSize: 14 }, userData: {} } }) // Save data store.set('settings.theme', 'dark') store.set('userData.name', 'John Doe') // Read data const theme = store.get('settings.theme') const userData = store.get('userData') // Get all data const allData = store.store // Delete data store.delete('userData.name') // Clear data store.clear() // Check if data exists const hasTheme = store.has('settings.theme') // Listen for data changes store.onDidChange('settings.theme', (newValue, oldValue) => { console.log('Theme changed from', oldValue, 'to', newValue) })
3. Using lowdb
lowdb is a lightweight JSON database.
bashnpm install lowdb
javascript// main.js const Low = require('lowdb') const FileSync = require('lowdb/adapters/FileSync') const path = require('path') const { app } = require('electron') // Create database const adapter = new FileSync(path.join(app.getPath('userData'), 'db.json')) const db = new Low(adapter) // Initialize default data db.defaults({ users: [], posts: [], settings: { theme: 'light', language: 'en' } }).write() // Add data db.get('users') .push({ id: 1, name: 'John', email: 'john@example.com' }) .write() // Query data const users = db.get('users').value() const user = db.get('users').find({ id: 1 }).value() // Update data db.get('users') .find({ id: 1 }) .assign({ name: 'John Doe' }) .write() // Delete data db.get('users') .remove({ id: 1 }) .write() // Chained queries const activeUsers = db.get('users') .filter({ status: 'active' }) .orderBy('name') .value()
SQLite Database
1. Using better-sqlite3
better-sqlite3 is a synchronous SQLite binding with excellent performance.
bashnpm install better-sqlite3
javascript// main.js const Database = require('better-sqlite3') const path = require('path') const { app } = require('electron') // Create database connection const dbPath = path.join(app.getPath('userData'), 'app.db') const db = new Database(dbPath) // Create tables db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, user_id INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ) `) // Insert data function insertUser(name, email) { const stmt = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)') const result = stmt.run(name, email) return result.lastInsertRowid } // Query data function getAllUsers() { const stmt = db.prepare('SELECT * FROM users') return stmt.all() } function getUserById(id) { const stmt = db.prepare('SELECT * FROM users WHERE id = ?') return stmt.get(id) } function getUserByEmail(email) { const stmt = db.prepare('SELECT * FROM users WHERE email = ?') return stmt.get(email) } // Update data function updateUser(id, updates) { const stmt = db.prepare('UPDATE users SET name = ?, email = ? WHERE id = ?') const result = stmt.run(updates.name, updates.email, id) return result.changes } // Delete data function deleteUser(id) { const stmt = db.prepare('DELETE FROM users WHERE id = ?') const result = stmt.run(id) return result.changes } // Transaction handling function transferFunds(fromUserId, toUserId, amount) { const insertStmt = db.prepare('INSERT INTO transactions (from_user, to_user, amount) VALUES (?, ?, ?)') const updateStmt = db.prepare('UPDATE users SET balance = balance - ? WHERE id = ?') const transaction = db.transaction((from, to, amt) => { updateStmt.run(amt, from) updateStmt.run(-amt, to) insertStmt.run(from, to, amt) }) transaction(fromUserId, toUserId, amount) }
2. Using Knex.js
Knex.js is a SQL query builder that supports multiple databases.
bashnpm install knex sqlite3
javascript// main.js const knex = require('knex')({ client: 'sqlite3', connection: { filename: path.join(app.getPath('userData'), 'app.db') }, useNullAsDefault: true }) // Create tables knex.schema .createTableIfNotExists('users', (table) => { table.increments('id').primary() table.string('name').notNullable() table.string('email').unique().notNullable() table.timestamps(true, true) }) .createTableIfNotExists('posts', (table) => { table.increments('id').primary() table.string('title').notNullable() table.text('content') table.integer('user_id').references('id').inTable('users') table.timestamps(true, true) }) .then(() => { console.log('Tables created successfully') }) // Insert data async function insertUser(name, email) { try { const [id] = await knex('users').insert({ name, email }) return id } catch (error) { console.error('Error inserting user:', error) } } // Query data async function getAllUsers() { try { const users = await knex('users').select('*') return users } catch (error) { console.error('Error fetching users:', error) } } async function getUserById(id) { try { const user = await knex('users').where({ id }).first() return user } catch (error) { console.error('Error fetching user:', error) } } // Update data async function updateUser(id, updates) { try { const count = await knex('users').where({ id }).update(updates) return count } catch (error) { console.error('Error updating user:', error) } } // Delete data async function deleteUser(id) { try { const count = await knex('users').where({ id }).del() return count } catch (error) { console.error('Error deleting user:', error) } } // Join queries async function getUserWithPosts(userId) { try { const user = await knex('users') .leftJoin('posts', 'users.id', 'posts.user_id') .where('users.id', userId) .select('users.*', 'posts.title as post_title') return user } catch (error) { console.error('Error fetching user with posts:', error) } }
Data Sync Solutions
1. Sync Local Data to Cloud
javascript// main.js const axios = require('axios') async function syncDataToCloud(localData) { try { const response = await axios.post('https://api.example.com/sync', { data: localData, timestamp: Date.now() }) return response.data } catch (error) { console.error('Sync failed:', error) throw error } } async function syncDataFromCloud() { try { const response = await axios.get('https://api.example.com/sync') return response.data } catch (error) { console.error('Sync failed:', error) throw error } }
2. Conflict Resolution
javascript// main.js function resolveConflict(localData, remoteData) { const localTimestamp = localData.timestamp || 0 const remoteTimestamp = remoteData.timestamp || 0 if (localTimestamp > remoteTimestamp) { return localData } else if (remoteTimestamp > localTimestamp) { return remoteData } else { // Timestamps are the same, use merge strategy return { ...localData, ...remoteData } } }
Best Practices
1. Data Encryption
javascript// main.js const crypto = require('crypto') function encryptData(data, key) { const algorithm = 'aes-256-cbc' const iv = crypto.randomBytes(16) const cipher = crypto.createCipheriv(algorithm, key, iv) let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex') encrypted += cipher.final('hex') return iv.toString('hex') + ':' + encrypted } function decryptData(encryptedData, key) { const algorithm = 'aes-256-cbc' const parts = encryptedData.split(':') const iv = Buffer.from(parts.shift(), 'hex') const encrypted = parts.join(':') const decipher = crypto.createDecipheriv(algorithm, key, iv) let decrypted = decipher.update(encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8') return JSON.parse(decrypted) }
2. Data Backup
javascript// main.js const fs = require('fs').promises const path = require('path') const { app } = require('electron') async function backupData() { const userDataPath = app.getPath('userData') const backupPath = path.join(userDataPath, 'backups', Date.now().toString()) try { await fs.mkdir(backupPath, { recursive: true }) // Copy data files const files = await fs.readdir(userDataPath) for (const file of files) { if (!file.startsWith('backup')) { await fs.copyFile( path.join(userDataPath, file), path.join(backupPath, file) ) } } console.log('Backup created successfully') } catch (error) { console.error('Backup failed:', error) } }
3. Data Migration
javascript// main.js const migrations = [ { version: 1, migrate: (data) => { // Version 1 migration logic return { ...data, version: 1 } } }, { version: 2, migrate: (data) => { // Version 2 migration logic data.users = data.users || [] return { ...data, version: 2 } } } ] async function runMigrations(data) { const currentVersion = data.version || 0 for (const migration of migrations) { if (migration.version > currentVersion) { data = migration.migrate(data) } } return data }
Common Questions
Q: How to choose the right data persistence solution?A: Choose based on data volume and complexity:
- Small amount of simple data: localStorage
- Medium structured data: IndexedDB or electron-store
- Large complex data: SQLite or other databases
Q: How to handle data encryption?A: Use Node.js's crypto module for encryption, ensure keys are stored securely.
Q: How to implement data synchronization?A: Use timestamps and version numbers for conflict detection and resolution, regularly sync local and cloud data.
Q: How to optimize database performance?A: Use indexes, batch operations, transactions, connection pools, and other techniques to optimize database performance.