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日


相关推荐
前端架构师-老李5 小时前
15、Electron专题:使用 electron-store 进行本地数据存储
前端·javascript·electron
Rysxt_5 小时前
Electron 教程:从背景到 Vue3 桌面应用开发
前端·javascript·electron
合作小小程序员小小店5 小时前
web网页开发,在线%考试,教资,题库%系统demo,基于vue,html,css,python,flask,随机分配,多角色,前后端分离,mysql数据库
前端·vue.js·后端·前端框架·flask
一枚前端小能手6 小时前
🔄 重学Vue之nextTick和slot - 从底层实现到实战应用的完整指南
前端·javascript·vue.js
一嘴一个橘子8 小时前
vue.js 视频截取为 gif - 2(将截取到的gif 转换为base64 、file)
vue.js
你的电影很有趣8 小时前
lesson73:Vue渐进式框架的进化之路——组合式API、选项式对比与响应式新范式
javascript·vue.js
小张成长计划..8 小时前
VUE工程化开发模式
前端·javascript·vue.js
菜鸟una10 小时前
【微信小程序 + map组件】自定义地图气泡?原生气泡?如何抉择?
前端·vue.js·程序人生·微信小程序·小程序·typescript
岁月宁静17 小时前
深度定制:在 Vue 3.5 应用中集成流式 AI 写作助手的实践
前端·vue.js·人工智能