Vue + Electron 桌面应用开发
【摘要】 一、引言1.1 Electron + Vue 的重要性Electron + Vue 组合是现代桌面应用开发的主流技术栈,通过Web技术构建跨平台桌面应用,实现一次开发,多端部署。在桌面应用市场年增长20%的背景下,该技术栈凭借开发效率高、生态丰富、性能优秀的特点,成为企业级桌面应用的首选方案。1.2 技术价值与市场分析class ElectronVueAnalysis { /** 桌...
一、引言
1.1 Electron + Vue 的重要性
1.2 技术价值与市场分析
class ElectronVueAnalysis {
/** 桌面应用市场分析 */
static getMarketAnalysis() {
return {
'市场规模': '2025年全球桌面应用市场将达3000亿美元',
'技术占比': 'Electron在跨平台桌面应用中占比65%',
'开发效率': '相比原生开发,效率提升3-5倍',
'人才需求': 'Electron+Vue开发者年需求增长40%',
'企业采用': '微软、Slack、Discord等知名公司广泛使用'
};
}
/** 技术方案对比 */
static getTechnologyComparison() {
return {
'Electron vs 原生开发': {
'开发成本': '⭐⭐⭐⭐⭐ vs ⭐⭐',
'跨平台能力': '⭐⭐⭐⭐⭐ vs ⭐',
'性能表现': '⭐⭐⭐ vs ⭐⭐⭐⭐⭐',
'开发速度': '⭐⭐⭐⭐⭐ vs ⭐⭐',
'用户体验': '⭐⭐⭐⭐ vs ⭐⭐⭐⭐⭐'
},
'Electron vs Flutter Desktop': {
'生态成熟度': '⭐⭐⭐⭐⭐ vs ⭐⭐⭐',
'Web技术复用': '⭐⭐⭐⭐⭐ vs ⭐',
'社区支持': '⭐⭐⭐⭐⭐ vs ⭐⭐⭐',
'包体积': '⭐⭐ vs ⭐⭐⭐⭐',
'热更新': '⭐⭐⭐⭐⭐ vs ⭐⭐⭐'
},
'Vue vs React (Electron)': {
'学习曲线': '⭐⭐⭐⭐⭐ vs ⭐⭐⭐',
'开发体验': '⭐⭐⭐⭐⭐ vs ⭐⭐⭐⭐',
'性能优化': '⭐⭐⭐⭐ vs ⭐⭐⭐⭐',
'TypeScript支持': '⭐⭐⭐⭐ vs ⭐⭐⭐⭐⭐',
'移动端兼容': '⭐⭐⭐⭐ vs ⭐⭐⭐'
}
};
}
/** 业务价值分析 */
static getBusinessValue() {
return {
'开发成本': '相比原生开发,成本降低60-70%',
'上线速度': '从需求到上线时间缩短50%',
'维护效率': '统一技术栈,维护成本降低40%',
'人才储备': 'Web开发者可快速转型,招聘容易',
'生态优势': '可复用Web生态大量组件和工具'
};
}
}
1.3 性能与体验基准
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
二、技术背景
2.1 Electron + Vue 架构原理
graph TB
A[Electron + Vue 应用架构] --> B[主进程 Main Process]
A --> C[渲染进程 Renderer Process]
A --> D[预加载脚本 Preload Scripts]
B --> B1[应用生命周期管理]
B --> B2[窗口管理]
B --> B3[原生API调用]
B --> B4[系统集成]
C --> C1[Vue应用]
C --> C2[UI渲染]
C --> C3[用户交互]
C --> C4[业务逻辑]
D --> D1[安全通信桥接]
D --> D2[API暴露]
D --> D3[上下文隔离]
D --> D4[权限控制]
B1 --> E[应用窗口]
C1 --> E
D1 --> E
E --> F[桌面应用程序]
B --> G[操作系统API]
C --> H[DOM/BOM API]
D --> I[IPC通信]
G --> J[系统功能调用]
H --> J
I --> J
2.2 核心技术栈
// 技术栈配置
export const ElectronVueTechStack = {
// 核心框架
electron: {
version: '^28.0.0',
features: [
'Chromium内核', 'Node.js运行时', '原生API访问',
'自动更新', '崩溃报告', '原生菜单', '系统托盘'
]
},
vue: {
version: '^3.3.0',
features: [
'组合式API', '响应式系统', '单文件组件',
'Vue Router', 'Pinia状态管理', 'Vite构建'
]
},
// 开发工具链
tooling: {
'构建工具': 'Vite | Webpack',
'包管理器': 'npm | yarn | pnpm',
'TypeScript': '类型安全',
'ESLint/Prettier': '代码规范',
'Husky': 'Git钩子'
},
// 桌面特性支持
desktop: {
'窗口管理': '多窗口、父子窗口、模态窗口',
'系统集成': '文件系统、系统托盘、全局快捷键',
'自动更新': '增量更新、静默更新',
'安全沙箱': '上下文隔离、CSP策略'
}
};
三、环境准备与配置
3.1 项目初始化配置
// package.json
{
"name": "my-electron-vue-app",
"version": "1.0.0",
"description": "A modern desktop app built with Electron and Vue 3",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && electron .\"",
"electron:build": "npm run build && electron-builder",
"electron:build-win": "npm run build && electron-builder --win",
"electron:build-mac": "npm run build && electron-builder --mac",
"electron:build-linux": "npm run build && electron-builder --linux"
},
"dependencies": {
"vue": "^3.3.0",
"vue-router": "^4.0.0",
"pinia": "^2.0.0",
"electron": "^28.0.0",
"electron-updater": "^6.0.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.0",
"@vue/tsconfig": "^0.1.0",
"typescript": "^5.0.0",
"vite": "^4.0.0",
"electron-builder": "^24.0.0",
"concurrently": "^7.0.0",
"wait-on": "^7.0.0"
},
"build": {
"appId": "com.yourcompany.yourapp",
"productName": "Your App",
"directories": {
"output": "dist_electron"
},
"files": [
"dist/**/*",
"node_modules/**/*"
],
"mac": {
"category": "public.app-category.productivity"
},
"win": {
"target": "nsis"
},
"linux": {
"target": "AppImage"
}
}
}
3.2 Vite 配置
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
base: './',
build: {
outDir: 'dist',
emptyOutDir: true,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html')
}
}
},
server: {
port: 5173,
strictPort: true
},
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
}
})
3.3 Electron 主进程配置
// electron/main.js
import { app, BrowserWindow, Menu, ipcMain, shell } from 'electron'
import { join } from 'path'
import { autoUpdater } from 'electron-updater'
class AppWindow {
constructor() {
this.mainWindow = null
this.init()
}
init() {
app.whenReady().then(() => {
this.createWindow()
this.setupMenu()
this.setupIPC()
this.setupAutoUpdate()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
this.createWindow()
}
})
}
createWindow() {
this.mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: join(__dirname, 'preload.js')
},
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
show: false,
icon: join(__dirname, '../assets/icon.png')
})
// 开发环境加载本地服务器,生产环境加载打包文件
if (process.env.NODE_ENV === 'development') {
this.mainWindow.loadURL('http://localhost:5173')
this.mainWindow.webContents.openDevTools()
} else {
this.mainWindow.loadFile(join(__dirname, '../dist/index.html'))
}
this.mainWindow.once('ready-to-show', () => {
this.mainWindow.show()
})
this.mainWindow.on('closed', () => {
this.mainWindow = null
})
}
setupMenu() {
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => {
this.createNewFile()
}
},
{ type: 'separator' },
{
label: '退出',
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
click: () => {
app.quit()
}
}
]
},
{
label: '编辑',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' }
]
},
{
label: '视图',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
setupIPC() {
// 处理窗口控制
ipcMain.handle('window-minimize', () => {
this.mainWindow.minimize()
})
ipcMain.handle('window-maximize', () => {
if (this.mainWindow.isMaximized()) {
this.mainWindow.unmaximize()
} else {
this.mainWindow.maximize()
}
})
ipcMain.handle('window-close', () => {
this.mainWindow.close()
})
// 处理文件操作
ipcMain.handle('show-save-dialog', async (event, options) => {
const { dialog } = require('electron')
const result = await dialog.showSaveDialog(this.mainWindow, options)
return result
})
ipcMain.handle('show-open-dialog', async (event, options) => {
const { dialog } = require('electron')
const result = await dialog.showOpenDialog(this.mainWindow, options)
return result
})
// 处理外部链接
ipcMain.handle('open-external', (event, url) => {
shell.openExternal(url)
})
}
setupAutoUpdate() {
if (process.env.NODE_ENV === 'production') {
autoUpdater.checkForUpdatesAndNotify()
}
}
createNewFile() {
// 创建新文件的逻辑
this.mainWindow.webContents.send('new-file-created')
}
}
new AppWindow()
3.4 预加载脚本配置
// electron/preload.js
import { contextBridge, ipcRenderer } from 'electron'
// 暴露安全的API给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
// 窗口控制
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
maximizeWindow: () => ipcRenderer.invoke('window-maximize'),
closeWindow: () => ipcRenderer.invoke('window-close'),
// 文件操作
showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
showOpenDialog: (options) => ipcRenderer.invoke('show-open-dialog', options),
// 系统操作
openExternal: (url) => ipcRenderer.invoke('open-external', url),
// 应用事件
onNewFile: (callback) => ipcRenderer.on('new-file-created', callback),
// 移除监听器
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
})
// 类型定义
contextBridge.exposeInMainWorld('env', {
platform: process.platform,
version: process.versions.electron
})
四、核心架构实现
4.1 Vue 应用架构
<!-- src/App.vue -->
<template>
<div id="app" :class="platformClass">
<TitleBar v-if="!isMac" @minimize="minimize" @maximize="maximize" @close="close" />
<div class="app-container">
<SideBar v-model:collapsed="sidebarCollapsed" />
<div class="main-content" :class="{ 'sidebar-collapsed': sidebarCollapsed }">
<RouterView />
</div>
</div>
<StatusBar />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import TitleBar from '@/components/TitleBar.vue'
import SideBar from '@/components/SideBar.vue'
import StatusBar from '@/components/StatusBar.vue'
const router = useRouter()
// 响应式状态
const sidebarCollapsed = ref(false)
const platformClass = ref('')
// 计算属性
const isMac = ref(false)
// 生命周期
onMounted(() => {
initializeApp()
setupEventListeners()
})
onUnmounted(() => {
cleanupEventListeners()
})
// 方法
const initializeApp = () => {
// 检测平台
isMac.value = window.env?.platform === 'darwin'
platformClass.value = `platform-${window.env?.platform || 'unknown'}`
// 监听Electron事件
if (window.electronAPI) {
window.electronAPI.onNewFile(() => {
handleNewFile()
})
}
}
const setupEventListeners = () => {
// 全局快捷键
document.addEventListener('keydown', handleGlobalShortcut)
}
const cleanupEventListeners = () => {
document.removeEventListener('keydown', handleGlobalShortcut)
if (window.electronAPI) {
window.electronAPI.removeAllListeners('new-file-created')
}
}
// 窗口控制
const minimize = async () => {
if (window.electronAPI) {
await window.electronAPI.minimizeWindow()
}
}
const maximize = async () => {
if (window.electronAPI) {
await window.electronAPI.maximizeWindow()
}
}
const close = async () => {
if (window.electronAPI) {
await window.electronAPI.closeWindow()
}
}
// 业务逻辑
const handleNewFile = () => {
router.push('/editor')
// 触发新建文件逻辑
}
const handleGlobalShortcut = (event: KeyboardEvent) => {
if (event.ctrlKey || event.metaKey) {
switch (event.key) {
case 'n':
event.preventDefault()
handleNewFile()
break
case 's':
event.preventDefault()
// 保存文件逻辑
break
case 'o':
event.preventDefault()
// 打开文件逻辑
break
}
}
}
</script>
<style scoped>
#app {
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-primary);
}
.app-container {
display: flex;
flex: 1;
overflow: hidden;
}
.main-content {
flex: 1;
transition: margin-left 0.3s ease;
}
.main-content.sidebar-collapsed {
margin-left: 0;
}
/* 平台特定样式 */
.platform-darwin {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.platform-win32 {
font-family: 'Segoe UI', system-ui, sans-serif;
}
.platform-linux {
font-family: 'Noto Sans', system-ui, sans-serif;
}
</style>
4.2 状态管理 (Pinia)
// src/stores/appStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface FileInfo {
id: string
name: string
path: string
content: string
modified: Date
isDirty: boolean
}
export const useAppStore = defineStore('app', () => {
// 状态
const currentFile = ref<FileInfo | null>(null)
const recentFiles = ref<FileInfo[]>([])
const settings = ref({
theme: 'dark',
fontSize: 14,
wordWrap: true,
autoSave: true,
autoSaveInterval: 3000
})
const isFullscreen = ref(false)
const isLoading = ref(false)
// Getter
const hasUnsavedChanges = computed(() =>
currentFile.value?.isDirty || false
)
const currentTheme = computed(() => settings.value.theme)
const displayName = computed(() =>
currentFile.value
? `${currentFile.value.name}${currentFile.value.isDirty ? ' *' : ''}`
: 'Untitled'
)
// Actions
const openFile = async (filePath: string) => {
isLoading.value = true
try {
// 模拟文件读取
const content = await window.electronAPI.readFile(filePath)
const fileInfo: FileInfo = {
id: generateId(),
name: filePath.split('/').pop() || 'Unknown',
path: filePath,
content,
modified: new Date(),
isDirty: false
}
currentFile.value = fileInfo
addToRecentFiles(fileInfo)
} catch (error) {
console.error('Failed to open file:', error)
throw error
} finally {
isLoading.value = false
}
}
const saveFile = async (content?: string) => {
if (!currentFile.value) return
try {
const saveContent = content || currentFile.value.content
await window.electronAPI.writeFile(currentFile.value.path, saveContent)
currentFile.value.content = saveContent
currentFile.value.modified = new Date()
currentFile.value.isDirty = false
} catch (error) {
console.error('Failed to save file:', error)
throw error
}
}
const newFile = () => {
const fileInfo: FileInfo = {
id: generateId(),
name: 'Untitled',
path: '',
content: '',
modified: new Date(),
isDirty: true
}
currentFile.value = fileInfo
}
const updateContent = (content: string) => {
if (currentFile.value) {
currentFile.value.content = content
currentFile.value.isDirty = true
currentFile.value.modified = new Date()
}
}
const toggleTheme = () => {
settings.value.theme = settings.value.theme === 'dark' ? 'light' : 'dark'
updateThemeInDOM()
}
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value
}
// 私有方法
const addToRecentFiles = (fileInfo: FileInfo) => {
const existingIndex = recentFiles.value.findIndex(f => f.path === fileInfo.path)
if (existingIndex >= 0) {
recentFiles.value.splice(existingIndex, 1)
}
recentFiles.value.unshift(fileInfo)
// 保持最近文件数量
if (recentFiles.value.length > 10) {
recentFiles.value = recentFiles.value.slice(0, 10)
}
}
const generateId = (): string => {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
const updateThemeInDOM = () => {
document.documentElement.setAttribute('data-theme', settings.value.theme)
}
// 初始化
updateThemeInDOM()
return {
// 状态
currentFile,
recentFiles,
settings,
isFullscreen,
isLoading,
// Getter
hasUnsavedChanges,
currentTheme,
displayName,
// Actions
openFile,
saveFile,
newFile,
updateContent,
toggleTheme,
toggleFullscreen
}
})
4.3 文件编辑器组件
<!-- src/components/FileEditor.vue -->
<template>
<div class="file-editor">
<div class="editor-toolbar">
<div class="left-actions">
<button @click="saveFile" :disabled="!hasChanges" title="Save (Ctrl+S)">
<SaveIcon /> Save
</button>
<button @click="formatCode" title="Format Code (Shift+Alt+F)">
<FormatIcon /> Format
</button>
</div>
<div class="file-info">
<span class="file-name">{{ fileName }}</span>
<span v-if="hasChanges" class="unsaved-indicator">●</span>
</div>
<div class="right-actions">
<button @click="toggleWordWrap" :class="{ active: wordWrap }" title="Toggle Word Wrap">
<WrapIcon /> Wrap
</button>
<button @click="findInFile" title="Find (Ctrl+F)">
<SearchIcon /> Find
</button>
</div>
</div>
<div class="editor-container">
<textarea
ref="editorRef"
v-model="localContent"
@input="onContentChange"
@keydown="onKeyDown"
:style="editorStyles"
spellcheck="false"
></textarea>
<div v-if="showFind" class="find-widget">
<input
ref="findInputRef"
v-model="findText"
@keydown.esc="closeFind"
@keydown.enter="findNext"
placeholder="Find..."
/>
<button @click="findNext">Next</button>
<button @click="findPrev">Prev</button>
<button @click="closeFind">×</button>
</div>
</div>
<StatusBar
:line="currentLine"
:column="currentColumn"
:language="currentLanguage"
@theme-change="$emit('theme-change')"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick, onMounted } from 'vue'
import { useAppStore } from '@/stores/appStore'
// 图标组件(简化表示)
const SaveIcon = { template: '<span>💾</span>' }
const FormatIcon = { template: '<span>🔧</span>' }
const WrapIcon = { template: '<span>↩</span>' }
const SearchIcon = { template: '<span>🔍</span>' }
const props = defineProps<{
content: string
}>()
const emit = defineEmits<{
'update:content': [string]
'theme-change': []
}>()
const appStore = useAppStore()
// 响应式数据
const editorRef = ref<HTMLTextAreaElement>()
const findInputRef = ref<HTMLInputElement>()
const localContent = ref(props.content)
const showFind = ref(false)
const findText = ref('')
const wordWrap = ref(true)
const currentLine = ref(1)
const currentColumn = ref(1)
// 计算属性
const fileName = computed(() => appStore.displayName)
const hasChanges = computed(() => appStore.hasUnsavedChanges)
const currentLanguage = computed(() => {
const ext = appStore.currentFile?.name.split('.').pop() || 'txt'
const languageMap: { [key: string]: string } = {
js: 'JavaScript',
ts: 'TypeScript',
vue: 'Vue',
html: 'HTML',
css: 'CSS',
json: 'JSON'
}
return languageMap[ext] || 'Text'
})
const editorStyles = computed(() => ({
fontFamily: "'Fira Code', monospace",
fontSize: `${appStore.settings.fontSize}px`,
lineHeight: '1.5',
whiteSpace: wordWrap.value ? 'pre-wrap' : 'pre',
wordWrap: wordWrap.value ? 'break-word' : 'normal'
}))
// 监视器
watch(() => props.content, (newContent) => {
if (newContent !== localContent.value) {
localContent.value = newContent
}
})
// 生命周期
onMounted(() => {
focusEditor()
setupEditorShortcuts()
})
// 方法
const onContentChange = () => {
emit('update:content', localContent.value)
updateCursorPosition()
}
const onKeyDown = (event: KeyboardEvent) => {
// 处理快捷键
if ((event.ctrlKey || event.metaKey) && !event.shiftKey) {
switch (event.key) {
case 's':
event.preventDefault()
saveFile()
break
case 'f':
event.preventDefault()
showFindWidget()
break
}
}
// 格式化快捷键
if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === 'F') {
event.preventDefault()
formatCode()
}
}
const saveFile = async () => {
try {
await appStore.saveFile(localContent.value)
} catch (error) {
console.error('Save failed:', error)
}
}
const formatCode = () => {
// 简单的格式化逻辑(实际项目中可使用Prettier)
if (currentLanguage.value === 'JSON') {
try {
const formatted = JSON.stringify(JSON.parse(localContent.value), null, 2)
localContent.value = formatted
emit('update:content', formatted)
} catch (error) {
console.warn('Not valid JSON')
}
}
}
const toggleWordWrap = () => {
wordWrap.value = !wordWrap.value
}
const showFindWidget = () => {
showFind.value = true
nextTick(() => {
findInputRef.value?.focus()
findInputRef.value?.select()
})
}
const closeFind = () => {
showFind.value = false
findText.value = ''
focusEditor()
}
const findNext = () => {
// 实现查找逻辑
console.log('Find next:', findText.value)
}
const findPrev = () => {
// 实现查找逻辑
console.log('Find previous:', findText.value)
}
const focusEditor = () => {
editorRef.value?.focus()
}
const updateCursorPosition = () => {
if (!editorRef.value) return
const text = localContent.value
const cursorPos = editorRef.value.selectionStart
// 计算行号和列号
const textBeforeCursor = text.substring(0, cursorPos)
const lines = textBeforeCursor.split('\n')
currentLine.value = lines.length
currentColumn.value = lines[lines.length - 1].length + 1
}
const setupEditorShortcuts = () => {
const handleKeydown = (event: KeyboardEvent) => {
if (event.target !== editorRef.value &&
(event.ctrlKey || event.metaKey) &&
event.key === 's') {
event.preventDefault()
saveFile()
}
}
document.addEventListener('keydown', handleKeydown)
// 清理函数
return () => document.removeEventListener('keydown', handleKeydown)
}
</script>
<style scoped>
.file-editor {
height: 100%;
display: flex;
flex-direction: column;
}
.editor-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.left-actions, .right-actions {
display: flex;
gap: 8px;
}
button {
padding: 4px 8px;
border: 1px solid var(--border-color);
background: var(--bg-primary);
color: var(--text-primary);
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
button:hover:not(:disabled) {
background: var(--bg-hover);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button.active {
background: var(--accent-color);
color: white;
}
.file-info {
display: flex;
align-items: center;
gap: 8px;
font-family: monospace;
}
.file-name {
font-weight: 500;
}
.unsaved-indicator {
color: var(--accent-color);
font-size: 16px;
}
.editor-container {
position: relative;
flex: 1;
overflow: hidden;
}
textarea {
width: 100%;
height: 100%;
border: none;
outline: none;
resize: none;
padding: 16px;
background: var(--bg-primary);
color: var(--text-primary);
tab-size: 2;
}
.find-widget {
position: absolute;
top: 10px;
right: 10px;
display: flex;
gap: 4px;
background: var(--bg-secondary);
padding: 8px;
border-radius: 4px;
border: 1px solid var(--border-color);
}
.find-widget input {
padding: 4px 8px;
border: 1px solid var(--border-color);
border-radius: 2px;
background: var(--bg-primary);
color: var(--text-primary);
}
</style>
五、实际应用场景
5.1 现代化代码编辑器
// src/utils/fileOperations.ts
import { dialog } from 'electron'
export class FileOperations {
static async openFile(): Promise<{ path: string; content: string } | null> {
try {
const result = await dialog.showOpenDialog({
filters: [
{ name: 'All Files', extensions: ['*'] },
{ name: 'Text Files', extensions: ['txt', 'md'] },
{ name: 'Code Files', extensions: ['js', 'ts', 'vue', 'html', 'css'] }
],
properties: ['openFile']
})
if (!result.canceled && result.filePaths.length > 0) {
const path = result.filePaths[0]
const content = await this.readFileContent(path)
return { path, content }
}
return null
} catch (error) {
console.error('Open file error:', error)
throw error
}
}
static async saveFile(content: string, defaultPath?: string): Promise<string | null> {
try {
const result = await dialog.showSaveDialog({
defaultPath,
filters: [
{ name: 'All Files', extensions: ['*'] },
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'Markdown', extensions: ['md'] },
{ name: 'JavaScript', extensions: ['js'] },
{ name: 'TypeScript', extensions: ['ts'] }
]
})
if (!result.canceled && result.filePath) {
await this.writeFileContent(result.filePath, content)
return result.filePath
}
return null
} catch (error) {
console.error('Save file error:', error)
throw error
}
}
private static async readFileContent(path: string): Promise<string> {
// 在实际应用中,这里会调用Electron的fs模块
// 这里使用模拟实现
return new Promise((resolve) => {
setTimeout(() => {
resolve(`// Content of ${path}\nconsole.log('Hello World');`)
}, 100)
})
}
private static async writeFileContent(path: string, content: string): Promise<void> {
// 在实际应用中,这里会调用Electron的fs模块
console.log(`Writing content to ${path}`)
}
}
5.2 系统托盘应用
// electron/trayManager.ts
import { app, Tray, Menu, nativeImage } from 'electron'
import { join } from 'path'
export class TrayManager {
private tray: Tray | null = null
constructor(private mainWindow: Electron.BrowserWindow) {}
createTray() {
// 创建托盘图标
const iconPath = join(__dirname, '../assets/tray-icon.png')
const trayIcon = nativeImage.createFromPath(iconPath)
this.tray = new Tray(trayIcon.resize({ width: 16, height: 16 }))
// 托盘菜单
const contextMenu = Menu.buildFromTemplate([
{
label: '显示窗口',
click: () => {
this.mainWindow.show()
this.mainWindow.focus()
}
},
{
label: '新建文件',
click: () => {
this.mainWindow.webContents.send('tray-new-file')
}
},
{ type: 'separator' },
{
label: '退出',
click: () => {
app.quit()
}
}
])
this.tray.setContextMenu(contextMenu)
this.tray.setToolTip('My Electron App')
// 托盘图标点击事件
this.tray.on('click', () => {
this.toggleWindow()
})
// 双击事件
this.tray.on('double-click', () => {
this.mainWindow.show()
this.mainWindow.focus()
})
}
private toggleWindow() {
if (this.mainWindow.isVisible()) {
this.mainWindow.hide()
} else {
this.mainWindow.show()
this.mainWindow.focus()
}
}
destroy() {
if (this.tray) {
this.tray.destroy()
this.tray = null
}
}
}
六、测试与质量保证
6.1 单元测试配置
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'happy-dom',
globals: true,
setupFiles: ['./tests/setup.js'],
coverage: {
reporter: ['text', 'html', 'lcov'],
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/electron/**',
'**/*.config.js'
]
}
}
})
6.2 组件测试示例
// tests/components/FileEditor.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import FileEditor from '@/components/FileEditor.vue'
import { useAppStore } from '@/stores/appStore'
// 模拟Electron API
vi.stubGlobal('electronAPI', {
readFile: vi.fn(),
writeFile: vi.fn()
})
// 模拟Pinia store
vi.mock('@/stores/appStore', () => ({
useAppStore: vi.fn()
}))
describe('FileEditor', () => {
const mockAppStore = {
currentFile: {
id: '1',
name: 'test.js',
path: '/path/to/test.js',
content: 'console.log("hello")',
modified: new Date(),
isDirty: false
},
displayName: 'test.js',
hasUnsavedChanges: false,
saveFile: vi.fn(),
settings: {
fontSize: 14
}
}
beforeEach(() => {
vi.mocked(useAppStore).mockReturnValue(mockAppStore)
})
it('renders file content correctly', () => {
const wrapper = mount(FileEditor, {
props: {
content: 'console.log("test")'
}
})
expect(wrapper.find('textarea').element.value).toBe('console.log("test")')
})
it('emits update event when content changes', async () => {
const wrapper = mount(FileEditor, {
props: {
content: 'initial'
}
})
const textarea = wrapper.find('textarea')
await textarea.setValue('updated content')
expect(wrapper.emitted('update:content')).toBeTruthy()
expect(wrapper.emitted('update:content')![0]).toEqual(['updated content'])
})
it('handles save shortcut', async () => {
const wrapper = mount(FileEditor, {
props: {
content: 'content to save'
}
})
// 模拟Ctrl+S快捷键
const event = new KeyboardEvent('keydown', {
ctrlKey: true,
key: 's'
})
await wrapper.find('textarea').element.dispatchEvent(event)
expect(mockAppStore.saveFile).toHaveBeenCalledWith('content to save')
})
})
6.3 E2E 测试配置
// tests/e2e/example.spec.js
import { test, expect } from '@playwright/test'
test('basic app functionality', async ({ page }) => {
// 启动应用(需要先启动开发服务器)
await page.goto('http://localhost:5173')
// 检查应用标题
await expect(page.locator('h1')).toContainText('My Electron App')
// 测试文件操作
await page.click('button:has-text("New File")')
await expect(page.locator('.file-name')).toContainText('Untitled')
// 测试编辑功能
const editor = page.locator('textarea')
await editor.fill('console.log("Hello World")')
await expect(editor).toHaveValue('console.log("Hello World")')
})
七、打包与部署
7.1 生产环境构建配置
// electron-builder.config.js
module.exports = {
appId: 'com.yourcompany.yourapp',
productName: 'Your App',
directories: {
output: 'release'
},
files: [
'dist/**/*',
'electron/**/*',
'node_modules/**/*',
'package.json'
],
extraMetadata: {
main: 'electron/main.js'
},
mac: {
category: 'public.app-category.developer-tools',
icon: 'build/icon.icns',
target: [
{
target: 'dmg',
arch: ['x64', 'arm64']
}
]
},
win: {
icon: 'build/icon.ico',
target: [
{
target: 'nsis',
arch: ['x64', 'ia32']
},
{
target: 'portable',
arch: ['x64']
}
]
},
linux: {
icon: 'build/icon.png',
category: 'Development',
target: [
{
target: 'AppImage',
arch: ['x64']
},
{
target: 'deb',
arch: ['x64']
}
]
},
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true,
createStartMenuShortcut: true
}
}
7.2 自动更新配置
// electron/updater.js
import { autoUpdater } from 'electron-updater'
import { dialog } from 'electron'
export class AppUpdater {
constructor(mainWindow) {
this.mainWindow = mainWindow
this.setupUpdater()
}
setupUpdater() {
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.on('checking-for-update', () => {
this.mainWindow.webContents.send('update-status', 'checking')
})
autoUpdater.on('update-available', (info) => {
this.mainWindow.webContents.send('update-available', info)
dialog.showMessageBox(this.mainWindow, {
type: 'info',
title: 'Update Available',
message: `A new version ${info.version} is available. Do you want to download it now?`,
buttons: ['Download', 'Later']
}).then((result) => {
if (result.response === 0) {
autoUpdater.downloadUpdate()
}
})
})
autoUpdater.on('update-not-available', (info) => {
this.mainWindow.webContents.send('update-status', 'up-to-date')
})
autoUpdater.on('download-progress', (progress) => {
this.mainWindow.webContents.send('download-progress', progress)
})
autoUpdater.on('update-downloaded', (info) => {
this.mainWindow.webContents.send('update-downloaded', info)
dialog.showMessageBox(this.mainWindow, {
type: 'info',
title: 'Update Ready',
message: 'The update has been downloaded. Restart the application to apply the update?',
buttons: ['Restart', 'Later']
}).then((result) => {
if (result.response === 0) {
autoUpdater.quitAndInstall()
}
})
})
autoUpdater.on('error', (error) => {
this.mainWindow.webContents.send('update-error', error)
console.error('Update error:', error)
})
}
checkForUpdates() {
autoUpdater.checkForUpdates()
}
}
八、总结
8.1 技术成果总结
核心架构特性
- •
跨平台一致性:一次开发,Windows、macOS、Linux 全平台支持 - •
现代化开发体验:Vue 3 组合式 API + TypeScript + Vite 热重载 - •
原生系统集成:文件系统、系统托盘、全局快捷键、自动更新 - •
性能优化:代码分割、懒加载、进程隔离、内存管理
开发效率指标
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8.2 最佳实践总结
架构设计原则
class ElectronVueBestPractices {
static getArchitecturePrinciples() {
return {
'进程隔离': '主进程处理系统API,渲染进程处理UI',
'安全第一': '禁用nodeIntegration,使用contextBridge',
'性能优化': '代码分割、懒加载、避免阻塞操作',
'错误处理': '完善的错误边界和崩溃报告',
'用户体验': '原生菜单、系统集成、流畅动画'
};
}
static getDevelopmentPractices() {
return {
'TypeScript': '提供类型安全,减少运行时错误',
'组件化': '可复用的Vue组件,提高开发效率',
'状态管理': 'Pinia统一状态管理,数据流清晰',
'自动化': 'CI/CD自动化测试、构建、部署',
'文档化': '完善的文档和代码注释'
};
}
}
8.3 未来展望
技术演进趋势
class ElectronVueFuture {
static getTechnologyTrends() {
return {
'2024': [
'Electron 28+ 性能大幅提升',
'Vue 3.3 更优的类型推导',
'Vite 5 更快的构建速度',
'WebContainer 技术集成'
],
'2025': [
'Electron 与 Tauri 融合',
'WebGPU 硬件加速',
'AI 辅助开发',
'低代码桌面应用开发'
]
};
}
static getIndustryTrends() {
return {
'跨平台开发': 'Electron 在桌面端持续领先',
'微前端架构': '大型桌面应用的模块化开发',
'云原生桌面': '桌面应用与云服务深度集成',
'智能化体验': 'AI 驱动的个性化功能'
};
}
}
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)