当前位置: 首页 > news >正文

Vue项目中的Electron桌面应用开发实践指南

Vue项目中的Electron桌面应用开发实践指南

文章目录

  • Vue项目中的Electron桌面应用开发实践指南
    • 1. 引言
      • 为什么选择 Electron + Vue?
    • 2. Electron基础概念
      • 2.1 Electron架构
      • 2.2 Electron与Vue的结合
    • 3. Vue项目集成Electron
      • 3.1 集成方式
      • 3.2 技术栈选择
    • 4. 项目初始化与配置
      • 4.1 使用Vue CLI插件
      • 4.2 手动集成方式
        • 4.2.1 安装依赖
        • 4.2.2 项目结构调整
        • 4.2.3 基础配置文件
      • 4.3 主进程配置
      • 4.4 预加载脚本
    • 5. 主进程与渲染进程通信
      • 5.1 IPC通信基础
      • 5.2 设置IPC通信
      • 5.3 Vue组件中使用IPC
    • 6. 窗口管理与菜单系统
      • 6.1 自定义菜单
      • 6.2 多窗口管理
      • 6.3 窗口控制组件
    • 7. 文件系统与本地存储
      • 7.1 文件系统操作
      • 7.2 本地存储管理
    • 8. 应用打包与发布
      • 8.1 Electron Builder 配置
      • 8.2 打包脚本
      • 8.3 自动更新
    • 9. 性能优化最佳实践
      • 9.1 渲染进程优化
      • 9.2 主进程优化
      • 9.3 构建优化
    • 10. 常见问题与解决方案
      • 10.1 构建问题
        • 问题1:原生模块编译失败
        • 问题2:打包后应用白屏
      • 10.2 运行时问题
        • 问题3:require is not defined
        • 问题4:CORS 跨域问题
      • 10.3 性能问题
        • 问题5:应用启动慢
      • 10.4 调试技巧
    • 11. 实战案例分析
      • 11.1 代码编辑器应用
      • 11.2 数据库管理工具
    • 12. 总结与展望
      • 12.1 Electron + Vue 的优势
      • 12.2 面临的挑战
      • 12.3 最佳实践建议
      • 12.4 未来发展趋势
      • 12.5 学习资源推荐

深入探讨如何在 Vue 项目中集成 Electron,构建跨平台桌面应用

1. 引言

随着桌面应用需求的不断增长,Electron 作为构建跨平台桌面应用的利器,结合 Vue.js 的组件化开发优势,为开发者提供了一套完整的桌面应用解决方案。本文将详细介绍如何在 Vue 项目中集成 Electron,从基础概念到实际项目开发,帮助开发者快速掌握 Electron + Vue 的开发技巧。

为什么选择 Electron + Vue?

  • 跨平台支持:一套代码,支持 Windows、macOS、Linux
  • Web技术栈:使用熟悉的 HTML/CSS/JavaScript 技术
  • Vue生态:丰富的 Vue 组件和工具链
  • 开发效率:快速开发和迭代
  • 原生能力:访问操作系统原生 API

2. Electron基础概念

2.1 Electron架构

Electron 应用由两个主要进程组成:

  1. 主进程(Main Process)

    • 控制应用生命周期
    • 管理原生 GUI(窗口、菜单等)
    • 运行在 Node.js 环境中
  2. 渲染进程(Renderer Process)

    • 负责 UI 渲染
    • 运行 Web 页面
    • 每个窗口对应一个渲染进程
// 主进程示例 (main.js)
const { app, BrowserWindow } = require('electron');function createWindow() {const win = new BrowserWindow({width: 1200,height: 800,webPreferences: {nodeIntegration: true,contextIsolation: false}});win.loadFile('index.html');
}app.whenReady().then(createWindow);

2.2 Electron与Vue的结合

Vue 作为渲染进程的 UI 框架,负责界面展示和用户交互,而 Electron 提供原生桌面应用的能力,两者结合可以创建功能丰富的桌面应用。

3. Vue项目集成Electron

3.1 集成方式

主要有两种集成方式:

  1. 在现有 Vue 项目中添加 Electron
  2. 使用脚手架工具创建新项目

3.2 技术栈选择

  • Vue 3 + Vite:现代化的构建工具,开发体验更好
  • Vue 2 + Webpack:成熟的生态系统,稳定性高
  • TypeScript:类型安全,更好的开发体验

4. 项目初始化与配置

4.1 使用Vue CLI插件

# 安装 Vue CLI(如果尚未安装)
npm install -g @vue/cli# 创建 Vue 项目
vue create electron-vue-app# 进入项目目录
cd electron-vue-app# 添加 Electron 插件
vue add electron-builder# 选择 Electron 版本
? Choose Electron Version (Use arrow keys)❯ ^13.0.0^12.0.0^11.0.0

4.2 手动集成方式

4.2.1 安装依赖
# 安装 Electron
npm install electron --save-dev# 安装 Electron Builder(打包工具)
npm install electron-builder --save-dev# 安装 concurrently(同时运行多个命令)
npm install concurrently --save-dev# 安装 wait-on(等待资源准备就绪)
npm install wait-on --save-dev
4.2.2 项目结构调整
electron-vue-app/
├── public/
│   ├── index.html
│   └── electron-icon.png
├── src/
│   ├── main/           # Electron 主进程
│   │   ├── index.js
│   │   ├── menu.js
│   │   └── ipc.js
│   ├── renderer/       # Vue 渲染进程
│   │   ├── main.js
│   │   ├── App.vue
│   │   └── components/
│   └── preload/        # 预加载脚本
│       └── preload.js
├── package.json
├── vue.config.js
└── electron-builder.json
4.2.3 基础配置文件

package.json

{"name": "electron-vue-app","version": "1.0.0","description": "Vue + Electron Desktop App","main": "dist_electron/index.js","scripts": {"serve": "vue-cli-service serve","build": "vue-cli-service build","electron:serve": "concurrently \"npm run serve\" \"wait-on http://localhost:8080 && electron .\"","electron:build": "vue-cli-service build && electron-builder","postinstall": "electron-builder install-app-deps"},"dependencies": {"vue": "^3.2.0","vue-router": "^4.0.0","vuex": "^4.0.0"},"devDependencies": {"@vue/cli-plugin-router": "~5.0.0","@vue/cli-plugin-vuex": "~5.0.0","@vue/cli-service": "~5.0.0","electron": "^18.0.0","electron-builder": "^23.0.0","concurrently": "^7.0.0","wait-on": "^6.0.0"}
}

vue.config.js

const { defineConfig } = require('@vue/cli-service');module.exports = defineConfig({transpileDependencies: true,pluginOptions: {electronBuilder: {nodeIntegration: true,contextIsolation: false,customFileProtocol: './',builderOptions: {appId: 'com.example.electron-vue-app',productName: 'Electron Vue App',directories: {output: 'dist_electron'},files: ['dist/**/*','dist_electron/**/*'],mac: {icon: 'build/icon.icns'},win: {icon: 'build/icon.ico'},linux: {icon: 'build/icon.png'}}}}
});

4.3 主进程配置

src/main/index.js

const { app, BrowserWindow, Menu } = require('electron');
const path = require('path');
const { createMenu } = require('./menu');
const { setupIPC } = require('./ipc');// 保持窗口对象的全局引用
let mainWindow;function createWindow() {// 创建浏览器窗口mainWindow = new BrowserWindow({width: 1200,height: 800,minWidth: 800,minHeight: 600,webPreferences: {nodeIntegration: true,contextIsolation: false,enableRemoteModule: true,preload: path.join(__dirname, '../preload/preload.js')},icon: path.join(__dirname, '../../build/icon.png'),show: false, // 先不显示窗口titleBarStyle: 'default'});// 加载应用界面const isDevelopment = process.env.NODE_ENV === 'development';if (isDevelopment) {mainWindow.loadURL('http://localhost:8080');mainWindow.webContents.openDevTools();} else {mainWindow.loadFile(path.join(__dirname, '../../dist/index.html'));}// 窗口准备就绪时显示mainWindow.once('ready-to-show', () => {mainWindow.show();});// 窗口关闭事件mainWindow.on('closed', () => {mainWindow = null;});// 创建菜单createMenu();// 设置 IPC 通信setupIPC();
}// 应用准备就绪时创建窗口
app.whenReady().then(() => {createWindow();app.on('activate', () => {if (BrowserWindow.getAllWindows().length === 0) {createWindow();}});
});// 所有窗口关闭时退出应用
app.on('window-all-closed', () => {if (process.platform !== 'darwin') {app.quit();}
});// 安全:在加载页面之前设置权限
app.on('web-contents-created', (event, contents) => {contents.on('will-navigate', (event, navigationUrl) => {const parsedUrl = new URL(navigationUrl);if (parsedUrl.origin !== 'http://localhost:8080') {event.preventDefault();}});
});

4.4 预加载脚本

src/preload/preload.js

const { contextBridge, ipcRenderer } = require('electron');// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {// 窗口控制minimizeWindow: () => ipcRenderer.send('minimize-window'),maximizeWindow: () => ipcRenderer.send('maximize-window'),closeWindow: () => ipcRenderer.send('close-window'),// 文件操作readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),writeFile: (filePath, data) => ipcRenderer.invoke('write-file', filePath, data),// 应用信息getAppVersion: () => ipcRenderer.invoke('get-app-version'),// 监听事件onUpdateAvailable: (callback) => ipcRenderer.on('update-available', callback),onDownloadProgress: (callback) => ipcRenderer.on('download-progress', callback)
});

5. 主进程与渲染进程通信

5.1 IPC通信基础

Electron 使用 IPC(Inter-Process Communication)机制实现主进程和渲染进程之间的通信。

5.2 设置IPC通信

src/main/ipc.js

const { ipcMain, dialog, app } = require('electron');
const fs = require('fs').promises;
const path = require('path');function setupIPC() {// 窗口控制ipcMain.on('minimize-window', (event) => {const window = require('./index').getMainWindow();window.minimize();});ipcMain.on('maximize-window', (event) => {const window = require('./index').getMainWindow();if (window.isMaximized()) {window.unmaximize();} else {window.maximize();}});ipcMain.on('close-window', (event) => {const window = require('./index').getMainWindow();window.close();});// 文件操作 - 读取文件ipcMain.handle('read-file', async (event, filePath) => {try {const data = await fs.readFile(filePath, 'utf-8');return { success: true, data };} catch (error) {return { success: false, error: error.message };}});// 文件操作 - 写入文件ipcMain.handle('write-file', async (event, filePath, data) => {try {await fs.writeFile(filePath, data, 'utf-8');return { success: true };} catch (error) {return { success: false, error: error.message };}});// 文件对话框ipcMain.handle('show-open-dialog', async (event, options) => {const window = require('./index').getMainWindow();const result = await dialog.showOpenDialog(window, options);return result;});ipcMain.handle('show-save-dialog', async (event, options) => {const window = require('./index').getMainWindow();const result = await dialog.showSaveDialog(window, options);return result;});// 应用信息ipcMain.handle('get-app-version', async (event) => {return app.getVersion();});// 获取应用数据路径ipcMain.handle('get-app-data-path', async (event) => {return app.getPath('userData');});// 数据库操作(使用 SQLite)ipcMain.handle('db-query', async (event, query, params = []) => {const db = require('./database');try {const result = await db.query(query, params);return { success: true, data: result };} catch (error) {return { success: false, error: error.message };}});
}module.exports = { setupIPC };

5.3 Vue组件中使用IPC

src/renderer/components/FileManager.vue

<template><div class="file-manager"><div class="toolbar"><button @click="openFile" class="btn btn-primary">打开文件</button><button @click="saveFile" class="btn btn-success">保存文件</button><button @click="createNewFile" class="btn btn-info">新建文件</button></div><div class="editor-container"><textarea v-model="fileContent" class="file-editor"placeholder="在这里编辑文件内容..."></textarea></div><div class="status-bar"><span>{{ currentFile || '未选择文件' }}</span><span>{{ fileContent.length }} 字符</span></div></div>
</template><script>
import { ref } from 'vue';
import { ElMessage } from 'element-plus';export default {name: 'FileManager',setup() {const fileContent = ref('');const currentFile = ref('');const isModified = ref(false);// 打开文件const openFile = async () => {try {const result = await window.electronAPI.showOpenDialog({title: '选择文件',filters: [{ name: '文本文件', extensions: ['txt', 'md', 'js', 'json'] },{ name: '所有文件', extensions: ['*'] }],properties: ['openFile']});if (!result.canceled && result.filePaths.length > 0) {const filePath = result.filePaths[0];const readResult = await window.electronAPI.readFile(filePath);if (readResult.success) {fileContent.value = readResult.data;currentFile.value = filePath;isModified.value = false;ElMessage.success('文件加载成功');} else {ElMessage.error('读取文件失败: ' + readResult.error);}}} catch (error) {ElMessage.error('打开文件失败: ' + error.message);}};// 保存文件const saveFile = async () => {try {let filePath = currentFile.value;// 如果没有当前文件,显示保存对话框if (!filePath) {const result = await window.electronAPI.showSaveDialog({title: '保存文件',filters: [{ name: '文本文件', extensions: ['txt'] },{ name: 'Markdown', extensions: ['md'] },{ name: 'JavaScript', extensions: ['js'] },{ name: 'JSON', extensions: ['json'] },{ name: '所有文件', extensions: ['*'] }]});if (result.canceled) {return;}filePath = result.filePath;}const writeResult = await window.electronAPI.writeFile(filePath, fileContent.value);if (writeResult.success) {currentFile.value = filePath;isModified.value = false;ElMessage.success('文件保存成功');} else {ElMessage.error('保存文件失败: ' + writeResult.error);}} catch (error) {ElMessage.error('保存文件失败: ' + error.message);}};// 创建新文件const createNewFile = () => {if (isModified.value) {ElMessage.warning('请先保存当前文件');return;}fileContent.value = '';currentFile.value = '';isModified.value = false;ElMessage.info('已创建新文件');};// 监听内容变化const handleContentChange = () => {isModified.value = true;};return {fileContent,currentFile,openFile,saveFile,createNewFile,handleContentChange};}
};
</script><style scoped>
.file-manager {display: flex;flex-direction: column;height: 100%;background: #f5f5f5;
}.toolbar {display: flex;gap: 10px;padding: 10px;background: white;border-bottom: 1px solid #ddd;
}.btn {padding: 8px 16px;border: none;border-radius: 4px;cursor: pointer;font-size: 14px;transition: all 0.3s;
}.btn-primary {background: #007bff;color: white;
}.btn-primary:hover {background: #0056b3;
}.btn-success {background: #28a745;color: white;
}.btn-success:hover {background: #1e7e34;
}.btn-info {background: #17a2b8;color: white;
}.btn-info:hover {background: #138496;
}.editor-container {flex: 1;padding: 10px;overflow: hidden;
}.file-editor {width: 100%;height: 100%;border: 1px solid #ddd;border-radius: 4px;padding: 15px;font-family: 'Consolas', 'Monaco', monospace;font-size: 14px;line-height: 1.5;resize: none;outline: none;
}.status-bar {display: flex;justify-content: space-between;padding: 8px 15px;background: white;border-top: 1px solid #ddd;font-size: 12px;color: #666;
}
</style>

6. 窗口管理与菜单系统

6.1 自定义菜单

src/main/menu.js

const { Menu, shell } = require('electron');function createMenu() {const template = [{label: '文件',submenu: [{label: '新建',accelerator: 'CmdOrCtrl+N',click: (menuItem, browserWindow) => {browserWindow.webContents.send('menu-new-file');}},{label: '打开',accelerator: 'CmdOrCtrl+O',click: (menuItem, browserWindow) => {browserWindow.webContents.send('menu-open-file');}},{label: '保存',accelerator: 'CmdOrCtrl+S',click: (menuItem, browserWindow) => {browserWindow.webContents.send('menu-save-file');}},{ type: 'separator' },{label: '退出',accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',click: () => {require('electron').app.quit();}}]},{label: '编辑',submenu: [{ role: 'undo' },{ role: 'redo' },{ type: 'separator' },{ role: 'cut' },{ role: 'copy' },{ role: 'paste' },{ role: 'selectall' }]},{label: '视图',submenu: [{ role: 'reload' },{ role: 'forceReload' },{ role: 'toggleDevTools' },{ type: 'separator' },{ role: 'resetZoom' },{ role: 'zoomIn' },{ role: 'zoomOut' },{ type: 'separator' },{ role: 'togglefullscreen' }]},{label: '窗口',submenu: [{ role: 'minimize' },{ role: 'close' }]},{label: '帮助',submenu: [{label: '关于',click: (menuItem, browserWindow) => {require('electron').dialog.showMessageBox(browserWindow, {type: 'info',title: '关于',message: 'Electron Vue App',detail: `版本: 1.0.0\nElectron: ${process.versions.electron}\nNode.js: ${process.versions.node}`});}},{label: '开发者工具',accelerator: 'F12',click: (menuItem, browserWindow) => {browserWindow.webContents.toggleDevTools();}},{label: '访问官网',click: () => {shell.openExternal('https://electronjs.org');}}]}];// macOS 特殊处理if (process.platform === 'darwin') {template.unshift({label: app.getName(),submenu: [{ role: 'about' },{ type: 'separator' },{ role: 'services' },{ type: 'separator' },{ role: 'hide' },{ role: 'hideothers' },{ role: 'unhide' },{ type: 'separator' },{ role: 'quit' }]});}const menu = Menu.buildFromTemplate(template);Menu.setApplicationMenu(menu);
}module.exports = { createMenu };

6.2 多窗口管理

src/main/windowManager.js

const { BrowserWindow } = require('electron');
const path = require('path');class WindowManager {constructor() {this.windows = new Map();this.windowIdCounter = 0;}createWindow(options = {}) {const windowId = ++this.windowIdCounter;const defaultOptions = {width: 1200,height: 800,minWidth: 600,minHeight: 400,webPreferences: {nodeIntegration: true,contextIsolation: false,enableRemoteModule: true,preload: path.join(__dirname, '../preload/preload.js')},show: false,titleBarStyle: 'default'};const windowOptions = { ...defaultOptions, ...options };const window = new BrowserWindow(windowOptions);// 加载页面const isDevelopment = process.env.NODE_ENV === 'development';if (isDevelopment) {window.loadURL('http://localhost:8080');} else {window.loadFile(path.join(__dirname, '../../dist/index.html'));}// 窗口事件window.once('ready-to-show', () => {window.show();});window.on('closed', () => {this.windows.delete(windowId);});// 保存窗口引用this.windows.set(windowId, window);return { windowId, window };}getWindow(windowId) {return this.windows.get(windowId);}getAllWindows() {return Array.from(this.windows.values());}closeWindow(windowId) {const window = this.windows.get(windowId);if (window) {window.close();this.windows.delete(windowId);}}closeAllWindows() {this.windows.forEach((window) => {window.close();});this.windows.clear();}sendToWindow(windowId, channel, ...args) {const window = this.windows.get(windowId);if (window && !window.isDestroyed()) {window.webContents.send(channel, ...args);}}broadcast(channel, ...args) {this.windows.forEach((window) => {if (!window.isDestroyed()) {window.webContents.send(channel, ...args);}});}
}module.exports = WindowManager;

6.3 窗口控制组件

src/renderer/components/WindowControls.vue

<template><div class="window-controls"><div class="control-btn minimize" @click="minimizeWindow"><svg width="12" height="12" viewBox="0 0 12 12"><rect x="1" y="6" width="10" height="1" fill="currentColor"/></svg></div><div class="control-btn maximize" @click="maximizeWindow"><svg width="12" height="12" viewBox="0 0 12 12"><rect x="1" y="1" width="10" height="10" fill="none" stroke="currentColor" stroke-width="1"/></svg></div><div class="control-btn close" @click="closeWindow"><svg width="12" height="12" viewBox="0 0 12 12"><path d="M1 1 L11 11 M11 1 L1 11" stroke="currentColor" stroke-width="1"/></svg></div></div>
</template><script>
export default {name: 'WindowControls',methods: {minimizeWindow() {window.electronAPI.minimizeWindow();},maximizeWindow() {window.electronAPI.maximizeWindow();},closeWindow() {window.electronAPI.closeWindow();}}
};
</script><style scoped>
.window-controls {display: flex;align-items: center;gap: 8px;padding: 8px;
}.control-btn {width: 30px;height: 30px;border-radius: 50%;display: flex;align-items: center;justify-content: center;cursor: pointer;transition: all 0.3s ease;
}.control-btn:hover {transform: scale(1.1);
}.control-btn.minimize {background: #ffbd2e;color: #333;
}.control-btn.maximize {background: #28ca42;color: #333;
}.control-btn.close {background: #ff5f57;color: white;
}.control-btn svg {opacity: 0;transition: opacity 0.3s ease;
}.control-btn:hover svg {opacity: 1;
}
</style>

7. 文件系统与本地存储

7.1 文件系统操作

src/main/fileManager.js

const fs = require('fs').promises;
const path = require('path');
const { app } = require('electron');class FileManager {constructor() {this.appDataPath = app.getPath('userData');this.recentFiles = [];this.maxRecentFiles = 10;}async readFile(filePath) {try {const data = await fs.readFile(filePath, 'utf-8');await this.addToRecentFiles(filePath);return { success: true, data };} catch (error) {return { success: false, error: error.message };}}async writeFile(filePath, data) {try {await fs.writeFile(filePath, data, 'utf-8');await this.addToRecentFiles(filePath);return { success: true };} catch (error) {return { success: false, error: error.message };}}async createDirectory(dirPath) {try {await fs.mkdir(dirPath, { recursive: true });return { success: true };} catch (error) {return { success: false, error: error.message };}}async readDirectory(dirPath) {try {const files = await fs.readdir(dirPath, { withFileTypes: true });const fileList = files.map(file => ({name: file.name,isDirectory: file.isDirectory(),path: path.join(dirPath, file.name),size: file.isDirectory() ? 0 : null,modified: null}));// 获取文件详细信息for (const file of fileList) {if (!file.isDirectory) {try {const stats = await fs.stat(file.path);file.size = stats.size;file.modified = stats.mtime;} catch (error) {console.error(`获取文件信息失败: ${file.path}`, error);}}}return { success: true, data: fileList };} catch (error) {return { success: false, error: error.message };}}async deleteFile(filePath) {try {await fs.unlink(filePath);await this.removeFromRecentFiles(filePath);return { success: true };} catch (error) {return { success: false, error: error.message };}}async deleteDirectory(dirPath) {try {await fs.rmdir(dirPath, { recursive: true });return { success: true };} catch (error) {return { success: false, error: error.message };}}async copyFile(sourcePath, destPath) {try {await fs.copyFile(sourcePath, destPath);return { success: true };} catch (error) {return { success: false, error: error.message };}}async moveFile(sourcePath, destPath) {try {await fs.rename(sourcePath, destPath);await this.updateRecentFilePath(sourcePath, destPath);return { success: true };} catch (error) {return { success: false, error: error.message };}}async getFileInfo(filePath) {try {const stats = await fs.stat(filePath);return {success: true,data: {size: stats.size,modified: stats.mtime,created: stats.birthtime,isDirectory: stats.isDirectory(),isFile: stats.isFile()}};} catch (error) {return { success: false, error: error.message };}}async addToRecentFiles(filePath) {// 移除已存在的相同文件this.recentFiles = this.recentFiles.filter(file => file !== filePath);// 添加到开头this.recentFiles.unshift(filePath);// 限制数量if (this.recentFiles.length > this.maxRecentFiles) {this.recentFiles = this.recentFiles.slice(0, this.maxRecentFiles);}// 保存到配置文件await this.saveRecentFiles();}async removeFromRecentFiles(filePath) {this.recentFiles = this.recentFiles.filter(file => file !== filePath);await this.saveRecentFiles();}async updateRecentFilePath(oldPath, newPath) {const index = this.recentFiles.indexOf(oldPath);if (index !== -1) {this.recentFiles[index] = newPath;await this.saveRecentFiles();}}async saveRecentFiles() {const configPath = path.join(this.appDataPath, 'recent-files.json');try {await fs.writeFile(configPath, JSON.stringify(this.recentFiles, null, 2));} catch (error) {console.error('保存最近文件列表失败:', error);}}async loadRecentFiles() {const configPath = path.join(this.appDataPath, 'recent-files.json');try {const data = await fs.readFile(configPath, 'utf-8');this.recentFiles = JSON.parse(data);} catch (error) {this.recentFiles = [];}}getRecentFiles() {return this.recentFiles;}
}module.exports = FileManager;

7.2 本地存储管理

src/renderer/utils/storage.js

class StorageManager {constructor() {this.dbName = 'electron-vue-app';this.version = 1;this.db = null;}// 初始化 IndexedDBasync initDB() {return new Promise((resolve, reject) => {const request = indexedDB.open(this.dbName, this.version);request.onerror = () => {reject(request.error);};request.onsuccess = () => {this.db = request.result;resolve(this.db);};request.onupgradeneeded = (event) => {const db = event.target.result;// 创建存储对象if (!db.objectStoreNames.contains('settings')) {const settingsStore = db.createObjectStore('settings', { keyPath: 'key' });settingsStore.createIndex('key', 'key', { unique: true });}if (!db.objectStoreNames.contains('documents')) {const documentsStore = db.createObjectStore('documents', { keyPath: 'id', autoIncrement: true });documentsStore.createIndex('title', 'title', { unique: false });documentsStore.createIndex('created', 'created', { unique: false });}if (!db.objectStoreNames.contains('cache')) {const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });cacheStore.createIndex('key', 'key', { unique: true });cacheStore.createIndex('expiry', 'expiry', { unique: false });}};});}// 设置值async setItem(storeName, key, value) {if (!this.db) await this.initDB();return new Promise((resolve, reject) => {const transaction = this.db.transaction([storeName], 'readwrite');const store = transaction.objectStore(storeName);const request = store.put({ key, value, timestamp: Date.now() });request.onsuccess = () => {resolve(request.result);};request.onerror = () => {reject(request.error);};});}// 获取值async getItem(storeName, key) {if (!this.db) await this.initDB();return new Promise((resolve, reject) => {const transaction = this.db.transaction([storeName], 'readonly');const store = transaction.objectStore(storeName);const request = store.get(key);request.onsuccess = () => {const result = request.result;resolve(result ? result.value : null);};request.onerror = () => {reject(request.error);};});}// 删除值async removeItem(storeName, key) {if (!this.db) await this.initDB();return new Promise((resolve, reject) => {const transaction = this.db.transaction([storeName], 'readwrite');const store = transaction.objectStore(storeName);const request = store.delete(key);request.onsuccess = () => {resolve();};request.onerror = () => {reject(request.error);};});}// 获取所有值async getAllItems(storeName) {if (!this.db) await this.initDB();return new Promise((resolve, reject) => {const transaction = this.db.transaction([storeName], 'readonly');const store = transaction.objectStore(storeName);const request = store.getAll();request.onsuccess = () => {resolve(request.result);};request.onerror = () => {reject(request.error);};});}// 缓存操作(带过期时间)async setCache(key, value, expiryMinutes = 60) {const expiry = Date.now() + (expiryMinutes * 60 * 1000);return this.setItem('cache', key, { value, expiry });}async getCache(key) {const cached = await this.getItem('cache', key);if (!cached) return null;if (Date.now() > cached.expiry) {await this.removeItem('cache', key);return null;}return cached.value;}// 清理过期缓存async clearExpiredCache() {if (!this.db) await this.initDB();return new Promise((resolve, reject) => {const transaction = this.db.transaction(['cache'], 'readwrite');const store = transaction.objectStore('cache');const index = store.index('expiry');const now = Date.now();const range = IDBKeyRange.upperBound(now);const request = index.openCursor(range);request.onsuccess = (event) => {const cursor = event.target.result;if (cursor) {cursor.delete();cursor.continue();} else {resolve();}};request.onerror = () => {reject(request.error);};});}
}// 创建单例实例
const storageManager = new StorageManager();// 初始化数据库
storageManager.initDB().catch(console.error);export default storageManager;

8. 应用打包与发布

8.1 Electron Builder 配置

electron-builder.json

{"appId": "com.example.electron-vue-app","productName": "Electron Vue Desktop App","directories": {"output": "dist_electron"},"files": ["dist/**/*","dist_electron/**/*","node_modules/**/*","package.json"],"mac": {"icon": "build/icon.icns","category": "public.app-category.productivity","target": [{"target": "dmg","arch": ["x64", "arm64"]},{"target": "zip","arch": ["x64", "arm64"]}]},"win": {"icon": "build/icon.ico","target": [{"target": "nsis","arch": ["x64", "ia32"]},{"target": "portable","arch": ["x64", "ia32"]}]},"linux": {"icon": "build/icon.png","target": [{"target": "AppImage","arch": ["x64"]},{"target": "deb","arch": ["x64"]}],"category": "Utility"},"dmg": {"title": "${productName} ${version}","icon": "build/icon.icns","iconSize": 100,"contents": [{"x": 380,"y": 240,"type": "link","path": "/Applications"},{"x": 110,"y": 240,"type": "file"}]},"nsis": {"oneClick": false,"allowToChangeInstallationDirectory": true,"createDesktopShortcut": true,"createStartMenuShortcut": true,"shortcutName": "${productName}"},"publish": {"provider": "github","owner": "your-github-username","repo": "your-repo-name"}
}

8.2 打包脚本

scripts/build.js

const { build } = require('electron-builder');
const path = require('path');const config = {config: {appId: 'com.example.electron-vue-app',productName: 'Electron Vue Desktop App',directories: {output: 'dist_electron'},files: ['dist/**/*','dist_electron/**/*','node_modules/**/*','package.json'],extraMetadata: {main: 'dist_electron/index.js'}}
};async function buildApp() {try {console.log('开始构建应用...');// 构建 Vue 应用console.log('构建 Vue 应用...');await require('child_process').execSync('npm run build', { stdio: 'inherit' });// 构建 Electron 应用console.log('构建 Electron 应用...');await build({...config,win: ['nsis', 'portable'],mac: ['dmg', 'zip'],linux: ['AppImage', 'deb']});console.log('构建完成!');} catch (error) {console.error('构建失败:', error);process.exit(1);}
}// 如果直接运行此脚本
if (require.main === module) {buildApp();
}module.exports = { buildApp };

8.3 自动更新

src/main/updater.js

const { app, autoUpdater, dialog } = require('electron');
const { ipcMain } = require('electron');class AppUpdater {constructor() {this.updateURL = 'https://your-update-server.com/updates';this.setupAutoUpdater();}setupAutoUpdater() {const server = 'https://your-update-server.com';const url = `${server}/update/${process.platform}/${app.getVersion()}`;autoUpdater.setFeedURL({ url });// 自动更新事件autoUpdater.on('checking-for-update', () => {console.log('正在检查更新...');});autoUpdater.on('update-available', (info) => {console.log('发现可用更新:', info);dialog.showMessageBox({type: 'info',title: '发现更新',message: '发现新版本,是否现在更新?',buttons: ['现在更新', '稍后提醒']}).then(result => {if (result.response === 0) {autoUpdater.downloadUpdate();}});});autoUpdater.on('update-not-available', (info) => {console.log('当前为最新版本:', info);});autoUpdater.on('error', (err) => {console.log('更新错误:', err);dialog.showErrorBox('更新错误', '检查更新时发生错误: ' + err.message);});autoUpdater.on('download-progress', (progressObj) => {let log_message = `下载速度: ${progressObj.bytesPerSecond}`;log_message += ` - 已下载 ${progressObj.percent}%`;log_message += ` (${progressObj.transferred}/${progressObj.total})`;console.log(log_message);// 通知渲染进程下载进度const windows = require('./index').getAllWindows();windows.forEach(window => {window.webContents.send('download-progress', progressObj);});});autoUpdater.on('update-downloaded', (info) => {console.log('更新下载完成:', info);dialog.showMessageBox({type: 'info',title: '更新已下载',message: '更新已下载完成,应用将重启以应用更新。',buttons: ['立即重启', '稍后重启']}).then(result => {if (result.response === 0) {autoUpdater.quitAndInstall();}});});// 检查更新autoUpdater.checkForUpdates();// 定时检查更新(每4小时)setInterval(() => {autoUpdater.checkForUpdates();}, 4 * 60 * 60 * 1000);}// 手动检查更新checkForUpdates() {autoUpdater.checkForUpdates();}// 下载更新downloadUpdate() {autoUpdater.downloadUpdate();}// 退出并安装更新quitAndInstall() {autoUpdater.quitAndInstall();}
}module.exports = AppUpdater;

9. 性能优化最佳实践

9.1 渲染进程优化

src/renderer/utils/performance.js

class PerformanceOptimizer {constructor() {this.isDevelopment = process.env.NODE_ENV === 'development';this.init();}init() {if (!this.isDevelopment) {this.disableDevTools();this.optimizeImages();this.enableHardwareAcceleration();}}// 禁用开发者工具(生产环境)disableDevTools() {document.addEventListener('keydown', (e) => {if (e.key === 'F12' || (e.ctrlKey && e.shiftKey && e.key === 'I')) {e.preventDefault();}});}// 图片优化optimizeImages() {// 懒加载图片const images = document.querySelectorAll('img[data-src]');const imageObserver = new IntersectionObserver((entries, observer) => {entries.forEach(entry => {if (entry.isIntersecting) {const img = entry.target;img.src = img.dataset.src;img.removeAttribute('data-src');observer.unobserve(img);}});});images.forEach(img => imageObserver.observe(img));}// 启用硬件加速enableHardwareAcceleration() {const style = document.createElement('style');style.textContent = `* {-webkit-transform: translateZ(0);-webkit-backface-visibility: hidden;-webkit-perspective: 1000;}`;document.head.appendChild(style);}// 内存管理cleanup() {// 清理事件监听器window.removeEventListener('resize', this.handleResize);window.removeEventListener('scroll', this.handleScroll);// 清理定时器if (this.timers) {this.timers.forEach(timer => clearTimeout(timer));}// 清理缓存if (window.caches) {window.caches.keys().then(names => {names.forEach(name => {window.caches.delete(name);});});}}// 优化长列表渲染virtualizeList(container, items, itemHeight, renderItem) {const visibleCount = Math.ceil(container.clientHeight / itemHeight);const totalHeight = items.length * itemHeight;let startIndex = 0;let endIndex = visibleCount;const updateVisibleItems = () => {const scrollTop = container.scrollTop;startIndex = Math.floor(scrollTop / itemHeight);endIndex = Math.min(startIndex + visibleCount + 1, items.length);const offsetY = startIndex * itemHeight;// 渲染可见项目container.innerHTML = '';const fragment = document.createDocumentFragment();for (let i = startIndex; i < endIndex; i++) {const item = renderItem(items[i], i);item.style.position = 'absolute';item.style.top = `${i * itemHeight - offsetY}px`;item.style.width = '100%';fragment.appendChild(item);}container.appendChild(fragment);};// 设置容器样式container.style.position = 'relative';container.style.height = `${totalHeight}px`;// 初始渲染updateVisibleItems();// 监听滚动事件container.addEventListener('scroll', updateVisibleItems);return {update: updateVisibleItems,destroy: () => {container.removeEventListener('scroll', updateVisibleItems);}};}// 防抖函数debounce(func, wait) {let timeout;return function executedFunction(...args) {const later = () => {clearTimeout(timeout);func(...args);};clearTimeout(timeout);timeout = setTimeout(later, wait);};}// 节流函数throttle(func, limit) {let inThrottle;return function() {const args = arguments;const context = this;if (!inThrottle) {func.apply(context, args);inThrottle = true;setTimeout(() => inThrottle = false, limit);}};}// 监控性能指标monitorPerformance() {// 监控内存使用if (performance.memory) {setInterval(() => {const memory = performance.memory;console.log('Memory Usage:', {used: Math.round(memory.usedJSHeapSize / 1048576), // MBtotal: Math.round(memory.totalJSHeapSize / 1048576), // MBlimit: Math.round(memory.jsHeapSizeLimit / 1048576) // MB});}, 30000); // 每30秒检查一次}// 监控帧率let lastTime = performance.now();let frameCount = 0;const measureFPS = () => {const currentTime = performance.now();frameCount++;if (currentTime >= lastTime + 1000) {const fps = Math.round((frameCount * 1000) / (currentTime - lastTime));console.log('FPS:', fps);frameCount = 0;lastTime = currentTime;}requestAnimationFrame(measureFPS);};requestAnimationFrame(measureFPS);}
}export default new PerformanceOptimizer();

9.2 主进程优化

src/main/performance.js

const { app } = require('electron');class MainProcessOptimizer {constructor() {this.init();}init() {this.optimizeAppStartup();this.manageMemory();this.optimizeWindowCreation();}// 优化应用启动optimizeAppStartup() {// 禁用硬件加速(在某些系统上可能有问题)if (process.platform === 'linux') {app.disableHardwareAcceleration();}// 优化 GPU 设置app.commandLine.appendSwitch('disable-gpu-compositing');app.commandLine.appendSwitch('disable-gpu-vsync');// 优化内存设置app.commandLine.appendSwitch('js-flags', '--max-old-space-size=4096');}// 内存管理manageMemory() {// 定期垃圾回收setInterval(() => {if (global.gc) {global.gc();console.log('手动垃圾回收完成');}}, 60000); // 每分钟一次// 监控内存使用setInterval(() => {const memoryUsage = process.memoryUsage();console.log('主进程内存使用:', {rss: Math.round(memoryUsage.rss / 1048576) + ' MB',heapTotal: Math.round(memoryUsage.heapTotal / 1048576) + ' MB',heapUsed: Math.round(memoryUsage.heapUsed / 1048576) + ' MB',external: Math.round(memoryUsage.external / 1048576) + ' MB'});}, 30000); // 每30秒}// 优化窗口创建optimizeWindowCreation() {// 预加载常用模块const commonModules = ['fs','path','electron','./ipc','./menu','./fileManager'];commonModules.forEach(module => {try {require(module);} catch (error) {console.warn(`预加载模块失败: ${module}`, error);}});}// 优化数据库连接optimizeDatabase() {// 连接池管理const connectionPool = {maxConnections: 10,idleTimeout: 30000,connections: []};// 定期清理空闲连接setInterval(() => {const now = Date.now();connectionPool.connections = connectionPool.connections.filter(conn => {if (now - conn.lastUsed > connectionPool.idleTimeout) {conn.close();return false;}return true;});}, 60000);}// 优化文件系统操作optimizeFileSystem() {// 文件缓存const fileCache = new Map();const cacheTimeout = 300000; // 5分钟// 定期清理文件缓存setInterval(() => {const now = Date.now();for (const [key, value] of fileCache.entries()) {if (now - value.timestamp > cacheTimeout) {fileCache.delete(key);}}}, 300000); // 每5分钟}
}module.exports = new MainProcessOptimizer();

9.3 构建优化

vue.config.js 优化配置

const { defineConfig } = require('@vue/cli-service');
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');module.exports = defineConfig({transpileDependencies: true,// 生产环境优化configureWebpack: config => {if (process.env.NODE_ENV === 'production') {// 代码压缩config.optimization.minimizer = [new TerserPlugin({terserOptions: {compress: {drop_console: true,drop_debugger: true},output: {comments: false}}})];// Gzip 压缩config.plugins.push(new CompressionPlugin({algorithm: 'gzip',test: /\.(js|css|html|svg)$/,threshold: 8192,minRatio: 0.8}));}},// 开发服务器配置devServer: {port: 8080,host: 'localhost',https: false,hot: true,open: false},pluginOptions: {electronBuilder: {nodeIntegration: true,contextIsolation: false,// 主进程文件mainProcessFile: 'src/main/index.js',// 预加载脚本preload: 'src/preload/preload.js',// 构建选项builderOptions: {appId: 'com.example.electron-vue-app',productName: 'Electron Vue Desktop App',// 文件配置files: ['dist/**/*','dist_electron/**/*','node_modules/**/*','package.json','!**/node_modules/**/*.{md,txt}','!**/node_modules/**/LICENSE','!**/node_modules/**/README*','!**/node_modules/**/test/**/*','!**/node_modules/**/tests/**/*'],// macOS 配置mac: {icon: 'build/icon.icns',category: 'public.app-category.productivity',hardenedRuntime: true,gatekeeperAssess: false,entitlements: 'build/entitlements.mac.plist',entitlementsInherit: 'build/entitlements.mac.plist'},// Windows 配置win: {icon: 'build/icon.ico',target: [{target: 'nsis',arch: ['x64', 'ia32']}]},// Linux 配置linux: {icon: 'build/icon.png',target: [{target: 'AppImage',arch: ['x64']}],category: 'Utility'},// NSIS 安装程序配置nsis: {oneClick: false,allowToChangeInstallationDirectory: true,createDesktopShortcut: true,createStartMenuShortcut: true}}}}
});

10. 常见问题与解决方案

10.1 构建问题

问题1:原生模块编译失败

解决方案:

# 安装 windows-build-tools(Windows)
npm install --global windows-build-tools# 安装 node-gyp
npm install -g node-gyp# 重新编译原生模块
npm rebuild
问题2:打包后应用白屏

解决方案:

// vue.config.js 中确保正确配置
module.exports = {pluginOptions: {electronBuilder: {nodeIntegration: true,contextIsolation: false,mainProcessFile: 'src/main/index.js'}}
};

10.2 运行时问题

问题3:require is not defined

解决方案:

// 在 vue.config.js 中启用 nodeIntegration
pluginOptions: {electronBuilder: {nodeIntegration: true,contextIsolation: false}
}
问题4:CORS 跨域问题

解决方案:

// main.js 中配置 webSecurity
const mainWindow = new BrowserWindow({webPreferences: {webSecurity: false,allowRunningInsecureContent: true}
});

10.3 性能问题

问题5:应用启动慢

解决方案:

// 延迟加载非关键模块
setTimeout(() => {require('./heavy-module');
}, 1000);// 使用动态导入
const loadModule = async () => {const module = await import('./heavy-module');return module.default;
};

10.4 调试技巧

主进程调试:

# 使用 VS Code 调试
# .vscode/launch.json
{"version": "0.2.0","configurations": [{"name": "Debug Main Process","type": "node","request": "launch","cwd": "${workspaceFolder}","runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron","windows": {"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"},"args": ["."],"env": {"NODE_ENV": "development"}}]
}

11. 实战案例分析

11.1 代码编辑器应用

功能特性:

  • 多标签页编辑
  • 语法高亮
  • 文件树浏览
  • 全局搜索
  • 主题切换

技术实现:

// src/renderer/components/CodeEditor.vue
<template><div class="code-editor"><div class="editor-tabs"><div v-for="tab in openTabs" :key="tab.id":class="['tab', { active: tab.id === activeTabId }]"@click="switchTab(tab.id)"><span class="tab-title">{{ tab.title }}</span><button class="tab-close" @click.stop="closeTab(tab.id)">×</button></div></div><div class="editor-container"><textarea ref="editor"v-model="currentContent"@input="onContentChange"class="editor-textarea"></textarea></div></div>
</template><script>
import { ref, onMounted, nextTick } from 'vue';
import Prism from 'prismjs';
import 'prismjs/themes/prism-tomorrow.css';export default {name: 'CodeEditor',setup() {const editor = ref(null);const openTabs = ref([]);const activeTabId = ref(null);const currentContent = ref('');const switchTab = (tabId) => {activeTabId.value = tabId;const tab = openTabs.value.find(t => t.id === tabId);if (tab) {currentContent.value = tab.content;nextTick(() => {highlightCode();});}};const highlightCode = () => {if (editor.value) {const code = editor.value.value;const highlighted = Prism.highlight(code, Prism.languages.javascript, 'javascript');// 应用高亮}};const onContentChange = () => {const tab = openTabs.value.find(t => t.id === activeTabId.value);if (tab) {tab.content = currentContent.value;tab.isModified = true;}highlightCode();};const openFile = async (filePath) => {try {const result = await window.electronAPI.readFile(filePath);if (result.success) {const tab = {id: Date.now(),title: filePath.split('/').pop(),content: result.data,filePath,isModified: false};openTabs.value.push(tab);switchTab(tab.id);}} catch (error) {console.error('打开文件失败:', error);}};return {editor,openTabs,activeTabId,currentContent,switchTab,onContentChange,openFile};}
};
</script>

11.2 数据库管理工具

功能特性:

  • 多数据库连接
  • SQL 编辑器
  • 数据表管理
  • 数据导入导出
  • 查询历史记录

技术实现:

// src/renderer/components/DatabaseManager.vue
<template><div class="database-manager"><div class="sidebar"><div class="connections"><h3>数据库连接</h3><div v-for="conn in connections" :key="conn.id" class="connection-item"><span @click="connectDatabase(conn)">{{ conn.name }}</span><button @click="deleteConnection(conn.id)">删除</button></div></div><div class="tables" v-if="currentConnection"><h3>数据表</h3><div v-for="table in tables" :key="table.name" class="table-item"><span @click="loadTableData(table.name)">{{ table.name }}</span></div></div></div><div class="main-content"><div class="sql-editor"><textarea v-model="sqlQuery" placeholder="输入 SQL 查询..."class="sql-input"></textarea><button @click="executeQuery" class="execute-btn">执行</button></div><div class="results" v-if="queryResults"><table class="results-table"><thead><tr><th v-for="column in queryResults.columns" :key="column">{{ column }}</th></tr></thead><tbody><tr v-for="row in queryResults.rows" :key="row.id"><td v-for="column in queryResults.columns" :key="column">{{ row[column] }}</td></tr></tbody></table></div></div></div>
</template><script>
import { ref } from 'vue';export default {name: 'DatabaseManager',setup() {const connections = ref([]);const currentConnection = ref(null);const tables = ref([]);const sqlQuery = ref('');const queryResults = ref(null);const connectDatabase = async (connection) => {try {const result = await window.electronAPI.connectDatabase(connection);if (result.success) {currentConnection.value = connection;await loadTables();}} catch (error) {console.error('连接数据库失败:', error);}};const loadTables = async () => {if (!currentConnection.value) return;try {const result = await window.electronAPI.getTables(currentConnection.value.id);if (result.success) {tables.value = result.data;}} catch (error) {console.error('获取数据表失败:', error);}};const loadTableData = async (tableName) => {sqlQuery.value = `SELECT * FROM ${tableName} LIMIT 100`;await executeQuery();};const executeQuery = async () => {if (!currentConnection.value || !sqlQuery.value) return;try {const result = await window.electronAPI.executeQuery({connectionId: currentConnection.value.id,query: sqlQuery.value});if (result.success) {queryResults.value = result.data;}} catch (error) {console.error('执行查询失败:', error);}};const deleteConnection = async (connectionId) => {try {await window.electronAPI.deleteConnection(connectionId);connections.value = connections.value.filter(c => c.id !== connectionId);} catch (error) {console.error('删除连接失败:', error);}};return {connections,currentConnection,tables,sqlQuery,queryResults,connectDatabase,loadTableData,executeQuery,deleteConnection};}
};
</script>

12. 总结与展望

12.1 Electron + Vue 的优势

  1. 跨平台能力:一套代码支持多个操作系统
  2. 开发效率:使用熟悉的 Web 技术栈
  3. 生态丰富:Vue 生态系统完善
  4. 原生功能:访问操作系统 API
  5. UI 灵活性:现代化的界面设计

12.2 面临的挑战

  1. 应用体积:打包后的应用相对较大
  2. 内存占用:相比原生应用内存占用较高
  3. 启动速度:首次启动可能较慢
  4. 安全性:需要注意安全防护措施

12.3 最佳实践建议

  1. 模块化开发:合理划分主进程和渲染进程职责
  2. 性能优化:注意内存管理和性能监控
  3. 安全防护:实施必要的安全措施
  4. 用户体验:提供良好的用户交互体验
  5. 持续集成:建立完善的构建和发布流程

12.4 未来发展趋势

  1. WebAssembly 集成:提升性能和功能
  2. PWA 融合:结合 PWA 技术优势
  3. 云原生支持:更好的云端部署方案
  4. AI 能力集成:融入人工智能功能
  5. 低代码平台:可视化开发工具

12.5 学习资源推荐

  • 官方文档:Electron 和 Vue 官方文档
  • 社区资源:GitHub 开源项目
  • 在线教程:视频教程和博客文章
  • 开发工具:VS Code 插件和调试工具
  • UI 框架:Element Plus、Ant Design Vue 等

总结:Electron + Vue 的组合为桌面应用开发提供了强大而灵活的解决方案。通过合理的架构设计、性能优化和最佳实践,开发者可以构建出高质量的跨平台桌面应用。随着技术的不断发展,这一技术栈将在桌面应用开发领域发挥越来越重要的作用。

作者简介:资深前端开发工程师,专注于 Electron 和 Vue 技术栈,拥有丰富的桌面应用开发经验。

http://www.dtcms.com/a/561848.html

相关文章:

  • 如何运行asp.net网站wordpress怎么导入demo文件
  • focusPolicy/setFocusPolicy(FocusPolicy),styleSheet
  • 六.DockerFile解析及其应用部署
  • wp企业网站模板数据分析师事务所
  • AWS DMS 大规模数据库迁移:完全加载+持续复制最佳实践
  • 阿里巴巴六边形架构-从解耦到可测试的架构设计利器
  • 中国世界排名前500大学seo网上培训多少钱
  • 做网站做哪个行业好商城网站建设高端
  • 正规网站建设建设公司雅安建设局网站
  • 如何在Java中整合Redis?
  • 官方网站是什么意思免费链接生成器
  • 增加网站访客珠宝首饰商城网站建设
  • 网络通信的奥秘:TCP与UDP详解(三)
  • 理财网站开发最近中国新闻
  • 详解网络安全免杀对抗:攻防的猫鼠游戏
  • 【开题答辩全过程】以 高考志愿分析系统为例,包含答辩的问题和答案
  • ESP-IDF基础入门(2)
  • 中国建设官方网站首页网上商城推广方案
  • 网站建设必须安装程序天眼查公司信息查询
  • 织梦网站首页是哪个文件网站手机访问跳转代码
  • 博弈dp|凸包|math分类
  • 网站浏览器兼容性问题wordpress手机端网站
  • 中国建设银行预约网站xampp做网站
  • VS2019+CUDA 编译通过但有错误提示
  • 有哪些做问卷调查挣钱的网站单页 网站模板
  • 承德网站制作数据库营销案例
  • 32位汇编:实验9分支程序结构使用
  • Kanass实践指南(3) - 开发团队如何通过kanass有效管控开发任务
  • 基于双向时序卷积网络与双向门控循环单元(BiTCN-BiGRU)混合模型的时间序列预测(Matlab源码)
  • 电子商务网站建设 精品课wordpress主题缓存