技术文档中心
首页
React
Vue
TypeScript
Kotlin
React Native
Electron
Android
首页
React
Vue
TypeScript
Kotlin
React Native
Electron
Android
  • 基础入门

    • Electron 学习指南
    • 窗口管理
    • 进程通信
    • 对话框
  • 进阶内容

    • 菜单和托盘
    • 打包发布
    • Electron 自动更新
  • 框架集成

    • React + Electron
    • Vue + Electron

Vue + Electron

项目搭建

使用 Vite 创建项目

# 创建 Vue 项目
yarn create vite my-app --template vue
cd my-app
yarn install

# 安装 Electron
yarn add -D electron electron-builder concurrently wait-on cross-env

项目结构

my-app/
├── electron/
│   ├── main.js           # 主进程
│   └── preload.js        # 预加载脚本
├── src/
│   ├── App.vue
│   ├── main.js
│   └── components/
├── index.html
├── package.json
└── vite.config.js

配置 package.json

{
  "name": "my-app",
  "version": "1.0.0",
  "main": "electron/main.js",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "electron": "wait-on tcp:5173 && cross-env NODE_ENV=development electron .",
    "electron:dev": "concurrently \"yarn dev\" \"yarn electron\"",
    "electron:build": "vite build && electron-builder"
  }
}

配置 vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  base: './',
  server: {
    port: 5173
  },
  build: {
    outDir: 'dist'
  }
})

主进程配置

electron/main.js

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

const isDev = process.env.NODE_ENV === 'development'

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: false,
      contextIsolation: true
    }
  })

  if (isDev) {
    win.loadURL('http://localhost:5173')
    win.webContents.openDevTools()
  } else {
    win.loadFile(path.join(__dirname, '../dist/index.html'))
  }
}

app.whenReady().then(() => {
  createWindow()

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow()
    }
  })
})

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

electron/preload.js

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  getAppVersion: () => ipcRenderer.invoke('get-app-version'),
  openFile: () => ipcRenderer.invoke('open-file'),
  saveFile: (data) => ipcRenderer.invoke('save-file', data),
  showNotification: (title, body) => ipcRenderer.send('show-notification', title, body),
  onUpdateAvailable: (callback) => {
    ipcRenderer.on('update-available', (event, info) => callback(info))
  }
})

Vue 3 组合式 API 集成

创建 Electron Composable

// src/composables/useElectron.js
import { ref, onMounted } from 'vue'

export function useElectron() {
  const isElectron = ref(false)
  const appVersion = ref('')

  onMounted(async () => {
    if (window.electronAPI) {
      isElectron.value = true
      appVersion.value = await window.electronAPI.getAppVersion()
    }
  })

  return {
    isElectron,
    appVersion,
    api: window.electronAPI
  }
}

在组件中使用

<!-- src/App.vue -->
<template>
  <div class="app">
    <header>
      <h1>Vue + Electron 应用</h1>
      <span v-if="isElectron">v{{ appVersion }}</span>
    </header>
    
    <main>
      <FileManager v-if="isElectron" />
      <p v-else>请在 Electron 环境中运行</p>
    </main>
  </div>
</template>

<script setup>
import { useElectron } from './composables/useElectron'
import FileManager from './components/FileManager.vue'

const { isElectron, appVersion } = useElectron()
</script>

实战示例:文件管理器

主进程文件操作

// electron/main.js
const { dialog } = require('electron')
const fs = require('fs').promises
const path = require('path')

ipcMain.handle('open-file', async () => {
  const result = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [
      { name: 'Text Files', extensions: ['txt', 'md', 'json'] },
      { name: 'All Files', extensions: ['*'] }
    ]
  })

  if (result.canceled) {
    return { success: false, canceled: true }
  }

  try {
    const filePath = result.filePaths[0]
    const content = await fs.readFile(filePath, 'utf-8')
    return {
      success: true,
      filePath,
      fileName: path.basename(filePath),
      content
    }
  } catch (error) {
    return { success: false, error: error.message }
  }
})

ipcMain.handle('save-file', async (event, { filePath, content }) => {
  try {
    let saveFilePath = filePath

    if (!saveFilePath) {
      const result = await dialog.showSaveDialog({
        filters: [{ name: 'Text Files', extensions: ['txt', 'md'] }]
      })

      if (result.canceled) {
        return { success: false, canceled: true }
      }

      saveFilePath = result.filePath
    }

    await fs.writeFile(saveFilePath, content, 'utf-8')
    return {
      success: true,
      filePath: saveFilePath,
      fileName: path.basename(saveFilePath)
    }
  } catch (error) {
    return { success: false, error: error.message }
  }
})

Vue 文件管理组件

<!-- src/components/FileManager.vue -->
<template>
  <div class="file-manager">
    <div class="toolbar">
      <button @click="handleOpenFile">打开文件</button>
      <button @click="handleSaveFile" :disabled="!isModified">
        保存 {{ isModified ? '*' : '' }}
      </button>
      <span class="file-name">
        {{ currentFile ? currentFile.name : '未打开文件' }}
      </span>
    </div>

    <textarea
      v-model="content"
      class="editor"
      @input="handleContentChange"
      placeholder="打开文件或开始输入..."
    />

    <div class="status-bar">
      <span>{{ content.length }} 字符</span>
      <span>{{ lineCount }} 行</span>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useElectron } from '../composables/useElectron'

const { api } = useElectron()

const currentFile = ref(null)
const content = ref('')
const isModified = ref(false)

const lineCount = computed(() => {
  return content.value.split('\n').length
})

const handleOpenFile = async () => {
  const result = await api.openFile()
  
  if (result.success) {
    currentFile.value = {
      path: result.filePath,
      name: result.fileName
    }
    content.value = result.content
    isModified.value = false
  } else if (!result.canceled) {
    alert('打开文件失败: ' + result.error)
  }
}

const handleSaveFile = async () => {
  const result = await api.saveFile({
    filePath: currentFile.value?.path,
    content: content.value
  })

  if (result.success) {
    currentFile.value = {
      path: result.filePath,
      name: result.fileName
    }
    isModified.value = false
    api.showNotification('保存成功', '文件已保存')
  } else if (!result.canceled) {
    alert('保存文件失败: ' + result.error)
  }
}

const handleContentChange = () => {
  isModified.value = true
}
</script>

<style scoped>
.file-manager {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.toolbar {
  display: flex;
  gap: 10px;
  padding: 10px;
  background: #f5f5f5;
  border-bottom: 1px solid #ddd;
}

.file-name {
  margin-left: auto;
  font-weight: 500;
}

.editor {
  flex: 1;
  padding: 20px;
  border: none;
  outline: none;
  font-family: 'Monaco', monospace;
  font-size: 14px;
  line-height: 1.6;
  resize: none;
}

.status-bar {
  display: flex;
  gap: 20px;
  padding: 8px 20px;
  background: #f5f5f5;
  border-top: 1px solid #ddd;
  font-size: 12px;
  color: #666;
}
</style>

使用 TypeScript

安装依赖

yarn add -D typescript vue-tsc @types/node

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

类型定义

// src/types/electron.d.ts
export interface ElectronAPI {
  getAppVersion: () => Promise<string>
  openFile: () => Promise<FileResult>
  saveFile: (data: SaveFileData) => Promise<FileResult>
  showNotification: (title: string, body: string) => void
  onUpdateAvailable: (callback: (info: UpdateInfo) => void) => void
}

export interface FileResult {
  success: boolean
  canceled?: boolean
  error?: string
  filePath?: string
  fileName?: string
  content?: string
}

export interface SaveFileData {
  filePath?: string
  content: string
}

declare global {
  interface Window {
    electronAPI: ElectronAPI
  }
}

TypeScript Composable

// src/composables/useElectron.ts
import { ref, onMounted } from 'vue'
import type { Ref } from 'vue'
import type { ElectronAPI } from '../types/electron'

interface UseElectronReturn {
  isElectron: Ref<boolean>
  appVersion: Ref<string>
  api: ElectronAPI | undefined
}

export function useElectron(): UseElectronReturn {
  const isElectron = ref<boolean>(false)
  const appVersion = ref<string>('')

  onMounted(async () => {
    if (window.electronAPI) {
      isElectron.value = true
      appVersion.value = await window.electronAPI.getAppVersion()
    }
  })

  return {
    isElectron,
    appVersion,
    api: window.electronAPI
  }
}

TypeScript 组件

<!-- src/components/FileManager.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { Ref } from 'vue'
import { useElectron } from '../composables/useElectron'

interface FileInfo {
  path: string
  name: string
}

const { api } = useElectron()

const currentFile = ref<FileInfo | null>(null)
const content = ref<string>('')
const isModified = ref<boolean>(false)

const lineCount = computed<number>(() => {
  return content.value.split('\n').length
})

const handleOpenFile = async (): Promise<void> => {
  if (!api) return
  
  const result = await api.openFile()
  
  if (result.success && result.filePath && result.fileName) {
    currentFile.value = {
      path: result.filePath,
      name: result.fileName
    }
    content.value = result.content || ''
    isModified.value = false
  }
}

const handleSaveFile = async (): Promise<void> => {
  if (!api) return
  
  const result = await api.saveFile({
    filePath: currentFile.value?.path,
    content: content.value
  })

  if (result.success && result.filePath && result.fileName) {
    currentFile.value = {
      path: result.filePath,
      name: result.fileName
    }
    isModified.value = false
    api.showNotification('保存成功', '文件已保存')
  }
}
</script>

状态管理集成

使用 Pinia

yarn add pinia
// src/stores/file.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

interface FileInfo {
  path: string
  name: string
}

export const useFileStore = defineStore('file', () => {
  const currentFile = ref<FileInfo | null>(null)
  const content = ref<string>('')
  const isModified = ref<boolean>(false)

  const lineCount = computed(() => {
    return content.value.split('\n').length
  })

  const charCount = computed(() => {
    return content.value.length
  })

  function setCurrentFile(file: FileInfo | null) {
    currentFile.value = file
  }

  function setContent(newContent: string) {
    content.value = newContent
    isModified.value = true
  }

  function markAsSaved() {
    isModified.value = false
  }

  return {
    currentFile,
    content,
    isModified,
    lineCount,
    charCount,
    setCurrentFile,
    setContent,
    markAsSaved
  }
})
// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')
<!-- 在组件中使用 -->
<script setup>
import { useFileStore } from '../stores/file'

const fileStore = useFileStore()

const handleOpenFile = async () => {
  const result = await api.openFile()
  
  if (result.success) {
    fileStore.setCurrentFile({
      path: result.filePath,
      name: result.fileName
    })
    fileStore.setContent(result.content)
    fileStore.markAsSaved()
  }
}
</script>

Vue Router 集成

yarn add vue-router
// src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Editor from '../views/Editor.vue'
import Settings from '../views/Settings.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/editor',
    name: 'Editor',
    component: Editor
  },
  {
    path: '/settings',
    name: 'Settings',
    component: Settings
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App)
  .use(router)
  .mount('#app')

性能优化

1. 异步组件

<script setup>
import { defineAsyncComponent } from 'vue'

const FileManager = defineAsyncComponent(() =>
  import('./components/FileManager.vue')
)
</script>

<template>
  <Suspense>
    <template #default>
      <FileManager />
    </template>
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

2. 虚拟滚动

yarn add vue-virtual-scroller
<template>
  <RecycleScroller
    :items="files"
    :item-size="40"
    key-field="id"
    v-slot="{ item }"
  >
    <div class="file-item">
      {{ item.name }}
    </div>
  </RecycleScroller>
</template>

<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

const files = ref([])
</script>

3. 防抖和节流

// src/composables/useDebounce.js
import { ref, watch } from 'vue'

export function useDebounce(value, delay = 500) {
  const debouncedValue = ref(value.value)
  let timeout

  watch(value, (newValue) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      debouncedValue.value = newValue
    }, delay)
  })

  return debouncedValue
}
<script setup>
import { ref, watch } from 'vue'
import { useDebounce } from '../composables/useDebounce'

const searchText = ref('')
const debouncedSearch = useDebounce(searchText, 500)

watch(debouncedSearch, (value) => {
  // 执行搜索
  console.log('搜索:', value)
})
</script>

打包发布

electron-builder 配置

{
  "build": {
    "appId": "com.example.myapp",
    "productName": "我的应用",
    "directories": {
      "output": "dist-electron"
    },
    "files": [
      "dist/**/*",
      "electron/**/*"
    ],
    "mac": {
      "target": ["dmg"],
      "icon": "build/icon.icns"
    },
    "win": {
      "target": ["nsis"],
      "icon": "build/icon.ico"
    },
    "linux": {
      "target": ["AppImage"],
      "icon": "build/icon.png"
    }
  }
}

构建命令

# 构建
yarn electron:build

# 指定平台
yarn electron:build --mac
yarn electron:build --win
yarn electron:build --linux

调试技巧

Vue Devtools

// electron/main.js
const { session } = require('electron')

app.whenReady().then(async () => {
  if (isDev) {
    try {
      await session.defaultSession.loadExtension(
        '/path/to/vue-devtools'
      )
    } catch (err) {
      console.log('Vue Devtools 加载失败:', err)
    }
  }
  
  createWindow()
})

VSCode 配置

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Electron: Main",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "args": ["."],
      "outputCapture": "std",
      "env": {
        "NODE_ENV": "development"
      }
    }
  ]
}

常见问题

1. 路由模式

Electron 中必须使用 Hash 模式:

import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(), // 使用 Hash 模式
  routes
})

2. 静态资源路径

// vite.config.js
export default defineConfig({
  base: './', // 使用相对路径
})

3. 热更新

yarn add -D electron-reloader
// electron/main.js
if (isDev) {
  try {
    require('electron-reloader')(module)
  } catch {}
}

完整示例项目

查看完整的 Vue + Electron 示例项目:

  • electron-vite-vue
  • vue-cli-plugin-electron-builder
最近更新: 2026/2/24 16:53
Contributors: hailong
Prev
React + Electron