Appearance
第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
安装依赖
bashnpm install axios使用示例
javascriptconst 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 模块
安装依赖
bashnpm install request使用示例
javascriptconst 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 使用代理解决跨域
另一种更安全的方法是使用主进程作为代理,处理跨域请求:
主进程代码
javascriptconst { 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 } })渲染进程代码
javascriptconst { 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 设计的持久化存储库,支持主进程和渲染进程使用:
安装依赖
bashnpm install electron-store使用示例
javascriptconst 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
安装依赖
bashnpm install sqlite3使用示例
javascriptconst 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
安装依赖
bashnpm install mysql2使用示例
javascriptconst 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 实现步骤
创建项目结构
weather-app/ ├── main.js ├── index.html ├── package.json └── assets/ └── icon.png修改 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" } }修改 main.js
javascriptconst { 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() })修改 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 运行效果
安装依赖
bashnpm install运行应用
bashnpm start测试功能
- 选择不同城市,查看天气数据
- 点击 "刷新" 按钮,获取最新天气数据
- 关闭应用后重新打开,查看是否保留了之前的数据
9.6 小结
通过本章的学习,你已经掌握了 Electron 网络请求和本地存储的核心知识:
- 渲染进程网络请求:使用 fetch API 或 axios 发送网络请求
- 主进程网络请求:使用 request 模块或 Node.js 内置模块发送网络请求
- 跨域问题解决:通过配置 webPreferences 或使用主进程代理解决跨域问题
- 本地存储方案:
- localStorage:适合存储轻量级数据
- electron-store:适合存储应用配置和用户数据
- 数据库存储:适合存储大量结构化数据
这些知识将帮助你创建能够与服务器交互并持久化存储数据的 Electron 应用。结合前面章节所学的内容,你已经具备了开发完整 Electron 应用的能力。
