Skip to content

第9章:Electron 网络请求与本地存储

9.1 渲染进程网络请求

在 Electron 中,渲染进程可以使用与前端浏览器相同的网络请求方法,如 fetch API 或第三方库如 axios

9.1.1 使用 fetch API

javascript
// 发送 GET 请求
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data')
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }
    const data = await response.json()
    console.log('获取的数据:', data)
  } catch (error) {
    console.error('请求失败:', error)
  }
}

// 发送 POST 请求
async function postData() {
  try {
    const response = await fetch('https://api.example.com/data', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ name: 'John', age: 30 })
    })
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }
    const data = await response.json()
    console.log('响应数据:', data)
  } catch (error) {
    console.error('请求失败:', error)
  }
}

// 调用函数
fetchData()
postData()

9.1.2 使用 axios

  1. 安装依赖

    bash
    npm install axios
  2. 使用示例

    javascript
    const axios = require('axios')
    
    // 发送 GET 请求
    async function fetchData() {
      try {
        const response = await axios.get('https://api.example.com/data')
        console.log('获取的数据:', response.data)
      } catch (error) {
        console.error('请求失败:', error)
      }
    }
    
    // 发送 POST 请求
    async function postData() {
      try {
        const response = await axios.post('https://api.example.com/data', {
          name: 'John',
          age: 30
        })
        console.log('响应数据:', response.data)
      } catch (error) {
        console.error('请求失败:', error)
      }
    }
    
    // 调用函数
    fetchData()
    postData()

9.2 主进程网络请求

主进程也可以执行网络请求,推荐使用 request 模块或 Node.js 内置的 http/https 模块。

9.2.1 使用 request 模块

  1. 安装依赖

    bash
    npm install request
  2. 使用示例

    javascript
    const request = require('request')
    
    // 发送 GET 请求
    request.get('https://api.example.com/data', (error, response, body) => {
      if (error) {
        console.error('请求失败:', error)
        return
      }
      if (response.statusCode !== 200) {
        console.error('请求失败,状态码:', response.statusCode)
        return
      }
      const data = JSON.parse(body)
      console.log('获取的数据:', data)
    })
    
    // 发送 POST 请求
    request.post({
      url: 'https://api.example.com/data',
      json: { name: 'John', age: 30 }
    }, (error, response, body) => {
      if (error) {
        console.error('请求失败:', error)
        return
      }
      if (response.statusCode !== 200) {
        console.error('请求失败,状态码:', response.statusCode)
        return
      }
      console.log('响应数据:', body)
    })

9.2.2 使用 Node.js 内置模块

javascript
const https = require('https')

// 发送 GET 请求
const options = {
  hostname: 'api.example.com',
  path: '/data',
  method: 'GET'
}

const req = https.request(options, (res) => {
  let data = ''
  res.on('data', (chunk) => {
    data += chunk
  })
  res.on('end', () => {
    console.log('获取的数据:', JSON.parse(data))
  })
})

req.on('error', (error) => {
  console.error('请求失败:', error)
})

req.end()

// 发送 POST 请求
const postData = JSON.stringify({ name: 'John', age: 30 })

const postOptions = {
  hostname: 'api.example.com',
  path: '/data',
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(postData)
  }
}

const postReq = https.request(postOptions, (res) => {
  let data = ''
  res.on('data', (chunk) => {
    data += chunk
  })
  res.on('end', () => {
    console.log('响应数据:', JSON.parse(data))
  })
})

postReq.on('error', (error) => {
  console.error('请求失败:', error)
})

postReq.write(postData)
postReq.end()

9.3 跨域问题解决

在 Electron 中,由于应用运行在本地环境,跨域限制与浏览器有所不同。

9.3.1 渲染进程跨域配置

默认情况下,Electron 的渲染进程会遵循浏览器的同源策略。要解决跨域问题,可以在创建窗口时配置 webPreferences

javascript
const { app, BrowserWindow } = require('electron')

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
      webSecurity: false // 禁用同源策略,允许跨域请求
    }
  })
  
  win.loadFile('index.html')
}

app.whenReady().then(createWindow)

9.3.2 使用代理解决跨域

另一种更安全的方法是使用主进程作为代理,处理跨域请求:

  1. 主进程代码

    javascript
    const { app, BrowserWindow, ipcMain } = require('electron')
    const axios = require('axios')
    
    ipcMain.handle('fetch-data', async (event, url, options = {}) => {
      try {
        const response = await axios({
          url,
          ...options
        })
        return response.data
      } catch (error) {
        throw error
      }
    })
  2. 渲染进程代码

    javascript
    const { ipcRenderer } = require('electron')
    
    async function fetchData() {
      try {
        const data = await ipcRenderer.invoke('fetch-data', 'https://api.example.com/data')
        console.log('获取的数据:', data)
      } catch (error) {
        console.error('请求失败:', error)
      }
    }
    
    async function postData() {
      try {
        const data = await ipcRenderer.invoke('fetch-data', 'https://api.example.com/data', {
          method: 'POST',
          data: { name: 'John', age: 30 }
        })
        console.log('响应数据:', data)
      } catch (error) {
        console.error('请求失败:', error)
      }
    }
    
    // 调用函数
    fetchData()
    postData()

9.4 本地存储方案

9.4.1 localStorage

localStorage 是浏览器提供的本地存储 API,在 Electron 的渲染进程中也可以使用:

javascript
// 存储数据
localStorage.setItem('user', JSON.stringify({ name: 'John', email: 'john@example.com' }))

// 获取数据
const user = JSON.parse(localStorage.getItem('user'))

// 删除数据
localStorage.removeItem('user')

// 清空所有数据
localStorage.clear()

特点

  • 存储在渲染进程中,仅对当前窗口有效
  • 存储容量约为 5MB
  • 数据以字符串形式存储
  • 适合存储轻量级数据,如用户偏好设置

9.4.2 electron-store

electron-store 是一个专为 Electron 设计的持久化存储库,支持主进程和渲染进程使用:

  1. 安装依赖

    bash
    npm install electron-store
  2. 使用示例

    javascript
    const Store = require('electron-store')
    
    // 创建存储实例
    const store = new Store({
      name: 'app-config',
      defaults: {
        theme: 'light',
        language: 'zh-CN',
        windowSize: { width: 800, height: 600 }
      }
    })
    
    // 存储数据
    store.set('user', { name: 'John', email: 'john@example.com' })
    
    // 获取数据
    const user = store.get('user')
    
    // 删除数据
    store.delete('user')
    
    // 清空所有数据
    store.clear()
    
    // 检查键是否存在
    const hasUser = store.has('user')
    
    // 获取所有数据
    const allData = store.store

特点

  • 存储在文件系统中,对整个应用有效
  • 存储容量几乎不受限制
  • 支持存储复杂数据类型
  • 适合存储应用配置、用户数据等

9.4.3 数据库存储

对于需要存储大量结构化数据的应用,可以使用数据库:

9.4.3.1 SQLite

  1. 安装依赖

    bash
    npm install sqlite3
  2. 使用示例

    javascript
    const sqlite3 = require('sqlite3').verbose()
    
    // 打开数据库连接
    const db = new sqlite3.Database('./app.db', (err) => {
      if (err) {
        console.error('打开数据库失败:', err)
        return
      }
      console.log('数据库连接成功')
    })
    
    // 创建表
    db.run(`CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      email TEXT UNIQUE NOT NULL
    )`)
    
    // 插入数据
    db.run('INSERT INTO users (name, email) VALUES (?, ?)', ['John', 'john@example.com'], function(err) {
      if (err) {
        console.error('插入数据失败:', err)
        return
      }
      console.log(`插入成功,ID: ${this.lastID}`)
    })
    
    // 查询数据
    db.all('SELECT * FROM users', [], (err, rows) => {
      if (err) {
        console.error('查询数据失败:', err)
        return
      }
      console.log('查询结果:', rows)
    })
    
    // 更新数据
    db.run('UPDATE users SET name = ? WHERE id = ?', ['Jane', 1], function(err) {
      if (err) {
        console.error('更新数据失败:', err)
        return
      }
      console.log(`更新成功,影响行数: ${this.changes}`)
    })
    
    // 删除数据
    db.run('DELETE FROM users WHERE id = ?', [1], function(err) {
      if (err) {
        console.error('删除数据失败:', err)
        return
      }
      console.log(`删除成功,影响行数: ${this.changes}`)
    })
    
    // 关闭数据库连接
    db.close((err) => {
      if (err) {
        console.error('关闭数据库失败:', err)
        return
      }
      console.log('数据库连接已关闭')
    })

9.4.3.2 MySQL

  1. 安装依赖

    bash
    npm install mysql2
  2. 使用示例

    javascript
    const mysql = require('mysql2/promise')
    
    async function main() {
      try {
        // 创建数据库连接
        const connection = await mysql.createConnection({
          host: 'localhost',
          user: 'root',
          password: 'password',
          database: 'app_db'
        })
    
        console.log('数据库连接成功')
    
        // 创建表
        await connection.execute(`
          CREATE TABLE IF NOT EXISTS users (
            id INT AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(255) NOT NULL,
            email VARCHAR(255) UNIQUE NOT NULL
          )
        `)
    
        // 插入数据
        const [insertResult] = await connection.execute(
          'INSERT INTO users (name, email) VALUES (?, ?)',
          ['John', 'john@example.com']
        )
        console.log(`插入成功,ID: ${insertResult.insertId}`)
    
        // 查询数据
        const [rows] = await connection.execute('SELECT * FROM users')
        console.log('查询结果:', rows)
    
        // 更新数据
        const [updateResult] = await connection.execute(
          'UPDATE users SET name = ? WHERE id = ?',
          ['Jane', 1]
        )
        console.log(`更新成功,影响行数: ${updateResult.affectedRows}`)
    
        // 删除数据
        const [deleteResult] = await connection.execute(
          'DELETE FROM users WHERE id = ?',
          [1]
        )
        console.log(`删除成功,影响行数: ${deleteResult.affectedRows}`)
    
        // 关闭数据库连接
        await connection.end()
        console.log('数据库连接已关闭')
      } catch (error) {
        console.error('数据库操作失败:', error)
      }
    }
    
    // 调用函数
    main()

特点

  • 适合存储大量结构化数据
  • 支持复杂查询和事务
  • SQLite 是文件数据库,无需额外服务
  • MySQL 是客户端-服务器数据库,需要单独安装

9.5 实操案例:网络请求与本地存储

9.5.1 场景描述

创建一个 Electron 应用,实现以下功能:

  • 发送网络请求获取天气数据
  • 将获取的数据存储到本地
  • 读取本地存储的数据并显示
  • 支持手动刷新数据

9.5.2 实现步骤

  1. 创建项目结构

    weather-app/
    ├── main.js
    ├── index.html
    ├── package.json
    └── assets/
        └── icon.png
  2. 修改 package.json

    json
    {
      "name": "weather-app",
      "version": "1.0.0",
      "description": "Electron 天气应用",
      "main": "main.js",
      "scripts": {
        "start": "electron ."
      },
      "devDependencies": {
        "electron": "^18.0.0"
      },
      "dependencies": {
        "axios": "^0.27.2",
        "electron-store": "^8.0.1"
      }
    }
  3. 修改 main.js

    javascript
    const { app, BrowserWindow, ipcMain } = require('electron')
    const axios = require('axios')
    const Store = require('electron-store')
    const path = require('path')
    
    // 创建存储实例
    const store = new Store({
      name: 'weather-data',
      defaults: {
        cities: ['北京', '上海', '广州', '深圳'],
        weatherData: {}
      }
    })
    
    let mainWindow
    
    function createWindow() {
      mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
          nodeIntegration: true,
          contextIsolation: false
        }
      })
    
      mainWindow.loadFile('index.html')
      mainWindow.webContents.openDevTools()
    
      mainWindow.on('closed', () => {
        mainWindow = null
      })
    }
    
    // 处理获取天气数据的请求
    ipcMain.handle('get-weather', async (event, city) => {
      try {
        // 这里使用 OpenWeatherMap API,需要替换为实际的 API Key
        const apiKey = 'YOUR_API_KEY'
        const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${apiKey}&units=metric&lang=zh_cn`
        
        const response = await axios.get(url)
        const weatherData = response.data
        
        // 存储数据
        const currentData = store.get('weatherData')
        currentData[city] = {
          data: weatherData,
          timestamp: new Date().toISOString()
        }
        store.set('weatherData', currentData)
        
        return weatherData
      } catch (error) {
        console.error('获取天气数据失败:', error)
        throw error
      }
    })
    
    // 处理获取存储数据的请求
    ipcMain.handle('get-stored-data', () => {
      return {
        cities: store.get('cities'),
        weatherData: store.get('weatherData')
      }
    })
    
    app.whenReady().then(createWindow)
    
    app.on('window-all-closed', function () {
      if (process.platform !== 'darwin') app.quit()
    })
    
    app.on('activate', function () {
      if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
  4. 修改 index.html

    html
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>天气应用</title>
      <style>
        body {
          font-family: Arial, sans-serif;
          margin: 20px;
          padding: 0;
          background-color: #f0f0f0;
        }
        .container {
          max-width: 700px;
          margin: 0 auto;
          background-color: white;
          padding: 20px;
          border-radius: 8px;
          box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        h1 {
          color: #333;
          text-align: center;
        }
        .city-selector {
          margin: 20px 0;
          text-align: center;
        }
        select {
          padding: 10px;
          font-size: 16px;
          border: 1px solid #ddd;
          border-radius: 4px;
        }
        button {
          padding: 10px 15px;
          background-color: #4CAF50;
          color: white;
          border: none;
          border-radius: 4px;
          cursor: pointer;
          margin-left: 10px;
        }
        button:hover {
          background-color: #45a049;
        }
        .weather-card {
          margin: 20px 0;
          padding: 20px;
          border: 1px solid #ddd;
          border-radius: 4px;
          background-color: #f9f9f9;
        }
        .weather-info {
          display: flex;
          align-items: center;
          justify-content: space-between;
        }
        .weather-main {
          flex: 1;
        }
        .weather-icon {
          font-size: 48px;
        }
        .weather-details {
          margin-top: 10px;
          display: grid;
          grid-template-columns: repeat(2, 1fr);
          gap: 10px;
        }
        .detail-item {
          padding: 5px;
          background-color: #f0f0f0;
          border-radius: 4px;
        }
        .error {
          color: red;
          text-align: center;
          margin: 10px 0;
        }
        .last-updated {
          font-size: 12px;
          color: #666;
          text-align: right;
          margin-top: 10px;
        }
      </style>
    </head>
    <body>
      <div class="container">
        <h1>天气应用</h1>
        
        <div class="city-selector">
          <select id="city-select">
            <option value="北京">北京</option>
            <option value="上海">上海</option>
            <option value="广州">广州</option>
            <option value="深圳">深圳</option>
          </select>
          <button id="refresh-btn">刷新</button>
        </div>
        
        <div id="error" class="error"></div>
        
        <div id="weather-card" class="weather-card" style="display: none;">
          <h2 id="city-name"></h2>
          <div class="weather-info">
            <div class="weather-main">
              <div id="weather-description"></div>
              <div id="temperature"></div>
            </div>
            <div class="weather-icon" id="weather-icon">☁️</div>
          </div>
          <div class="weather-details">
            <div class="detail-item">
              <strong>湿度:</strong> <span id="humidity"></span>
            </div>
            <div class="detail-item">
              <strong>气压:</strong> <span id="pressure"></span>
            </div>
            <div class="detail-item">
              <strong>风速:</strong> <span id="wind-speed"></span>
            </div>
            <div class="detail-item">
              <strong>能见度:</strong> <span id="visibility"></span>
            </div>
          </div>
          <div class="last-updated" id="last-updated"></div>
        </div>
      </div>
      
      <script>
        const { ipcRenderer } = require('electron')
        
        // DOM 元素
        const citySelect = document.getElementById('city-select')
        const refreshBtn = document.getElementById('refresh-btn')
        const errorDiv = document.getElementById('error')
        const weatherCard = document.getElementById('weather-card')
        const cityName = document.getElementById('city-name')
        const weatherDescription = document.getElementById('weather-description')
        const temperature = document.getElementById('temperature')
        const weatherIcon = document.getElementById('weather-icon')
        const humidity = document.getElementById('humidity')
        const pressure = document.getElementById('pressure')
        const windSpeed = document.getElementById('wind-speed')
        const visibility = document.getElementById('visibility')
        const lastUpdated = document.getElementById('last-updated')
        
        // 天气图标映射
        const weatherIcons = {
          'Clear': '☀️',
          'Clouds': '☁️',
          'Rain': '🌧️',
          'Drizzle': '🌦️',
          'Thunderstorm': '⛈️',
          'Snow': '❄️',
          'Mist': '🌫️',
          'Smoke': '🌫️',
          'Haze': '🌫️',
          'Dust': '🌫️',
          'Fog': '🌫️',
          'Sand': '🌫️',
          'Ash': '🌫️',
          'Squall': '💨',
          'Tornado': '🌪️'
        }
        
        // 加载存储的天气数据
        async function loadStoredData() {
          try {
            const storedData = await ipcRenderer.invoke('get-stored-data')
            const selectedCity = citySelect.value
            const cityData = storedData.weatherData[selectedCity]
            
            if (cityData) {
              displayWeather(cityData.data, cityData.timestamp)
            }
          } catch (error) {
            console.error('加载存储数据失败:', error)
          }
        }
        
        // 获取天气数据
        async function getWeather() {
          const city = citySelect.value
          errorDiv.textContent = ''
          
          try {
            const weatherData = await ipcRenderer.invoke('get-weather', city)
            displayWeather(weatherData, new Date().toISOString())
          } catch (error) {
            errorDiv.textContent = `获取天气数据失败: ${error.message}`
            weatherCard.style.display = 'none'
          }
        }
        
        // 显示天气数据
        function displayWeather(data, timestamp) {
          cityName.textContent = `${data.name}, ${data.sys.country}`
          weatherDescription.textContent = data.weather[0].description
          temperature.textContent = `${data.main.temp}°C`
          
          // 设置天气图标
          const weatherMain = data.weather[0].main
          weatherIcon.textContent = weatherIcons[weatherMain] || '☁️'
          
          // 设置详细信息
          humidity.textContent = `${data.main.humidity}%`
          pressure.textContent = `${data.main.pressure} hPa`
          windSpeed.textContent = `${data.wind.speed} m/s`
          visibility.textContent = `${(data.visibility / 1000).toFixed(1)} km`
          
          // 设置最后更新时间
          const date = new Date(timestamp)
          lastUpdated.textContent = `最后更新: ${date.toLocaleString()}`
          
          // 显示天气卡片
          weatherCard.style.display = 'block'
        }
        
        // 事件监听
        refreshBtn.addEventListener('click', getWeather)
        citySelect.addEventListener('change', loadStoredData)
        
        // 初始化加载数据
        loadStoredData()
      </script>
    </body>
    </html>

9.5.3 运行效果

  1. 安装依赖

    bash
    npm install
  2. 运行应用

    bash
    npm start
  3. 测试功能

    • 选择不同城市,查看天气数据
    • 点击 "刷新" 按钮,获取最新天气数据
    • 关闭应用后重新打开,查看是否保留了之前的数据

9.6 小结

通过本章的学习,你已经掌握了 Electron 网络请求和本地存储的核心知识:

  • 渲染进程网络请求:使用 fetch API 或 axios 发送网络请求
  • 主进程网络请求:使用 request 模块或 Node.js 内置模块发送网络请求
  • 跨域问题解决:通过配置 webPreferences 或使用主进程代理解决跨域问题
  • 本地存储方案
    • localStorage:适合存储轻量级数据
    • electron-store:适合存储应用配置和用户数据
    • 数据库存储:适合存储大量结构化数据

这些知识将帮助你创建能够与服务器交互并持久化存储数据的 Electron 应用。结合前面章节所学的内容,你已经具备了开发完整 Electron 应用的能力。

© 2026 编程马·菜鸟教程 版权所有