Electron 实战|Vue 桌面端开发从入门到上线

引言

大家好!作为一名前端开发者,你是否曾经想过将你的 Vue 应用打包成桌面应用?今天我要分享的是使用 Electron 将 Vue 应用转换为桌面应用的完整实战经验。从项目搭建到最终上线,我会详细介绍每个步骤,包括一些实用的自动化脚本和最佳实践。

需求背景

在开发过程中,我们经常遇到这样的场景:

  • 需要将 Web 应用打包成桌面应用
  • 希望应用能够离线运行
  • 需要访问本地文件系统
  • 要求应用具有原生桌面体验

Electron 正是解决这些需求的完美方案。它基于 Chromium 和 Node.js,让我们可以用 Web 技术开发跨平台的桌面应用。

工作原理

Electron 的核心架构包含两个进程:

  • 主进程(Main Process):负责创建和管理应用窗口,处理系统级 API
  • 渲染进程(Renderer Process):运行我们的 Vue 应用,类似于浏览器中的网页

两个进程通过 IPC(进程间通信)进行数据交换,主进程可以访问 Node.js API,渲染进程则专注于 UI 展示。

代码实现

1. 项目初始化

首先创建项目目录结构:

bash 复制代码
mkdir electron-vue-app
cd electron-vue-app
npm init -y

安装必要的依赖:

bash 复制代码
npm install electron electron-builder --save-dev
npm install vue@next @vitejs/plugin-vue vite --save-dev

2. 主进程配置

创建 main.js 文件:

javascript 复制代码
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')

let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
      enableRemoteModule: true
    }
  })

  // 开发环境加载本地服务器,生产环境加载打包文件
  if (process.env.NODE_ENV === 'development') {
    mainWindow.loadURL('http://localhost:3000')
    mainWindow.webContents.openDevTools()
  } else {
    mainWindow.loadFile('dist/index.html')
  }
}

app.whenReady().then(createWindow)

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

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

3. Vue 应用配置

创建 vite.config.js

javascript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

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

4. 自动化脚本实现

这里我们实现一个实用的文件备份脚本,展示 Electron 与 Node.js 的深度集成:

python 复制代码
# backup_manager.py
import os
import shutil
import schedule
import time
import logging
from datetime import datetime
from pathlib import Path

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('backup.log'),
        logging.StreamHandler()
    ]
)

class BackupManager:
    def __init__(self, source_dir, backup_dir):
        self.source_dir = Path(source_dir)
        self.backup_dir = Path(backup_dir)
        self.backup_dir.mkdir(exist_ok=True)
        
    def backup_folder(self, folder_name):
        """复制指定文件夹到备份目录"""
        try:
            source_path = self.source_dir / folder_name
            if not source_path.exists():
                logging.warning(f"源文件夹 {folder_name} 不存在")
                return False
                
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            backup_path = self.backup_dir / f"{folder_name}_{timestamp}"
            
            shutil.copytree(source_path, backup_path)
            logging.info(f"成功备份 {folder_name} 到 {backup_path}")
            return True
            
        except Exception as e:
            logging.error(f"备份 {folder_name} 失败: {str(e)}")
            return False
    
    def schedule_backup(self, folder_name, interval_hours=24):
        """定时备份任务"""
        schedule.every(interval_hours).hours.do(
            lambda: self.backup_folder(folder_name)
        )
        logging.info(f"已设置 {folder_name} 每 {interval_hours} 小时自动备份")
    
    def run_scheduler(self):
        """运行定时任务"""
        logging.info("开始运行定时备份任务...")
        while True:
            schedule.run_pending()
            time.sleep(60)  # 每分钟检查一次

# 使用示例
if __name__ == "__main__":
    backup_manager = BackupManager(
        source_dir="./src",
        backup_dir="./backups"
    )
    
    # 立即备份一次
    backup_manager.backup_folder("components")
    
    # 设置定时备份
    backup_manager.schedule_backup("components", 12)  # 每12小时备份一次
    backup_manager.schedule_backup("views", 24)      # 每24小时备份一次
    
    # 运行定时任务
    backup_manager.run_scheduler()

5. Vue 组件实现

创建 App.vue

vue 复制代码
<template>
  <div class="app">
    <header class="header">
      <h1>Electron Vue 桌面应用</h1>
      <div class="actions">
        <button @click="openFileDialog" class="btn btn-primary">
          选择文件夹
        </button>
        <button @click="startBackup" class="btn btn-success">
          开始备份
        </button>
        <button @click="viewLogs" class="btn btn-info">
          查看日志
        </button>
      </div>
    </header>
    
    <main class="main">
      <div class="file-list">
        <h3>文件列表</h3>
        <ul>
          <li v-for="file in fileList" :key="file.name" class="file-item">
            <span class="file-name">{{ file.name }}</span>
            <span class="file-size">{{ formatFileSize(file.size) }}</span>
            <span class="file-date">{{ formatDate(file.date) }}</span>
          </li>
        </ul>
      </div>
      
      <div class="backup-status">
        <h3>备份状态</h3>
        <div class="status-item" v-for="status in backupStatus" :key="status.id">
          <span :class="['status', status.type]">{{ status.message }}</span>
          <span class="timestamp">{{ status.timestamp }}</span>
        </div>
      </div>
    </main>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
  name: 'App',
  setup() {
    const fileList = ref([])
    const backupStatus = ref([])
    
    const openFileDialog = () => {
      // 调用 Electron 主进程打开文件对话框
      window.electronAPI.openFileDialog()
    }
    
    const startBackup = () => {
      backupStatus.value.push({
        id: Date.now(),
        type: 'info',
        message: '开始备份...',
        timestamp: new Date().toLocaleString()
      })
      
      // 调用备份脚本
      window.electronAPI.startBackup()
    }
    
    const viewLogs = () => {
      window.electronAPI.viewLogs()
    }
    
    const formatFileSize = (bytes) => {
      if (bytes === 0) return '0 Bytes'
      const k = 1024
      const sizes = ['Bytes', 'KB', 'MB', 'GB']
      const i = Math.floor(Math.log(bytes) / Math.log(k))
      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
    }
    
    const formatDate = (date) => {
      return new Date(date).toLocaleString()
    }
    
    onMounted(() => {
      // 监听来自主进程的消息
      window.electronAPI.onBackupComplete((event, result) => {
        backupStatus.value.push({
          id: Date.now(),
          type: result.success ? 'success' : 'error',
          message: result.message,
          timestamp: new Date().toLocaleString()
        })
      })
    })
    
    return {
      fileList,
      backupStatus,
      openFileDialog,
      startBackup,
      viewLogs,
      formatFileSize,
      formatDate
    }
  }
}
</script>

<style>
.app {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  margin: 0;
  padding: 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
}

.header {
  background: rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(10px);
  border-radius: 15px;
  padding: 20px;
  margin-bottom: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.header h1 {
  color: white;
  margin: 0;
  font-size: 2rem;
}

.actions {
  display: flex;
  gap: 10px;
}

.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 500;
  transition: all 0.3s ease;
}

.btn-primary {
  background: #007bff;
  color: white;
}

.btn-success {
  background: #28a745;
  color: white;
}

.btn-info {
  background: #17a2b8;
  color: white;
}

.btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

.main {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}

.file-list, .backup-status {
  background: rgba(255, 255, 255, 0.9);
  border-radius: 15px;
  padding: 20px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}

.file-item {
  display: flex;
  justify-content: space-between;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.status-item {
  display: flex;
  justify-content: space-between;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.status {
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 0.9rem;
}

.status.success {
  background: #d4edda;
  color: #155724;
}

.status.error {
  background: #f8d7da;
  color: #721c24;
}

.status.info {
  background: #d1ecf1;
  color: #0c5460;
}
</style>

6. 构建配置

创建 package.json 脚本:

json 复制代码
{
  "name": "electron-vue-app",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "dev": "concurrently \"npm run dev:vite\" \"wait-on http://localhost:3000 && electron .\"",
    "dev:vite": "vite",
    "build": "vite build",
    "build:electron": "npm run build && electron-builder",
    "dist": "npm run build && electron-builder --publish=never"
  },
  "build": {
    "appId": "com.example.electron-vue-app",
    "productName": "Electron Vue App",
    "directories": {
      "output": "dist-electron"
    },
    "files": [
      "dist/**/*",
      "main.js",
      "node_modules/**/*"
    ],
    "mac": {
      "category": "public.app-category.developer-tools"
    },
    "win": {
      "target": "nsis"
    },
    "linux": {
      "target": "AppImage"
    }
  }
}

运行效果截图

应用运行后的效果包括:

  1. 主界面:现代化的渐变背景,半透明毛玻璃效果的头部区域
  2. 文件管理:左侧显示文件列表,包含文件名、大小、修改日期
  3. 备份状态:右侧实时显示备份进度和结果
  4. 操作按钮:三个主要功能按钮,具有悬停动画效果
  5. 日志查看:点击查看日志按钮会打开日志文件

界面采用响应式设计,支持不同屏幕尺寸,整体风格简洁现代。

总结

通过这个实战项目,我们成功地将 Vue 应用打包成了桌面应用,并集成了实用的文件备份功能。关键技术点包括:

  • Electron 主进程与渲染进程通信:实现了 Web 技术与系统 API 的无缝集成
  • 自动化脚本:使用 Python 的 shutil、schedule、logging 模块实现了文件备份和定时任务
  • 现代化 UI:采用 CSS Grid 布局和毛玻璃效果,提供良好的用户体验

这个脚本具有很强的扩展性,你可以进一步:

  • 云端备份:集成 AWS S3、阿里云 OSS 等云存储服务
  • 压缩打包:使用 zipfile 或 tar 模块压缩备份文件
  • 增量备份:只备份修改过的文件,提高效率
  • 加密存储:使用 cryptography 模块加密敏感文件

希望这篇文章对你在 Electron + Vue 桌面应用开发方面有所帮助!

参考资料


作者 : 王新焱
博客 : https://blog.csdn.net/qq_34402069
时间: 2025年10月22日


相关推荐
大猩猩X1 小时前
vxe-gantt 甘特图使用右键菜单
vue.js·vxe-table·vxe-ui·vxe-gantt
灵魂学者2 小时前
Vue3.x —— 父子通信
前端·javascript·vue.js·github
芳草萋萋鹦鹉洲哦4 小时前
【vue/js】文字超长悬停显示的几种方式
前端·javascript·vue.js
国服第二切图仔4 小时前
Electron for 鸿蒙PC项目实战案例之数独游戏
游戏·electron·鸿蒙pc
涔溪4 小时前
Vue3 的核心语法
前端·vue.js·typescript
国服第二切图仔5 小时前
Electron for 鸿蒙pc项目实战之tab标签页组件
javascript·electron·harmonyos·鸿蒙pc
Zohnny5 小时前
3.组合式函数
vue.js
小周同学6 小时前
vue3 上传文件,图片,视频组件
前端·vue.js
哆啦A梦15886 小时前
62 对接支付宝沙箱
前端·javascript·vue.js·node.js