网络请求与数据存储

深入理解 Electron 应用中的网络请求实现、本地数据库(SQLite、Better-SQLite3)的使用,以及 electron-store 等轻量级存储方案。


网络请求

使用 Node.js 原生 fetch

Node.js 18+ 原生支持 fetch:

// 主进程网络请求
async function fetchData(url) {
  try {
    const response = await fetch('https://api.example.com/data');

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (err) {
    console.error('Network error:', err);
    throw err;
  }
}

// 带认证的请求
async function fetchWithAuth(url, token) {
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  });

  return response.json();
}

// POST 请求
async function postData(url, data) {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  });

  return response.json();
}

使用 axios(推荐)

axios 是最常用的 HTTP 客户端:

const axios = require('axios');

// 安装:npm install axios

async function getData() {
  const response = await axios.get('https://api.example.com/data', {
    timeout: 10000,  // 10秒超时
    headers: {
      'User-Agent': 'MyElectronApp/1.0'
    }
  });

  return response.data;
}

async function postData(url, data) {
  const response = await axios.post(url, data, {
    timeout: 10000,
    headers: {
      'Authorization': 'Bearer token'
    }
  });

  return response.data;
}

// 请求拦截器
axios.interceptors.request.use(config => {
  console.log('Request:', config.url);
  return config;
});

// 响应拦截器
axios.interceptors.response.use(
  response => response,
  error => {
    console.error('Response error:', error.message);
    return Promise.reject(error);
  }
);

代理设置

如果需要通过代理访问网络:

const axios = require('axios');

const proxyAgent = new (require('https-proxy-agent'))('http://proxy.example.com:8080');

const response = await axios.get('https://api.example.com/data', {
  httpAgent: proxyAgent,
  httpsAgent: proxyAgent
});

本地数据库

SQLite 与 better-sqlite3

better-sqlite3 是同步的 SQLite 绑定,性能优秀:

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

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

// 启用 WAL 模式,提升并发性能
db.pragma('journal_mode = WAL');

创建表

function initializeDatabase() {
  // 创建用户表
  db.exec(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      email TEXT UNIQUE,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);

  // 创建设置表
  db.exec(`
    CREATE TABLE IF NOT EXISTS settings (
      key TEXT PRIMARY KEY,
      value TEXT NOT NULL
    )
  `);

  // 创建文章表
  db.exec(`
    CREATE TABLE IF NOT EXISTS articles (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      title TEXT NOT NULL,
      content TEXT,
      status TEXT DEFAULT 'draft',
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);
}

插入数据

// 插入单条
const insertUser = db.prepare(`
  INSERT INTO users (name, email) VALUES (?, ?)
`);

const result = insertUser.run('张三', '[email protected]');
console.log('Inserted user ID:', result.lastInsertRowid);

// 插入多条(事务)
function insertUsersBatch(users) {
  const insert = db.prepare(`
    INSERT INTO users (name, email) VALUES (?, ?)
  `);

  const transaction = db.transaction((items) => {
    for (const user of items) {
      insert.run(user.name, user.email);
    }
  });

  transaction(users);
  console.log(`Inserted ${users.length} users`);
}

查询数据

// 查询单条
const getUser = db.prepare(`
  SELECT * FROM users WHERE id = ?
`);

const user = getUser.get(1);
console.log(user);

// 查询多条
const getAllUsers = db.prepare('SELECT * FROM users');
const users = getAllUsers.all();

// 带条件查询
const searchUsers = db.prepare(`
  SELECT * FROM users WHERE name LIKE ? ORDER BY created_at DESC
`);

const results = searchUsers.all('%张%');

// 聚合查询
const countUsers = db.prepare('SELECT COUNT(*) as count FROM users');
const { count } = countUsers.get();

更新和删除

// 更新
const updateUser = db.prepare(`
  UPDATE users SET email = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`);

updateUser.run('[email protected]', 1);

// 删除
const deleteUser = db.prepare('DELETE FROM users WHERE id = ?');
deleteUser.run(1);

数据库迁移

const migrations = [
  {
    version: 1,
    up: `
      CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        name TEXT
      )
    `
  },
  {
    version: 2,
    up: `
      ALTER TABLE users ADD COLUMN email TEXT;
    `
  },
  {
    version: 3,
    up: `
      CREATE TABLE settings (
        key TEXT PRIMARY KEY,
        value TEXT
      )
    `
  }
];

function runMigrations() {
  // 创建版本表
  db.exec(`
    CREATE TABLE IF NOT EXISTS schema_migrations (
      version INTEGER PRIMARY KEY
    )
  `);

  const currentVersion = db.prepare('SELECT MAX(version) as v FROM schema_migrations').get().v || 0;

  for (const migration of migrations) {
    if (migration.version > currentVersion) {
      console.log(`Running migration ${migration.version}`);
      db.exec(migration.up);
      db.prepare('INSERT INTO schema_migrations (version) VALUES (?)').run(migration.version);
    }
  }
}

electron-store(键值存储)

electron-store 是轻量级的 JSON 存储方案:

const Store = require('electron-store');

// 定义 schema
const store = new Store({
  name: 'config',
  defaults: {
    window: {
      width: 1200,
      height: 800,
      x: undefined,
      y: undefined
    },
    theme: 'light',
    autoSave: true,
    recentFiles: []
  }
});

// 使用
store.get('window.width');  // 1200
store.set('theme', 'dark');
store.get('theme');         // 'dark'

// 获取嵌套属性
store.get('window.bounds.x');

// 删除
store.delete('recentFiles');

// 检查是否存在
store.has('theme');  // true

electron-store 高级用法

const Store = require('electron-store');
const schema = {
  settings: {
    type: 'object',
    properties: {
      theme: {
        type: 'string',
        enum: ['light', 'dark', 'system'],
        default: 'system'
      },
      fontSize: {
        type: 'number',
        minimum: 10,
        maximum: 24,
        default: 14
      }
    },
    default: {}
  }
};

const store = new Store({ schema });

// 监听变化
store.onDidChange('settings.theme', (newValue, oldValue) => {
  console.log(`Theme changed from ${oldValue} to ${newValue}`);
});

// 清除所有
store.clear();

IndexedDB(浏览器存储)

在渲染进程中使用 IndexedDB:

// renderer.js - 打开数据库
function openDatabase() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('MyAppDB', 1);

    request.onerror = () => reject(request.error);
    request.onsuccess = () => resolve(request.result);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;

      // 创建对象存储
      if (!db.objectStoreNames.contains('documents')) {
        const store = db.createObjectStore('documents', {
          keyPath: 'id',
          autoIncrement: true
        });

        store.createIndex('title', 'title', { unique: false });
        store.createIndex('createdAt', 'createdAt', { unique: false });
      }

      if (!db.objectStoreNames.contains('cache')) {
        db.createObjectStore('cache', { keyPath: 'url' });
      }
    };
  });
}

IndexedDB CRUD 操作

// 添加
async function addDocument(db, doc) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('documents', 'readwrite');
    const store = tx.objectStore('documents');
    const request = store.add({ ...doc, createdAt: new Date() });

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// 查询
async function getDocument(db, id) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('documents', 'readonly');
    const store = tx.objectStore('documents');
    const request = store.get(id);

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// 更新
async function updateDocument(db, doc) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('documents', 'readwrite');
    const store = tx.objectStore('documents');
    const request = store.put({ ...doc, updatedAt: new Date() });

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// 删除
async function deleteDocument(db, id) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('documents', 'readwrite');
    const store = tx.objectStore('documents');
    const request = store.delete(id);

    request.onsuccess = () => resolve();
    request.onerror = () => reject(request.error);
  });
}

// 查询所有
async function getAllDocuments(db) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('documents', 'readonly');
    const store = tx.objectStore('documents');
    const request = store.getAll();

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

缓存策略

内存缓存

class MemoryCache {
  constructor(maxSize = 100, ttl = 3600000) {
    this.cache = new Map();
    this.maxSize = maxSize;
    this.ttl = ttl;  // 毫秒
  }

  set(key, value) {
    if (this.cache.size >= this.maxSize) {
      // 删除最老的条目
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }

    this.cache.set(key, {
      value,
      timestamp: Date.now()
    });
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;

    // 检查是否过期
    if (Date.now() - item.timestamp > this.ttl) {
      this.cache.delete(key);
      return null;
    }

    return item.value;
  }

  clear() {
    this.cache.clear();
  }
}

HTTP 缓存

const { session } = require('electron');

// 配置会话缓存
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
  // 可以在这里添加自定义请求头
  callback({
    cancel: false,
    requestHeaders: {
      ...details.requestHeaders,
      'X-Custom-Header': 'value'
    }
  });
});

// 配置响应缓存
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
  callback({
    cancel: false,
    responseHeaders: {
      ...details.responseHeaders,
      'Cache-Control': ['max-age=3600']
    }
  });
});

数据同步策略

离线优先

class OfflineFirstSync {
  constructor(db, api) {
    this.db = db;
    this.api = api;
    this.syncQueue = [];
  }

  async save(doc) {
    // 先保存到本地
    await this.db.put('documents', doc);

    // 加入同步队列
    this.syncQueue.push({
      action: 'save',
      doc,
      timestamp: Date.now()
    });

    // 尝试同步
    this.trySync();
  }

  async trySync() {
    if (!navigator.onLine) return;

    while (this.syncQueue.length > 0) {
      const item = this.syncQueue[0];

      try {
        await this.api.sync(item);
        this.syncQueue.shift();
      } catch (err) {
        console.error('Sync failed:', err);
        break;
      }
    }
  }
}

// 监听网络状态
window.addEventListener('online', () => {
  syncManager.trySync();
});

这一章想说的

Electron 应用的网络和数据存储:

  1. 网络请求:使用原生 fetch 或 axios,主进程可配置代理
  2. SQLite:better-sqlite3 提供高性能的本地数据库
  3. 键值存储:electron-store 适合配置和轻量数据
  4. IndexedDB:适合渲染进程中的结构化数据存储
  5. 缓存策略:内存缓存 + HTTP 缓存 + 离线优先同步

根据数据特点和访问模式选择合适的存储方案。


延展阅读