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, // 头像URLdefaultValue: ''} });// 同步表结构(开发环境) 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]; // 从请求头获取tokenif (!token) return res.status(401).json({ msg: '未登录' });try {// 验证token(密钥从环境变量获取)const decoded = jwt.verify(token, process.env.JWT_SECRET || 'secret_key');req.user = decoded; // 将用户信息挂载到reqnext();} 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: '' }) // 登录tokentoken;@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) {// 登录成功,保存状态到PiniauserStore.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 的沙箱机制)
- 文件路径处理使用
