Appearance
第8章:Electron 页面交互与前端集成
8.1 渲染进程与前端框架集成
8.1.1 Vue 项目集成 Electron
Vue 是一个流行的前端框架,与 Electron 的集成非常方便。以下是将 Vue 项目与 Electron 集成的步骤:
8.1.1.1 使用 vue-cli-plugin-electron-builder
创建 Vue 项目
bashvue create my-vue-electron-app cd my-vue-electron-app安装插件
bashvue add electron-builder运行项目
bashnpm run electron:serve打包项目
bashnpm run electron:build
8.1.1.2 项目结构
集成后,项目会生成以下核心文件:
src/background.js:Electron 主进程文件src/main.js:Vue 应用入口文件public/:静态资源src/components/:Vue 组件
8.1.2 React 项目集成 Electron
React 也是一个流行的前端框架,与 Electron 的集成同样简单。
8.1.2.1 使用 create-react-app + electron
创建 React 项目
bashnpx create-react-app my-react-electron-app cd my-react-electron-app安装 Electron
bashnpm install electron --save-dev创建主进程文件 创建
public/electron.js文件:javascriptconst { app, BrowserWindow } = require('electron') const path = require('path') const url = require('url') let mainWindow function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, contextIsolation: false } }) const startUrl = process.env.ELECTRON_START_URL || url.format({ pathname: path.join(__dirname, '../build/index.html'), protocol: 'file:', slashes: true }) mainWindow.loadURL(startUrl) mainWindow.webContents.openDevTools() mainWindow.on('closed', function () { mainWindow = null }) } app.on('ready', createWindow) app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() }) app.on('activate', function () { if (mainWindow === null) createWindow() })修改 package.json
json{ "name": "my-react-electron-app", "main": "public/electron.js", "scripts": { "start": "react-scripts start", "build": "react-scripts build", "electron:dev": "ELECTRON_START_URL=http://localhost:3000 electron .", "electron:build": "npm run build && electron ." } }运行项目
- 首先启动 React 开发服务器:
npm start - 然后启动 Electron:
npm run electron:dev
- 首先启动 React 开发服务器:
8.2 渲染进程调用主进程 API
8.2.1 remote 模块(已废弃)
在 Electron 早期版本中,渲染进程可以通过 remote 模块直接访问主进程的模块和方法。但由于安全问题,该模块已被废弃。
不推荐使用:
javascript
// 已废弃的用法
const { remote } = require('electron')
const { BrowserWindow } = remote
// 创建新窗口
const newWindow = new BrowserWindow({ width: 800, height: 600 })
newWindow.loadURL('https://electronjs.org')8.2.2 推荐方案:使用 IPC 通信
现在推荐使用 IPC 通信来实现渲染进程与主进程的交互:
8.2.2.1 主进程代码
javascript
const { app, BrowserWindow, ipcMain } = require('electron')
// 处理创建新窗口的请求
ipcMain.handle('create-window', (event, url) => {
const newWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
newWindow.loadURL(url)
return '窗口创建成功'
})8.2.2.2 渲染进程代码
javascript
const { ipcRenderer } = require('electron')
// 请求主进程创建新窗口
async function createNewWindow() {
try {
const result = await ipcRenderer.invoke('create-window', 'https://electronjs.org')
console.log(result)
} catch (error) {
console.error('创建窗口失败:', error)
}
}
// 调用函数
createNewWindow()8.2.3 使用预加载脚本
为了提高安全性,推荐使用预加载脚本(preload.js)来暴露主进程的功能给渲染进程:
8.2.3.1 创建预加载脚本
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
// 向渲染进程暴露安全的 API
contextBridge.exposeInMainWorld('electronAPI', {
// 发送消息到主进程
sendMessage: (message) => ipcRenderer.send('message', message),
// 接收主进程的消息
onMessage: (callback) => ipcRenderer.on('message', (event, ...args) => callback(...args)),
// 调用主进程的方法
createWindow: (url) => ipcRenderer.invoke('create-window', url)
})8.2.3.2 在主进程中配置预加载脚本
javascript
const { app, BrowserWindow } = require('electron')
const path = require('path')
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}8.2.3.3 在渲染进程中使用
javascript
// 渲染进程中使用暴露的 API
window.electronAPI.sendMessage('Hello from renderer')
// 监听主进程的消息
window.electronAPI.onMessage((message) => {
console.log('收到消息:', message)
})
// 调用主进程的方法
async function createWindow() {
const result = await window.electronAPI.createWindow('https://electronjs.org')
console.log(result)
}8.3 前端页面与主进程的数据同步
8.3.1 使用 IPC 通信
通过 IPC 通信可以实现渲染进程和主进程之间的数据同步:
8.3.1.1 主进程代码
javascript
const { app, BrowserWindow, ipcMain } = require('electron')
// 存储应用状态
let appState = {
user: null,
settings: {
theme: 'light',
language: 'zh-CN'
}
}
// 处理获取状态的请求
ipcMain.handle('get-app-state', () => {
return appState
})
// 处理更新状态的请求
ipcMain.handle('update-app-state', (event, newState) => {
appState = { ...appState, ...newState }
// 向所有窗口广播状态更新
BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('app-state-updated', appState)
})
return '状态更新成功'
})8.3.1.2 渲染进程代码
javascript
const { ipcRenderer } = require('electron')
// 获取应用状态
async function getAppState() {
try {
const state = await ipcRenderer.invoke('get-app-state')
console.log('应用状态:', state)
return state
} catch (error) {
console.error('获取状态失败:', error)
}
}
// 更新应用状态
async function updateAppState(newState) {
try {
const result = await ipcRenderer.invoke('update-app-state', newState)
console.log(result)
} catch (error) {
console.error('更新状态失败:', error)
}
}
// 监听状态更新
ipcRenderer.on('app-state-updated', (event, state) => {
console.log('状态已更新:', state)
// 更新 UI
updateUI(state)
})
// 初始化获取状态
getAppState()8.3.2 使用 localStorage
对于不需要跨窗口同步的简单数据,可以使用 localStorage 存储:
javascript
// 存储数据
localStorage.setItem('user', JSON.stringify({ name: 'John', email: 'john@example.com' }))
// 获取数据
const user = JSON.parse(localStorage.getItem('user'))
// 删除数据
localStorage.removeItem('user')
// 清空所有数据
localStorage.clear()8.3.3 使用 electron-store
对于需要持久化存储的复杂数据,推荐使用 electron-store 库:
8.3.3.1 安装依赖
bash
npm install electron-store8.3.3.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()8.4 页面路由管理
8.4.1 单页面应用路由
在 Electron 应用中,可以使用前端路由库来实现单页面应用的路由管理,如 Vue Router 或 React Router。
8.4.1.1 Vue Router 示例
安装依赖
bashnpm install vue-router创建路由配置
javascript// router/index.js import Vue from 'vue' import VueRouter from 'vue-router' import Home from '../views/Home.vue' import About from '../views/About.vue' Vue.use(VueRouter) const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/about', name: 'About', component: About } ] const router = new VueRouter({ mode: 'hash', // 使用 hash 模式,避免与文件路径冲突 base: process.env.BASE_URL, routes }) export default router在主应用中使用
javascript// main.js import Vue from 'vue' import App from './App.vue' import router from './router' new Vue({ router, render: h => h(App) }).$mount('#app')创建路由视图
vue<!-- App.vue --> <template> <div id="app"> <nav> <router-link to="/">首页</router-link> <router-link to="/about">关于</router-link> </nav> <router-view/> </div> </template>
8.4.1.2 React Router 示例
安装依赖
bashnpm install react-router-dom创建路由配置
javascript// App.js import React from 'react' import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom' import Home from './Home' import About from './About' function App() { return ( <Router> <div> <nav> <Link to="/">首页</Link> <Link to="/about">关于</Link> </nav> <Switch> <Route path="/about"> <About /> </Route> <Route path="/"> <Home /> </Route> </Switch> </div> </Router> ) } export default App
8.4.2 多窗口路由
对于复杂应用,可以使用多窗口来实现不同功能模块的分离:
javascript
const { app, BrowserWindow } = require('electron')
const path = require('path')
// 主窗口
let mainWindow
// 设置窗口
let settingsWindow
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
mainWindow.loadFile('index.html')
}
function createSettingsWindow() {
settingsWindow = new BrowserWindow({
width: 600,
height: 400,
parent: mainWindow,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
settingsWindow.loadFile('settings.html')
}
// 暴露创建设置窗口的方法
exports.createSettingsWindow = createSettingsWindow8.5 实操案例:Vue 与 Electron 集成
8.5.1 场景描述
创建一个使用 Vue 框架的 Electron 应用,实现以下功能:
- 主窗口显示应用主界面
- 点击按钮打开设置窗口
- 主窗口和设置窗口之间的数据同步
- 使用 Vue Router 管理单页面路由
8.5.2 实现步骤
创建 Vue + Electron 项目
bashvue create vue-electron-app cd vue-electron-app vue add electron-builder安装依赖
bashnpm install vue-router electron-store创建路由配置
javascript// src/router/index.js import Vue from 'vue' import VueRouter from 'vue-router' import Home from '../views/Home.vue' import About from '../views/About.vue' Vue.use(VueRouter) const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/about', name: 'About', component: About } ] const router = new VueRouter({ mode: 'hash', base: process.env.BASE_URL, routes }) export default router创建视图组件
vue<!-- src/views/Home.vue --> <template> <div class="home"> <h1>首页</h1> <p>欢迎使用 Vue + Electron 应用</p> <button @click="openSettings">打开设置</button> <div class="settings-info"> <h2>当前设置</h2> <p>主题:{{ settings.theme }}</p> <p>语言:{{ settings.language }}</p> </div> </div> </template> <script> export default { name: 'Home', data() { return { settings: { theme: 'light', language: 'zh-CN' } } }, mounted() { this.loadSettings() // 监听设置更新 window.electronAPI.onSettingsUpdate((settings) => { this.settings = settings }) }, methods: { openSettings() { window.electronAPI.openSettings() }, async loadSettings() { const settings = await window.electronAPI.getSettings() this.settings = settings } } } </script>vue<!-- src/views/About.vue --> <template> <div class="about"> <h1>关于</h1> <p>这是一个 Vue + Electron 示例应用</p> <p>版本:1.0.0</p> </div> </template> <script> export default { name: 'About' } </script>创建设置窗口
html<!-- public/settings.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>设置</title> <style> body { font-family: Arial, sans-serif; margin: 20px; padding: 0; } .container { max-width: 500px; margin: 0 auto; } h1 { color: #333; } .setting-item { margin: 15px 0; } label { display: block; margin-bottom: 5px; font-weight: bold; } select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } button { padding: 10px 20px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; margin-top: 20px; } button:hover { background-color: #45a049; } </style> </head> <body> <div class="container"> <h1>设置</h1> <div class="setting-item"> <label for="theme">主题</label> <select id="theme"> <option value="light">浅色</option> <option value="dark">深色</option> </select> </div> <div class="setting-item"> <label for="language">语言</label> <select id="language"> <option value="zh-CN">中文</option> <option value="en-US">英文</option> </select> </div> <button id="save-btn">保存设置</button> </div> <script> const { ipcRenderer } = require('electron') // 加载当前设置 async function loadSettings() { const settings = await ipcRenderer.invoke('get-settings') document.getElementById('theme').value = settings.theme document.getElementById('language').value = settings.language } // 保存设置 document.getElementById('save-btn').addEventListener('click', async () => { const settings = { theme: document.getElementById('theme').value, language: document.getElementById('language').value } await ipcRenderer.invoke('save-settings', settings) alert('设置已保存') }) // 初始化加载设置 loadSettings() </script> </body> </html>修改主进程文件
javascript// src/background.js 'use strict' import { app, protocol, BrowserWindow, ipcMain } from 'electron' import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer' import Store from 'electron-store' const isDevelopment = process.env.NODE_ENV !== 'production' // 创建存储实例 const store = new Store({ name: 'app-settings', defaults: { theme: 'light', language: 'zh-CN' } }) // 全局变量 let win let settingsWindow // Scheme must be registered before the app is ready protocol.registerSchemesAsPrivileged([ { scheme: 'app', privileges: { secure: true, standard: true } } ]) function createWindow() { // Create the browser window. win = new BrowserWindow({ width: 800, height: 600, webPreferences: { // Use pluginOptions.nodeIntegration, leave this alone // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION, contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION, preload: path.join(__dirname, 'preload.js') } }) if (process.env.WEBPACK_DEV_SERVER_URL) { // Load the url of the dev server if in development mode win.loadURL(process.env.WEBPACK_DEV_SERVER_URL) if (!process.env.IS_TEST) win.webContents.openDevTools() } else { createProtocol('app') // Load the index.html when not in development win.loadURL('app://./index.html') } win.on('closed', () => { win = null }) } function createSettingsWindow() { settingsWindow = new BrowserWindow({ width: 600, height: 400, parent: win, webPreferences: { nodeIntegration: true, contextIsolation: false } }) settingsWindow.loadFile('public/settings.html') settingsWindow.on('closed', () => { settingsWindow = null }) } // Quit when all windows are closed. app.on('window-all-closed', () => { // On macOS it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', () => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (win === null) { createWindow() } }) // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.on('ready', async () => { if (isDevelopment && !process.env.IS_TEST) { // Install Vue Devtools try { await installExtension(VUEJS_DEVTOOLS) } catch (e) { console.error('Vue Devtools failed to install:', e.toString()) } } createWindow() }) // Exit cleanly on request from parent process in development mode. if (isDevelopment) { if (process.platform === 'win32') { process.on('message', (data) => { if (data === 'graceful-exit') { app.quit() } }) } else { process.on('SIGTERM', () => { app.quit() }) } } // IPC 事件处理 ipcMain.handle('get-settings', () => { return store.store }) ipcMain.handle('save-settings', (event, settings) => { store.set(settings) // 通知主窗口设置已更新 if (win) { win.webContents.send('settings-updated', store.store) } return '设置已保存' }) ipcMain.handle('open-settings', () => { createSettingsWindow() return '设置窗口已打开' })创建预加载脚本
javascript// src/preload.js const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electronAPI', { getSettings: () => ipcRenderer.invoke('get-settings'), openSettings: () => ipcRenderer.invoke('open-settings'), onSettingsUpdate: (callback) => ipcRenderer.on('settings-updated', (event, settings) => callback(settings)) })修改主应用文件
vue<!-- src/App.vue --> <template> <div id="app"> <nav> <router-link to="/">首页</router-link> <router-link to="/about">关于</router-link> </nav> <router-view/> </div> </template> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } nav { padding: 30px; background-color: #f8f8f8; margin-bottom: 20px; } nav a { margin: 0 10px; color: #4CAF50; text-decoration: none; } nav a:hover { text-decoration: underline; } </style>修改 main.js
javascript// src/main.js import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false new Vue({ router, render: h => h(App) }).$mount('#app')
8.5.3 运行效果
启动开发服务器
bashnpm run electron:serve测试功能
- 在首页点击 "打开设置" 按钮,打开设置窗口
- 在设置窗口修改主题和语言设置,点击 "保存设置"
- 观察主窗口中的设置信息是否更新
- 点击导航链接,测试页面路由功能
8.6 小结
通过本章的学习,你已经掌握了 Electron 与前端框架集成的核心知识:
- Vue 项目集成:使用 vue-cli-plugin-electron-builder 快速集成
- React 项目集成:使用 create-react-app + electron 集成
- 渲染进程调用主进程 API:使用 IPC 通信替代已废弃的 remote 模块
- 数据同步:通过 IPC 通信和 electron-store 实现数据持久化
- 页面路由管理:使用前端路由库实现单页面应用路由
- 多窗口管理:创建和管理多个应用窗口
这些知识将帮助你创建更加复杂和功能丰富的 Electron 应用,结合前端框架的优势,提供更好的用户体验。在接下来的章节中,我们将学习 Electron 的网络请求和本地存储功能。
