Vue3 + Electron + Node.js 桌面项目完整开发指南

以下是包含前后端全链路的开发方案,从项目创建到部署的全流程说明,附带详细注释。

一、技术栈总览

模块技术选型说明
桌面端框架Vue3 + Vite + Electron构建跨平台桌面应用
本地数据库SQLite3 + typeorm本地数据缓存
状态管理Pinia管理应用状态(含登录状态)
UI 组件Element Plus桌面级 UI 组件库
服务端Node.js + Express + MySQL提供 API 接口、鉴权、数据同步
通信协议Axios(HTTP)+ WebSocket(聊天)接口请求与实时通信
打包工具Electron-builder打包成桌面应用

二、项目架构设计

plaintext

project/
├── client/                # 桌面端(Vue3 + Electron)
│   ├── electron/          # Electron主进程
│   │   ├── main.js        # 窗口管理、IPC主进程逻辑
│   │   ├── preload.js     # 渲染进程与主进程通信桥接
│   │   └── db/            # SQLite操作
│   │       ├── connection.js  # 数据库连接
│   │       └── models/     # 数据模型(用户、聊天记录等)
│   ├── src/               # Vue渲染进程
│   │   ├── api/           # 接口请求封装
│   │   ├── components/    # 公共组件(图片、音视频等)
│   │   ├── router/        # 路由(含鉴权守卫)
│   │   ├── stores/        # Pinia状态管理
│   │   ├── utils/         # 工具函数(加密、日期等)
│   │   ├── views/         # 页面(登录、聊天、表格等)
│   │   └── main.js        # Vue入口
│   └── package.json       # 客户端依赖
├── server/                # 服务端(Node.js)
│   ├── src/
│   │   ├── config/        # 配置(数据库、端口等)
│   │   ├── controller/    # 接口逻辑
│   │   ├── middleware/    # 中间件(鉴权、日志等)
│   │   ├── model/         # 服务端数据模型
│   │   ├── router/        # 接口路由
│   │   └── app.js         # 服务端入口
│   └── package.json       # 服务端依赖
└── README.md              # 项目说明

三、详细开发步骤

1. 创建服务端(Node.js)
① 初始化服务端项目
mkdir project && cd project
mkdir server && cd server
npm init -y
npm install express mysql2 sequelize jsonwebtoken cors ws dotenv  # 核心依赖
npm install nodemon --save-dev  # 开发热重载
② 配置服务端基础结构

通过以上步骤,可完成一个功能完整的桌面应用,支持本地缓存、云端同步、多媒体处理和实时聊天等核心需求。

六、注意事项

  • 数据库配置server/src/config/db.js):
    const { Sequelize } = require('sequelize');
    require('dotenv').config(); // 加载环境变量
    
    // 连接MySQL(服务端数据库)
    const sequelize = new Sequelize(
      process.env.DB_NAME || 'electron_app',
      process.env.DB_USER || 'root',
      process.env.DB_PASS || '123456',
      {
        host: process.env.DB_HOST || 'localhost',
        dialect: 'mysql'
      }
    );
    
    // 测试连接
    sequelize.authenticate()
      .then(() => console.log('服务端数据库连接成功'))
      .catch(err => console.error('连接失败:', err));
    
    module.exports = sequelize;

  • 用户模型server/src/model/user.js):
  • const { DataTypes } = require('sequelize');
    const sequelize = require('../config/db');
    
    // 服务端用户表(存储用户账号信息)
    const User = sequelize.define('User', {
      username: {
        type: DataTypes.STRING,
        unique: true,
        allowNull: false
      },
      password: {
        type: DataTypes.STRING,
        allowNull: false // 存储加密后的密码
      },
      avatar: {
        type: DataTypes.STRING, // 头像URL
        defaultValue: ''
      }
    });
    
    // 同步表结构(开发环境)
    User.sync();
    
    module.exports = User;

  • 鉴权中间件server/src/middleware/auth.js):
    const jwt = require('jsonwebtoken');
    require('dotenv').config();
    
    // 验证token的中间件
    const auth = (req, res, next) => {
      const token = req.headers.authorization?.split(' ')[1]; // 从请求头获取token
      if (!token) return res.status(401).json({ msg: '未登录' });
    
      try {
        // 验证token(密钥从环境变量获取)
        const decoded = jwt.verify(token, process.env.JWT_SECRET || 'secret_key');
        req.user = decoded; // 将用户信息挂载到req
        next();
      } catch (err) {
        res.status(401).json({ msg: 'token无效' });
      }
    };
    
    module.exports = auth;

  • 登录接口server/src/controller/authController.js):
    const User = require('../model/user');
    const bcrypt = require('bcrypt');
    const jwt = require('jsonwebtoken');
    
    // 用户登录
    exports.login = async (req, res) => {
      const { username, password } = req.body;
    
      try {
        // 查询用户
        const user = await User.findOne({ where: { username } });
        if (!user) return res.status(400).json({ msg: '用户不存在' });
    
        // 验证密码(bcrypt比对)
        const isMatch = await bcrypt.compare(password, user.password);
        if (!isMatch) return res.status(400).json({ msg: '密码错误' });
    
        // 生成token(有效期24小时)
        const token = jwt.sign(
          { id: user.id, username: user.username },
          process.env.JWT_SECRET || 'secret_key',
          { expiresIn: '24h' }
        );
    
        res.json({
          success: true,
          token,
          user: { id: user.id, username: user.username, avatar: user.avatar }
        });
      } catch (err) {
        console.error(err);
        res.status(500).json({ msg: '服务器错误' });
      }
    };

  • WebSocket 聊天服务server/src/app.js 中集成):
    const express = require('express');
    const http = require('http');
    const WebSocket = require('ws');
    const cors = require('cors');
    const sequelize = require('./config/db');
    const auth = require('./middleware/auth');
    
    const app = express();
    const server = http.createServer(app);
    const wss = new WebSocket.Server({ server }); // 创建WebSocket服务
    
    // 中间件
    app.use(cors()); // 允许跨域
    app.use(express.json()); // 解析JSON请求
    
    // 路由示例(登录接口)
    app.post('/api/login', require('./controller/authController').login);
    
    // WebSocket连接处理(实时聊天)
    wss.on('connection', (ws) => {
      console.log('新客户端连接');
      
      // 接收消息并广播给所有客户端
      ws.on('message', (data) => {
        const message = JSON.parse(data.toString());
        // 广播消息
        wss.clients.forEach(client => {
          if (client.readyState === WebSocket.OPEN) {
            client.send(JSON.stringify(message));
          }
        });
      });
    
      ws.on('close', () => {
        console.log('客户端断开连接');
      });
    });
    
    // 启动服务
    const PORT = process.env.PORT || 3000;
    server.listen(PORT, () => {
      console.log(`服务端运行在 http://localhost:${PORT}`);
    });

  • 启动配置server/package.json 添加脚本):
    "scripts": {
      "start": "node src/app.js",
      "dev": "nodemon src/app.js"
    }
    2. 创建客户端(Vue3 + Electron)
    ① 初始化 Vue 项目并集成 Electron
    cd ..  # 返回project目录
    npm create vite@latest client -- --template vue  # 创建Vue项目
    cd client
    npm install
    npm install electron electron-builder vite-plugin-electron electron-squirrel-startup --save-dev  # Electron相关依赖
    npm install sqlite3 typeorm pinia element-plus axios vue-router video.js  # 核心依赖
    ② 配置 Vite 集成 Electron(client/vite.config.js):
    import { defineConfig } from 'vite';
    import vue from '@vitejs/plugin-vue';
    import electron from 'vite-plugin-electron';
    
    export default defineConfig({
      plugins: [
        vue(),
        electron({
          entry: 'electron/main.js'  // Electron主进程入口
        })
      ]
    });
    ③ 配置 Electron 主进程(client/electron/main.js):
    const { app, BrowserWindow, ipcMain } = require('electron');
    const path = require('path');
    const { initDB } = require('./db/connection'); // 导入SQLite初始化
    
    // 确保应用单实例运行
    if (!app.requestSingleInstanceLock()) {
      app.quit();
    }
    
    // 创建窗口函数
    function createWindow() {
      const mainWindow = new BrowserWindow({
        width: 1200,
        height: 800,
        webPreferences: {
          preload: path.join(__dirname, 'preload.js'), // 预加载脚本
          nodeIntegration: false, // 禁用节点集成(安全)
          contextIsolation: true // 启用上下文隔离
        }
      });
    
      // 开发环境加载Vite服务,生产环境加载本地文件
      if (process.env.VITE_DEV_SERVER_URL) {
        mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
        mainWindow.webContents.openDevTools(); // 开发环境打开调试工具
      } else {
        mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
      }
    }
    
    // 初始化SQLite数据库并创建窗口
    app.whenReady().then(async () => {
      await initDB(); // 初始化本地数据库
      createWindow();
    
      app.on('activate', () => {
        if (BrowserWindow.getAllWindows().length === 0) createWindow();
      });
    });
    
    // 关闭所有窗口时退出应用(macOS除外)
    app.on('window-all-closed', () => {
      if (process.platform !== 'darwin') app.quit();
    });
    
    // IPC主进程处理(示例:登录验证)
    ipcMain.handle('login', async (_, { username, password }) => {
      // 调用服务端登录接口
      try {
        const response = await fetch('http://localhost:3000/api/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ username, password })
        });
        const data = await response.json();
        
        // 登录成功后缓存用户信息到本地数据库
        if (data.success) {
          const { User } = require('./db/models/User');
          await User.upsert({  // 存在则更新,不存在则插入
            username: data.user.username,
            password: password, // 实际项目中应加密存储
            token: data.token
          });
        }
        
        return data;
      } catch (err) {
        return { success: false, msg: '接口请求失败' };
      }
    });
    ④ 配置预加载脚本(client/electron/preload.js):
    const { contextBridge, ipcRenderer } = require('electron');
    
    // 暴露有限的API给渲染进程(安全通信)
    contextBridge.exposeInMainWorld('electron', {
      ipcRenderer: {
        invoke: (channel, data) => ipcRenderer.invoke(channel, data), // 调用主进程方法
        on: (channel, callback) => ipcRenderer.on(channel, callback) // 监听主进程事件
      }
    });
    ⑤ 配置本地 SQLite 数据库(client/electron/db/connection.js):
    const { DataSource } = require('typeorm');
    const path = require('path');
    const { app } = require('electron');
    
    // 数据库存储路径(用户数据目录,避免权限问题)
    const dbPath = path.join(app.getPath('userData'), 'local.db');
    
    // 创建数据源
    const AppDataSource = new DataSource({
      type: 'sqlite',
      database: dbPath,
      entities: [path.join(__dirname, 'models/*.js')], // 数据模型路径
      synchronize: true, // 开发环境自动同步表结构(生产环境建议关闭)
      logging: false // 关闭日志
    });
    
    // 初始化数据库连接
    exports.initDB = async () => {
      if (!AppDataSource.isInitialized) {
        await AppDataSource.initialize();
        console.log('本地SQLite数据库初始化成功');
      }
    };
    
    exports.AppDataSource = AppDataSource;
    ⑥ 创建本地用户模型(client/electron/db/models/User.js):
    const { Entity, Column, PrimaryColumn } = require('typeorm');
    
    @Entity()
    exports.User = class User {
      @PrimaryColumn() // 用户名作为主键
      username;
    
      @Column() // 加密后的密码
      password;
    
      @Column({ default: '' }) // 登录token
      token;
    
      @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) // 最后登录时间
      lastLogin;
    };
    ⑦ 配置 Vue 路由与鉴权(client/src/router/index.js):
    import { createRouter, createWebHashHistory } from 'vue-router';
    import { useUserStore } from '../stores/user';
    
    const routes = [
      {
        path: '/login',
        component: () => import('../views/Login.vue'),
        meta: { noAuth: true } // 无需登录
      },
      {
        path: '/',
        component: () => import('../views/Home.vue'),
        meta: { requiresAuth: true }, // 需要登录
        children: [
          { path: 'chat', component: () => import('../views/Chat.vue') },
          { path: 'media', component: () => import('../views/Media.vue') },
          { path: 'table', component: () => import('../views/Table.vue') }
        ]
      }
    ];
    
    const router = createRouter({
      history: createWebHashHistory(),
      routes
    });
    
    // 路由守卫:未登录拦截
    router.beforeEach((to, from, next) => {
      const userStore = useUserStore();
      // 需要登录但未登录时,跳转登录页
      if (to.meta.requiresAuth && !userStore.isLogin) {
        next('/login');
      } else {
        next();
      }
    });
    
    export default router;
    ⑧ 登录页面实现(client/src/views/Login.vue):
    <template>
      <el-form :model="form" label-width="80px" @submit.prevent="handleLogin">
        <el-form-item label="用户名">
          <el-input v-model="form.username"></el-input>
        </el-form-item>
        <el-form-item label="密码">
          <el-input v-model="form.password" type="password"></el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleLogin">登录</el-button>
        </el-form-item>
      </el-form>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    import { useRouter } from 'vue-router';
    import { useUserStore } from '../stores/user';
    
    const form = ref({ username: '', password: '' });
    const router = useRouter();
    const userStore = useUserStore();
    
    // 登录处理
    const handleLogin = async () => {
      // 调用主进程的登录接口
      const result = await window.electron.ipcRenderer.invoke('login', form.value);
      if (result.success) {
        // 登录成功,保存状态到Pinia
        userStore.setUser({
          username: result.user.username,
          token: result.token
        });
        router.push('/chat'); // 跳转到聊天页
      } else {
        alert(result.msg);
      }
    };
    </script>
    ⑨ 聊天功能实现(client/src/views/Chat.vue):
    <template>
      <div class="chat-container">
        <div class="messages">
          <div v-for="msg in messages" :key="msg.id">
            <strong>{{ msg.username }}:</strong> {{ msg.content }}
          </div>
        </div>
        <el-input 
          v-model="message" 
          placeholder="输入消息" 
          @keyup.enter="sendMessage"
        ></el-input>
      </div>
    </template>
    
    <script setup>
    import { ref, onMounted } from 'vue';
    import { useUserStore } from '../stores/user';
    
    const userStore = useUserStore();
    const messages = ref([]);
    const message = ref('');
    let ws = null;
    
    // 初始化WebSocket连接
    onMounted(() => {
      // 连接服务端WebSocket(带token鉴权)
      ws = new WebSocket(`ws://localhost:3000?token=${userStore.token}`);
      
      // 接收消息
      ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        messages.value.push(data);
        // 保存到本地数据库
        window.electron.ipcRenderer.invoke('saveChatMessage', data);
      };
    
      // 加载本地历史消息
      loadLocalMessages();
    });
    
    // 发送消息
    const sendMessage = () => {
      if (!message.value) return;
      const msg = {
        id: Date.now(),
        username: userStore.user.username,
        content: message.value,
        time: new Date().toISOString()
      };
      ws.send(JSON.stringify(msg)); // 发送到服务端
      message.value = '';
    };
    
    // 加载本地缓存的聊天记录
    const loadLocalMessages = async () => {
      const localMsgs = await window.electron.ipcRenderer.invoke('getChatMessages');
      messages.value = localMsgs;
    };
    </script>

    四、运行与部署

    1. 运行服务端
    cd server
    npm run dev  # 启动开发服务器(默认3000端口)
    2. 运行客户端(开发模式)
    cd client
    npm run dev  # 启动Vue + Electron开发环境
    3. 打包客户端(生成桌面应用)
  • 配置client/package.json
    "scripts": {
      "dev": "vite",
      "build": "vite build && electron-builder"
    },
    "build": {
      "appId": "com.example.electronapp",
      "productName": "MyElectronApp",
      "directories": {
        "output": "dist-electron"
      },
      "win": {
        "target": "nsis"  // Windows安装包
      },
      "mac": {
        "target": "dmg"   // macOS安装包
      },
      "linux": {
        "target": "deb"   // Linux安装包
      }
    }

  • 执行打包:
    cd client
    npm run build  # 生成安装包(在dist-electron目录)

    五、核心功能说明

  • 数据同步策略

    • 本地优先:查询数据时先读 SQLite,提升响应速度
    • 后台同步:定期调用接口拉取更新,差异数据写入本地
    • 提交更新:修改数据时先更本地,再异步同步到服务端
  • 鉴权流程

    • 客户端登录 → 服务端验证 → 返回 JWT token
    • token 存储在本地 SQLite + Pinia
    • 接口请求时通过 Axios 拦截器自动添加 token
    • 路由守卫拦截未登录状态
  • 多媒体处理

    • 图片:通过 FileReader 转 Base64,存储本地或上传服务端
    • 音视频:使用 video.js 播放,支持本地文件和网络地址
    • 聊天:WebSocket 实时通信,消息本地缓存 + 服务端同步
  • 安全问题

    • 密码必须加密存储(bcrypt)
    • 避免在渲染进程直接操作文件系统,通过 IPC 由主进程处理
    • JWT 密钥不要硬编码,使用环境变量
  • 性能优化

    • SQLite 大量数据查询需添加索引
    • 大文件(音视频)建议分片上传
    • 聊天记录分页加载,避免一次性加载过多
  • 跨平台兼容

    • 文件路径处理使用path模块,避免硬编码斜杠
    • 不同系统的权限差异(如 macOS 的沙箱机制)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值