Skip to content

第12章:进阶实战

实战4:简易浏览器

12.1 需求分析

创建一个简易的桌面浏览器应用,实现以下功能:

  • 地址栏输入和导航
  • 页面加载和显示
  • 前进/后退/刷新按钮
  • 窗口控制(最小化、最大化、关闭)
  • 页面加载状态显示
  • 错误提示

12.2 核心实现

  • BrowserWindow:创建应用窗口,设置窗口属性
  • webContents:控制网页加载和导航
  • 菜单/工具栏:创建应用菜单和工具栏
  • IPC通信:主进程与渲染进程之间的通信
  • dialog模块:显示错误和警告对话框

12.3 实现步骤

  1. 创建项目结构

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

    json
    {
      "name": "browser-app",
      "version": "1.0.0",
      "description": "简易浏览器",
      "main": "main.js",
      "scripts": {
        "start": "electron ."
      },
      "devDependencies": {
        "electron": "^18.0.0"
      }
    }
  3. 修改 main.js

    javascript
    const { app, BrowserWindow, Menu, ipcMain, dialog } = require('electron')
    const path = require('path')
    
    let mainWindow
    
    function createWindow() {
      mainWindow = new BrowserWindow({
        width: 1000,
        height: 700,
        webPreferences: {
          nodeIntegration: true,
          contextIsolation: false
        }
      })
    
      mainWindow.loadFile('index.html')
      mainWindow.webContents.openDevTools()
    
      // 创建菜单
      createMenu()
    
      // 监听页面加载状态
      mainWindow.webContents.on('did-start-loading', () => {
        mainWindow.webContents.send('loading-started')
      })
    
      mainWindow.webContents.on('did-stop-loading', () => {
        mainWindow.webContents.send('loading-stopped')
      })
    
      mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
        mainWindow.webContents.send('load-failed', errorCode, errorDescription)
      })
    
      mainWindow.webContents.on('did-navigate', (event, url) => {
        mainWindow.webContents.send('url-changed', url)
      })
    
      mainWindow.on('closed', () => {
        mainWindow = null
      })
    }
    
    function createMenu() {
      const template = [
        {
          label: '文件',
          submenu: [
            {
              label: '新建窗口',
              accelerator: 'CmdOrCtrl+N',
              click: () => createWindow()
            },
            { type: 'separator' },
            {
              label: '退出',
              accelerator: 'CmdOrCtrl+Q',
              click: () => app.quit()
            }
          ]
        },
        {
          label: '编辑',
          submenu: [
            { role: 'undo' },
            { role: 'redo' },
            { type: 'separator' },
            { role: 'cut' },
            { role: 'copy' },
            { role: 'paste' },
            { role: 'selectall' }
          ]
        },
        {
          label: '视图',
          submenu: [
            { role: 'reload' },
            { role: 'forcereload' },
            { role: 'toggledevtools' },
            { type: 'separator' },
            { role: 'resetzoom' },
            { role: 'zoomin' },
            { role: 'zoomout' },
            { type: 'separator' },
            { role: 'togglefullscreen' }
          ]
        },
        {
          label: '历史',
          submenu: [
            { role: 'back' },
            { role: 'forward' }
          ]
        }
      ]
    
      const menu = Menu.buildFromTemplate(template)
      Menu.setApplicationMenu(menu)
    }
    
    // IPC 事件处理
    ipcMain.handle('navigate', (event, url) => {
      if (!url.startsWith('http://') && !url.startsWith('https://')) {
        url = 'http://' + url
      }
      mainWindow.loadURL(url)
      return url
    })
    
    ipcMain.handle('go-back', () => {
      if (mainWindow.webContents.canGoBack()) {
        mainWindow.webContents.goBack()
        return true
      }
      return false
    })
    
    ipcMain.handle('go-forward', () => {
      if (mainWindow.webContents.canGoForward()) {
        mainWindow.webContents.goForward()
        return true
      }
      return false
    })
    
    ipcMain.handle('reload', () => {
      mainWindow.webContents.reload()
      return true
    })
    
    ipcMain.handle('stop-loading', () => {
      mainWindow.webContents.stop()
      return true
    })
    
    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 {
          margin: 0;
          padding: 0;
          font-family: Arial, sans-serif;
          height: 100vh;
          display: flex;
          flex-direction: column;
        }
        .toolbar {
          background-color: #f0f0f0;
          padding: 10px;
          display: flex;
          align-items: center;
          border-bottom: 1px solid #ddd;
        }
        .nav-buttons {
          display: flex;
          margin-right: 10px;
        }
        .nav-button {
          width: 30px;
          height: 30px;
          border: none;
          background-color: #e0e0e0;
          margin-right: 5px;
          cursor: pointer;
          border-radius: 4px;
        }
        .nav-button:hover {
          background-color: #d0d0d0;
        }
        .nav-button:disabled {
          opacity: 0.5;
          cursor: not-allowed;
        }
        .url-bar {
          flex: 1;
          display: flex;
          align-items: center;
        }
        #url-input {
          flex: 1;
          height: 30px;
          padding: 0 10px;
          border: 1px solid #ddd;
          border-radius: 4px;
          font-size: 14px;
        }
        #go-button {
          margin-left: 5px;
          height: 30px;
          padding: 0 15px;
          background-color: #4CAF50;
          color: white;
          border: none;
          border-radius: 4px;
          cursor: pointer;
        }
        #go-button:hover {
          background-color: #45a049;
        }
        .loading-indicator {
          margin-left: 10px;
          font-size: 12px;
          color: #666;
        }
        #webview-container {
          flex: 1;
          position: relative;
          overflow: hidden;
        }
        #webview {
          width: 100%;
          height: 100%;
          border: none;
        }
        .error-message {
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          background-color: white;
          padding: 20px;
          border: 1px solid #ddd;
          border-radius: 4px;
          box-shadow: 0 2px 4px rgba(0,0,0,0.1);
          text-align: center;
        }
        .error-message h3 {
          color: #f44336;
          margin-top: 0;
        }
        .error-message button {
          margin-top: 15px;
          padding: 8px 15px;
          background-color: #4CAF50;
          color: white;
          border: none;
          border-radius: 4px;
          cursor: pointer;
        }
        .error-message button:hover {
          background-color: #45a049;
        }
      </style>
    </head>
    <body>
      <div class="toolbar">
        <div class="nav-buttons">
          <button class="nav-button" id="back-button" disabled>
    
          </button>
          <button class="nav-button" id="forward-button" disabled>
    
          </button>
          <button class="nav-button" id="reload-button">
    
          </button>
          <button class="nav-button" id="stop-button" style="display: none">
    
          </button>
        </div>
        <div class="url-bar">
          <input type="text" id="url-input" placeholder="输入网址..." value="https://www.google.com">
          <button id="go-button">前往</button>
        </div>
        <div class="loading-indicator" id="loading-indicator" style="display: none">
          加载中...
        </div>
      </div>
      <div id="webview-container">
        <iframe id="webview" src="https://www.google.com"></iframe>
        <div class="error-message" id="error-message" style="display: none">
          <h3>加载失败</h3>
          <div id="error-details"></div>
          <button id="retry-button">重试</button>
        </div>
      </div>
      
      <script>
        const { ipcRenderer } = require('electron')
        const urlInput = document.getElementById('url-input')
        const goButton = document.getElementById('go-button')
        const backButton = document.getElementById('back-button')
        const forwardButton = document.getElementById('forward-button')
        const reloadButton = document.getElementById('reload-button')
        const stopButton = document.getElementById('stop-button')
        const loadingIndicator = document.getElementById('loading-indicator')
        const webview = document.getElementById('webview')
        const errorMessage = document.getElementById('error-message')
        const errorDetails = document.getElementById('error-details')
        const retryButton = document.getElementById('retry-button')
    
        // 导航到指定 URL
        async function navigate() {
          const url = urlInput.value
          if (!url) return
          
          try {
            const newUrl = await ipcRenderer.invoke('navigate', url)
            urlInput.value = newUrl
          } catch (error) {
            console.error('导航失败:', error)
          }
        }
    
        // 后退
        async function goBack() {
          const success = await ipcRenderer.invoke('go-back')
          if (success) {
            // 导航成功后,webContents 的 did-navigate 事件会更新 URL
          }
        }
    
        // 前进
        async function goForward() {
          const success = await ipcRenderer.invoke('go-forward')
          if (success) {
            // 导航成功后,webContents 的 did-navigate 事件会更新 URL
          }
        }
    
        // 刷新
        async function reload() {
          await ipcRenderer.invoke('reload')
        }
    
        // 停止加载
        async function stopLoading() {
          await ipcRenderer.invoke('stop-loading')
        }
    
        // 事件监听
        goButton.addEventListener('click', navigate)
        urlInput.addEventListener('keypress', (e) => {
          if (e.key === 'Enter') {
            navigate()
          }
        })
        backButton.addEventListener('click', goBack)
        forwardButton.addEventListener('click', goForward)
        reloadButton.addEventListener('click', reload)
        stopButton.addEventListener('click', stopLoading)
        retryButton.addEventListener('click', navigate)
    
        // 监听主进程消息
        ipcRenderer.on('loading-started', () => {
          loadingIndicator.style.display = 'block'
          stopButton.style.display = 'block'
          reloadButton.style.display = 'none'
          errorMessage.style.display = 'none'
        })
    
        ipcRenderer.on('loading-stopped', () => {
          loadingIndicator.style.display = 'none'
          stopButton.style.display = 'none'
          reloadButton.style.display = 'block'
        })
    
        ipcRenderer.on('load-failed', (event, errorCode, errorDescription) => {
          loadingIndicator.style.display = 'none'
          stopButton.style.display = 'none'
          reloadButton.style.display = 'block'
          errorDetails.textContent = `错误代码: ${errorCode}\n错误描述: ${errorDescription}`
          errorMessage.style.display = 'block'
        })
    
        ipcRenderer.on('url-changed', (event, url) => {
          urlInput.value = url
        })
      </script>
    </body>
    </html>
  5. 运行应用

    bash
    npm install
    npm start

实战5:Electron + Vue 桌面应用

12.4 需求分析

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

  • 用 Vue 编写页面
  • 实现数据展示和用户交互
  • 本地存储用户数据
  • 主进程与渲染进程通信
  • 响应式设计,适配不同窗口大小

12.5 核心实现

  • Vue项目集成Electron:使用 vue-cli-plugin-electron-builder 集成
  • IPC通信:主进程与渲染进程之间的通信
  • electron-store:本地存储用户数据
  • Vue组件:构建用户界面
  • Vuex:状态管理

12.6 实现步骤

  1. 创建 Vue + Electron 项目

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

    bash
    npm install electron-store vuex
  3. 创建 Vuex 存储

    javascript
    // src/store/index.js
    import { createStore } from 'vuex'
    
    export default createStore({
      state: {
        user: null,
        settings: {
          theme: 'light',
          language: 'zh-CN'
        },
        todos: []
      },
      mutations: {
        setUser(state, user) {
          state.user = user
        },
        setSettings(state, settings) {
          state.settings = { ...state.settings, ...settings }
        },
        addTodo(state, todo) {
          state.todos.push(todo)
        },
        toggleTodo(state, id) {
          const todo = state.todos.find(t => t.id === id)
          if (todo) {
            todo.completed = !todo.completed
          }
        },
        deleteTodo(state, id) {
          state.todos = state.todos.filter(t => t.id !== id)
        }
      },
      actions: {
        saveUser({ commit }, user) {
          commit('setUser', user)
          // 可以在这里添加保存到本地存储的逻辑
        },
        updateSettings({ commit }, settings) {
          commit('setSettings', settings)
          // 可以在这里添加保存到本地存储的逻辑
        },
        addTodo({ commit }, todo) {
          commit('addTodo', { ...todo, id: Date.now() })
          // 可以在这里添加保存到本地存储的逻辑
        },
        toggleTodo({ commit }, id) {
          commit('toggleTodo', id)
          // 可以在这里添加保存到本地存储的逻辑
        },
        deleteTodo({ commit }, id) {
          commit('deleteTodo', id)
          // 可以在这里添加保存到本地存储的逻辑
        }
      },
      getters: {
        completedTodos(state) {
          return state.todos.filter(t => t.completed)
        },
        pendingTodos(state) {
          return state.todos.filter(t => !t.completed)
        }
      }
    })
  4. 修改主进程文件

    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: 'vue-electron-app',
      defaults: {
        user: null,
        settings: {
          theme: 'light',
          language: 'zh-CN'
        },
        todos: []
      }
    })
    
    // Scheme must be registered before the app is ready
    protocol.registerSchemesAsPrivileged([
      { scheme: 'app', privileges: { secure: true, standard: true } }
    ])
    
    async function createWindow() {
      // Create the browser window.
      const 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
        }
      })
    
      if (process.env.WEBPACK_DEV_SERVER_URL) {
        // Load the url of the dev server if in development mode
        await 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')
      }
    }
    
    // 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 (BrowserWindow.getAllWindows().length === 0) 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-stored-data', () => {
      return store.store
    })
    
    ipcMain.handle('save-stored-data', (event, data) => {
      store.set(data)
      return '数据保存成功'
    })
  5. 创建 Vue 组件

    vue
    <!-- src/components/TodoList.vue -->
    <template>
      <div class="todo-list">
        <h2>待办事项</h2>
        <div class="todo-input">
          <input 
            type="text" 
            v-model="newTodo" 
            placeholder="输入新的待办事项"
            @keyup.enter="addTodo"
          >
          <button @click="addTodo">添加</button>
        </div>
        <ul class="todo-items">
          <li v-for="todo in todos" :key="todo.id" :class="{ completed: todo.completed }">
            <input type="checkbox" v-model="todo.completed" @change="toggleTodo(todo.id)">
            <span>{{ todo.text }}</span>
            <button @click="deleteTodo(todo.id)">删除</button>
          </li>
        </ul>
        <div class="todo-stats">
          <span>已完成: {{ completedCount }}</span>
          <span>待完成: {{ pendingCount }}</span>
        </div>
      </div>
    </template>
    
    <script>
    import { mapState, mapActions, mapGetters } from 'vuex'
    
    export default {
      name: 'TodoList',
      data() {
        return {
          newTodo: ''
        }
      },
      computed: {
        ...mapState(['todos']),
        ...mapGetters(['completedTodos', 'pendingTodos']),
        completedCount() {
          return this.completedTodos.length
        },
        pendingCount() {
          return this.pendingTodos.length
        }
      },
      methods: {
        ...mapActions(['addTodo', 'toggleTodo', 'deleteTodo']),
        addTodo() {
          if (this.newTodo.trim()) {
            this.addTodo({ text: this.newTodo, completed: false })
            this.newTodo = ''
          }
        }
      }
    }
    </script>
    
    <style scoped>
    .todo-list {
      background-color: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    h2 {
      margin-top: 0;
      color: #333;
    }
    .todo-input {
      display: flex;
      margin-bottom: 20px;
    }
    .todo-input input {
      flex: 1;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px 0 0 4px;
    }
    .todo-input button {
      padding: 10px 15px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 0 4px 4px 0;
      cursor: pointer;
    }
    .todo-input button:hover {
      background-color: #45a049;
    }
    .todo-items {
      list-style: none;
      padding: 0;
      margin: 0 0 20px 0;
    }
    .todo-items li {
      display: flex;
      align-items: center;
      padding: 10px;
      border-bottom: 1px solid #eee;
    }
    .todo-items li.completed {
      text-decoration: line-through;
      color: #999;
    }
    .todo-items li input {
      margin-right: 10px;
    }
    .todo-items li span {
      flex: 1;
    }
    .todo-items li button {
      background-color: #f44336;
      color: white;
      border: none;
      border-radius: 4px;
      padding: 5px 10px;
      cursor: pointer;
    }
    .todo-items li button:hover {
      background-color: #d32f2f;
    }
    .todo-stats {
      display: flex;
      justify-content: space-between;
      font-size: 14px;
      color: #666;
      padding-top: 10px;
      border-top: 1px solid #eee;
    }
    </style>
    vue
    <!-- src/components/Settings.vue -->
    <template>
      <div class="settings">
        <h2>设置</h2>
        <div class="setting-item">
          <label for="theme">主题</label>
          <select id="theme" v-model="localSettings.theme" @change="updateSettings">
            <option value="light">浅色</option>
            <option value="dark">深色</option>
          </select>
        </div>
        <div class="setting-item">
          <label for="language">语言</label>
          <select id="language" v-model="localSettings.language" @change="updateSettings">
            <option value="zh-CN">中文</option>
            <option value="en-US">英文</option>
          </select>
        </div>
        <div class="setting-item">
          <label for="username">用户名</label>
          <input type="text" id="username" v-model="localUser.name" @change="updateUser">
        </div>
        <div class="setting-item">
          <label for="email">邮箱</label>
          <input type="email" id="email" v-model="localUser.email" @change="updateUser">
        </div>
      </div>
    </template>
    
    <script>
    import { mapState, mapActions } from 'vuex'
    
    export default {
      name: 'Settings',
      data() {
        return {
          localSettings: {
            theme: 'light',
            language: 'zh-CN'
          },
          localUser: {
            name: '',
            email: ''
          }
        }
      },
      computed: {
        ...mapState(['settings', 'user'])
      },
      watch: {
        settings: {
          handler(newSettings) {
            this.localSettings = { ...newSettings }
          },
          deep: true
        },
        user: {
          handler(newUser) {
            this.localUser = { ...newUser } || { name: '', email: '' }
          },
          deep: true
        }
      },
      mounted() {
        this.localSettings = { ...this.settings }
        this.localUser = { ...this.user } || { name: '', email: '' }
      },
      methods: {
        ...mapActions(['updateSettings', 'saveUser']),
        updateSettings() {
          this.updateSettings(this.localSettings)
        },
        updateUser() {
          this.saveUser(this.localUser)
        }
      }
    }
    </script>
    
    <style scoped>
    .settings {
      background-color: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    h2 {
      margin-top: 0;
      color: #333;
    }
    .setting-item {
      margin-bottom: 15px;
    }
    label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }
    input, select {
      width: 100%;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      box-sizing: border-box;
    }
    </style>
  6. 修改 App.vue

    vue
    <template>
      <div id="app" :class="{ 'dark-theme': isDarkTheme }">
        <nav>
          <div class="logo">Vue + Electron App</div>
          <div class="nav-links">
            <a href="#" @click.prevent="activeView = 'todo'">待办事项</a>
            <a href="#" @click.prevent="activeView = 'settings'">设置</a>
          </div>
        </nav>
        <main>
          <TodoList v-if="activeView === 'todo'" />
          <Settings v-if="activeView === 'settings'" />
        </main>
        <footer>
          <p>© {{ new Date().getFullYear() }} Vue + Electron App</p>
        </footer>
      </div>
    </template>
    
    <script>
    import TodoList from './components/TodoList.vue'
    import Settings from './components/Settings.vue'
    import { mapState } from 'vuex'
    import { ipcRenderer } from 'electron'
    
    export default {
      name: 'App',
      components: {
        TodoList,
        Settings
      },
      data() {
        return {
          activeView: 'todo'
        }
      },
      computed: {
        ...mapState(['settings']),
        isDarkTheme() {
          return this.settings.theme === 'dark'
        }
      },
      async mounted() {
        // 从本地存储加载数据
        const storedData = await ipcRenderer.invoke('get-stored-data')
        if (storedData) {
          // 这里可以使用 Vuex actions 来更新状态
          console.log('加载存储的数据:', storedData)
        }
      },
      beforeUnmount() {
        // 保存数据到本地存储
        // 这里可以使用 Vuex actions 来保存状态
      }
    }
    </script>
    
    <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: Arial, sans-serif;
      background-color: #f5f5f5;
      color: #333;
    }
    
    #app {
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      transition: background-color 0.3s, color 0.3s;
    }
    
    #app.dark-theme {
      background-color: #333;
      color: #f5f5f5;
    }
    
    nav {
      background-color: #4CAF50;
      color: white;
      padding: 15px 20px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    .logo {
      font-size: 18px;
      font-weight: bold;
    }
    
    .nav-links a {
      color: white;
      text-decoration: none;
      margin-left: 20px;
    }
    
    .nav-links a:hover {
      text-decoration: underline;
    }
    
    main {
      flex: 1;
      padding: 20px;
      max-width: 800px;
      margin: 0 auto;
      width: 100%;
    }
    
    footer {
      background-color: #333;
      color: white;
      text-align: center;
      padding: 15px;
      margin-top: auto;
    }
    
    #app.dark-theme footer {
      background-color: #111;
    }
    </style>
  7. 修改 main.js

    javascript
    // src/main.js
    import { createApp } from 'vue'
    import App from './App.vue'
    import store from './store'
    
    createApp(App).use(store).mount('#app')
  8. 运行应用

    bash
    npm run electron:serve

12.7 小结

通过本章的两个进阶实战案例,你已经掌握了更复杂的 Electron 应用开发技能:

  • 简易浏览器:学习了如何使用 BrowserWindow 和 webContents 控制网页加载和导航,如何创建应用菜单和工具栏,以及如何处理页面加载状态和错误。

  • Electron + Vue 桌面应用:学习了如何将 Vue 框架与 Electron 集成,如何使用 Vuex 进行状态管理,如何使用 electron-store 存储用户数据,以及如何构建响应式的用户界面。

这些进阶实战案例涵盖了更复杂的应用场景,帮助你进一步巩固和扩展了 Electron 开发的知识和技能。在接下来的章节中,我们将学习 Electron 应用的打包发布和进阶提升。

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