Skip to content

第8章:Electron 页面交互与前端集成

8.1 渲染进程与前端框架集成

8.1.1 Vue 项目集成 Electron

Vue 是一个流行的前端框架,与 Electron 的集成非常方便。以下是将 Vue 项目与 Electron 集成的步骤:

8.1.1.1 使用 vue-cli-plugin-electron-builder

  1. 创建 Vue 项目

    bash
    vue create my-vue-electron-app
    cd my-vue-electron-app
  2. 安装插件

    bash
    vue add electron-builder
  3. 运行项目

    bash
    npm run electron:serve
  4. 打包项目

    bash
    npm 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

  1. 创建 React 项目

    bash
    npx create-react-app my-react-electron-app
    cd my-react-electron-app
  2. 安装 Electron

    bash
    npm install electron --save-dev
  3. 创建主进程文件 创建 public/electron.js 文件:

    javascript
    const { 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()
    })
  4. 修改 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 ."
      }
    }
  5. 运行项目

    • 首先启动 React 开发服务器:npm start
    • 然后启动 Electron:npm run electron:dev

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-store

8.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 示例

  1. 安装依赖

    bash
    npm install vue-router
  2. 创建路由配置

    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
  3. 在主应用中使用

    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')
  4. 创建路由视图

    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 示例

  1. 安装依赖

    bash
    npm install react-router-dom
  2. 创建路由配置

    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 = createSettingsWindow

8.5 实操案例:Vue 与 Electron 集成

8.5.1 场景描述

创建一个使用 Vue 框架的 Electron 应用,实现以下功能:

  • 主窗口显示应用主界面
  • 点击按钮打开设置窗口
  • 主窗口和设置窗口之间的数据同步
  • 使用 Vue Router 管理单页面路由

8.5.2 实现步骤

  1. 创建 Vue + Electron 项目

    bash
    vue create vue-electron-app
    cd vue-electron-app
    vue add electron-builder
  2. 安装依赖

    bash
    npm install vue-router electron-store
  3. 创建路由配置

    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
  4. 创建视图组件

    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>
  5. 创建设置窗口

    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>
  6. 修改主进程文件

    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 '设置窗口已打开'
    })
  7. 创建预加载脚本

    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))
    })
  8. 修改主应用文件

    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>
  9. 修改 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 运行效果

  1. 启动开发服务器

    bash
    npm run electron:serve
  2. 测试功能

    • 在首页点击 "打开设置" 按钮,打开设置窗口
    • 在设置窗口修改主题和语言设置,点击 "保存设置"
    • 观察主窗口中的设置信息是否更新
    • 点击导航链接,测试页面路由功能

8.6 小结

通过本章的学习,你已经掌握了 Electron 与前端框架集成的核心知识:

  • Vue 项目集成:使用 vue-cli-plugin-electron-builder 快速集成
  • React 项目集成:使用 create-react-app + electron 集成
  • 渲染进程调用主进程 API:使用 IPC 通信替代已废弃的 remote 模块
  • 数据同步:通过 IPC 通信和 electron-store 实现数据持久化
  • 页面路由管理:使用前端路由库实现单页面应用路由
  • 多窗口管理:创建和管理多个应用窗口

这些知识将帮助你创建更加复杂和功能丰富的 Electron 应用,结合前端框架的优势,提供更好的用户体验。在接下来的章节中,我们将学习 Electron 的网络请求和本地存储功能。

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