Electron 35 + React 19 + Vite 桌面应用开发实战

Electron 35 + React 19 + Vite 桌面应用开发实战

简介

Electron 是目前最流行的跨平台桌面应用框架,结合 React 和 Vite 可以构建出开发体验和运行性能俱佳的应用。Electron 35 引入了诸多改进:增强的安全模型、更好的 Wayland 支持、改进的 Ozone 平台抽象层、以及更高效的窗口管理。

本文以开发一个数据库管理工具(DBView)为实战案例,从项目初始化、架构设计、IPC 通信、数据库驱动集成到打包发布的完整流程,讲解如何用 Electron 35 + React 19 + Vite 构建生产级桌面应用。

前置要求

  • Node.js 20 LTS 或更高
  • pnpm(推荐)或 npm / yarn
  • 基本的 React 和 TypeScript 知识
  • 了解 Electron 的基本概念(主进程/渲染进程)
  • 操作系统:macOS 12+ / Windows 10+ / Ubuntu 20.04+

详细步骤

1. 项目初始化

1.1 使用 electron-vite 脚手架

electron-vite 是专为 Electron 设计的 Vite 集成方案,支持主进程、预加载脚本和渲染进程的独立构建配置。

1
2
3
4
5
6
# 使用 pnpm 创建项目
pnpm create @electron-vite/app my-desktop-app -- --template react-ts
cd my-desktop-app

# 安装依赖
pnpm install

1.2 项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
my-desktop-app/
├── electron.vite.config.ts # electron-vite 构建配置
├── package.json
├── src/
│ ├── main/ # 主进程代码
│ │ └── index.ts # 主进程入口
│ ├── preload/ # 预加载脚本
│ │ └── index.ts # contextBridge 暴露 API
│ └── renderer/ # 渲染进程(React 应用)
│ ├── index.html
│ ├── src/
│ │ ├── App.tsx
│ │ └── main.tsx # React 入口
│ └── ...
├── resources/ # 应用资源(图标等)
└── build/ # 打包输出

1.3 安装核心依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# UI 框架
pnpm add antd @ant-design/icons
pnpm add zustand # 状态管理
pnpm add i18next react-i18next # 国际化

# 数据库驱动
pnpm add mysql2 # MySQL
pnpm add pg # PostgreSQL
pnpm add better-sqlite3 # SQLite
pnpm add tedious # SQL Server

# 工具库
pnpm add dayjs # 日期处理
pnpm add uuid # UUID 生成

2. 主进程架构

2.1 主进程入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// src/main/index.ts
import { app, BrowserWindow, ipcMain, shell } from 'electron';
import { join } from 'path';
import { is } from '@electron-toolkit/utils';

let mainWindow: BrowserWindow | null = null;

function createWindow(): void {
mainWindow = new BrowserWindow({
width: 1280,
height: 860,
minWidth: 960,
minHeight: 600,
show: false,
autoHideMenuBar: true,
titleBarStyle: 'hiddenInset', // macOS 隐藏标题栏
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false, // 需要访问 Node.js API
contextIsolation: true, // 启用上下文隔离
nodeIntegration: false, // 禁用 Node 集成
},
});

// 窗口准备好后再显示,避免白屏闪烁
mainWindow.on('ready-to-show', () => {
mainWindow?.show();
});

// 外部链接用默认浏览器打开
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
return { action: 'deny' };
});

// 开发模式加载本地 dev server,生产模式加载打包文件
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']);
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
}
}

// 应用就绪后创建窗口
app.whenReady().then(() => {
createWindow();

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

// macOS 以外平台点击关闭时退出应用
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});

2.2 安全配置详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 主进程安全配置
const webPreferences = {
// contextIsolation: 将预加载脚本与渲染进程隔离
// 渲染进程无法直接访问 Node.js API
contextIsolation: true,

// nodeIntegration: 禁用渲染进程的 Node.js 集成
// 防止恶意网页执行系统命令
nodeIntegration: false,

// sandbox: 启用沙箱模式(更安全但限制更多)
// 如果不需要文件系统访问,建议设为 true
sandbox: false,

// preload: 预加载脚本路径
// 通过 contextBridge 暴露有限的 API
preload: join(__dirname, '../preload/index.js'),

// webSecurity: 启用同源策略
webSecurity: true,

// 禁用远程模块(Electron 12+ 已废弃)
enableRemoteModule: false,
};

3. 预加载脚本与 IPC 通信

3.1 预加载脚本

预加载脚本是主进程和渲染进程之间的安全桥梁,通过 contextBridge 暴露有限的 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron';

// 定义暴露给渲染进程的 API 类型
export interface ElectronAPI {
// 数据库操作
db: {
connect: (config: ConnectionConfig) => Promise<ConnectResult>;
disconnect: (connectionId: string) => Promise<void>;
executeQuery: (connectionId: string, sql: string) => Promise<QueryResult>;
getDatabases: (connectionId: string) => Promise<string[]>;
getTables: (connectionId: string, database: string) => Promise<TableInfo[]>;
getTableSchema: (connectionId: string, tableName: string) => Promise<ColumnInfo[]>;
};
// 文件操作
file: {
saveFile: (content: string, defaultName: string) => Promise<string | null>;
openFile: (filters?: FileFilter[]) => Promise<FileResult | null>;
exportCsv: (data: any[][], filename: string) => Promise<boolean>;
};
// 应用操作
app: {
getVersion: () => Promise<string>;
minimize: () => void;
maximize: () => void;
close: () => void;
showSaveDialog: (options: SaveDialogOptions) => Promise<string | null>;
};
// 事件监听
on: (channel: string, callback: (...args: any[]) => void) => () => void;
}

// 通过 contextBridge 暴露 API
contextBridge.exposeInMainWorld('electronAPI', {
// 数据库操作
db: {
connect: (config) => ipcRenderer.invoke('db:connect', config),
disconnect: (connectionId) => ipcRenderer.invoke('db:disconnect', connectionId),
executeQuery: (connectionId, sql) => ipcRenderer.invoke('db:execute-query', connectionId, sql),
getDatabases: (connectionId) => ipcRenderer.invoke('db:get-databases', connectionId),
getTables: (connectionId, database) => ipcRenderer.invoke('db:get-tables', connectionId, database),
getTableSchema: (connectionId, tableName) => ipcRenderer.invoke('db:get-table-schema', connectionId, tableName),
},

// 文件操作
file: {
saveFile: (content, defaultName) => ipcRenderer.invoke('file:save', content, defaultName),
openFile: (filters) => ipcRenderer.invoke('file:open', filters),
exportCsv: (data, filename) => ipcRenderer.invoke('file:export-csv', data, filename),
},

// 应用操作
app: {
getVersion: () => ipcRenderer.invoke('app:get-version'),
minimize: () => ipcRenderer.send('app:minimize'),
maximize: () => ipcRenderer.send('app:maximize'),
close: () => ipcRenderer.send('app:close'),
showSaveDialog: (options) => ipcRenderer.invoke('dialog:save', options),
},

// 事件监听(返回取消监听的函数)
on: (channel, callback) => {
const listener = (_event, ...args) => callback(...args);
ipcRenderer.on(channel, listener);
return () => ipcRenderer.removeListener(channel, listener);
},
});

3.2 类型声明

在渲染进程中为暴露的 API 添加类型声明:

1
2
3
4
5
6
7
8
// src/renderer/src/types/electron.d.ts
import type { ElectronAPI } from '../../preload/index';

declare global {
interface Window {
electronAPI: ElectronAPI;
}
}

3.3 主进程 IPC 处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// src/main/ipc-handlers.ts
import { ipcMain, dialog, app, BrowserWindow } from 'electron';
import { writeFile, readFile } from 'fs/promises';
import { ConnectionManager } from './db/connection-manager';

const connectionManager = new ConnectionManager();

export function registerIpcHandlers(): void {
// ── 数据库连接 ──
ipcMain.handle('db:connect', async (_event, config) => {
try {
const connectionId = await connectionManager.connect(config);
return { success: true, connectionId };
} catch (error: any) {
return { success: false, error: error.message };
}
});

ipcMain.handle('db:disconnect', async (_event, connectionId) => {
await connectionManager.disconnect(connectionId);
return { success: true };
});

ipcMain.handle('db:execute-query', async (_event, connectionId, sql) => {
try {
const result = await connectionManager.executeQuery(connectionId, sql);
return { success: true, ...result };
} catch (error: any) {
return { success: false, error: error.message };
}
});

ipcMain.handle('db:get-databases', async (_event, connectionId) => {
try {
const databases = await connectionManager.getDatabases(connectionId);
return { success: true, databases };
} catch (error: any) {
return { success: false, error: error.message };
}
});

ipcMain.handle('db:get-tables', async (_event, connectionId, database) => {
try {
const tables = await connectionManager.getTables(connectionId, database);
return { success: true, tables };
} catch (error: any) {
return { success: false, error: error.message };
}
});

ipcMain.handle('db:get-table-schema', async (_event, connectionId, tableName) => {
try {
const columns = await connectionManager.getTableSchema(connectionId, tableName);
return { success: true, columns };
} catch (error: any) {
return { success: false, error: error.message };
}
});

// ── 文件操作 ──
ipcMain.handle('file:save', async (_event, content, defaultName) => {
const result = await dialog.showSaveDialog({
defaultPath: defaultName,
filters: [{ name: 'SQL 文件', extensions: ['sql'] }],
});
if (!result.canceled && result.filePath) {
await writeFile(result.filePath, content, 'utf-8');
return result.filePath;
}
return null;
});

ipcMain.handle('file:open', async (_event, filters) => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters,
});
if (!result.canceled && result.filePaths.length > 0) {
const content = await readFile(result.filePaths[0], 'utf-8');
return { path: result.filePaths[0], content };
}
return null;
});

ipcMain.handle('file:export-csv', async (_event, data, filename) => {
const result = await dialog.showSaveDialog({
defaultPath: filename,
filters: [{ name: 'CSV 文件', extensions: ['csv'] }],
});
if (!result.canceled && result.filePath) {
const csvContent = data.map((row: any[]) =>
row.map((cell) => {
if (cell === null) return '';
const str = String(cell);
return str.includes(',') || str.includes('"') || str.includes('\n')
? `"${str.replace(/"/g, '""')}"`
: str;
}).join(',')
).join('\n');
await writeFile(result.filePath, '\uFEFF' + csvContent, 'utf-8');
return true;
}
return false;
});

// ── 应用操作 ──
ipcMain.handle('app:get-version', () => app.getVersion());

ipcMain.on('app:minimize', () => {
BrowserWindow.getFocusedWindow()?.minimize();
});

ipcMain.on('app:maximize', () => {
const win = BrowserWindow.getFocusedWindow();
if (win?.isMaximized()) {
win.unmaximize();
} else {
win?.maximize();
}
});

ipcMain.on('app:close', () => {
BrowserWindow.getFocusedWindow()?.close();
});

ipcMain.handle('dialog:save', async (_event, options) => {
const result = await dialog.showSaveDialog(options);
return result.canceled ? null : result.filePath;
});
}

4. 数据库驱动集成

4.1 连接管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// src/main/db/connection-manager.ts
import mysql from 'mysql2/promise';
import { Pool as PgPool } from 'pg';
import Database from 'better-sqlite3';
import { Connection as TediousConnection } from 'tedious';
import { randomUUID } from 'crypto';
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

// 连接配置
interface ConnectionConfig {
name: string;
dbType: 'mysql' | 'postgresql' | 'sqlite' | 'mssql';
host?: string;
port?: number;
database?: string;
user?: string;
password?: string; // 加密存储
ssl?: boolean;
filePath?: string; // SQLite 文件路径
}

// 连接状态
interface ConnectionState {
id: string;
config: ConnectionConfig;
client: any;
connectedAt: Date;
}

export class ConnectionManager {
private connections: Map<string, ConnectionState> = new Map();

// 加密密钥(实际应用中应从安全存储读取)
private encryptionKey = process.env.DB_ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef';

async connect(config: ConnectionConfig): Promise<string> {
const id = randomUUID();
let client: any;

switch (config.dbType) {
case 'mysql':
client = await mysql.createConnection({
host: config.host || 'localhost',
port: config.port || 3306,
database: config.database,
user: config.user,
password: config.password,
ssl: config.ssl ? {} : undefined,
connectTimeout: 10000,
});
break;

case 'postgresql':
const pgPool = new PgPool({
host: config.host || 'localhost',
port: config.port || 5432,
database: config.database,
user: config.user,
password: config.password,
ssl: config.ssl ? { rejectUnauthorized: false } : false,
max: 5,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 10000,
});
// 测试连接
const pgClient = await pgPool.connect();
pgClient.release();
client = pgPool;
break;

case 'sqlite':
client = new Database(config.filePath || ':memory:', {
readonly: false,
fileMustExist: true,
});
client.pragma('journal_mode = WAL');
client.pragma('foreign_keys = ON');
break;

case 'mssql':
// 使用 tedious 连接 SQL Server
// 实际项目中建议使用 mssql 包(基于 tedious 的封装)
throw new Error('SQL Server 连接请使用 mssql 包');

default:
throw new Error(`不支持的数据库类型: ${config.dbType}`);
}

const state: ConnectionState = {
id,
config,
client,
connectedAt: new Date(),
};

this.connections.set(id, state);
return id;
}

async disconnect(id: string): Promise<void> {
const conn = this.connections.get(id);
if (!conn) return;

try {
switch (conn.config.dbType) {
case 'mysql':
await (conn.client as mysql.Connection).end();
break;
case 'postgresql':
await (conn.client as PgPool).end();
break;
case 'sqlite':
(conn.client as Database).close();
break;
}
} finally {
this.connections.delete(id);
}
}

async executeQuery(connectionId: string, sql: string): Promise<any> {
const conn = this.connections.get(connectionId);
if (!conn) throw new Error('连接不存在或已断开');

switch (conn.config.dbType) {
case 'mysql': {
const [rows, fields] = await (conn.client as mysql.Connection).execute(sql);
return { rows, fields, rowCount: (rows as any[]).length };
}
case 'postgresql': {
const result = await (conn.client as PgPool).query(sql);
return { rows: result.rows, fields: result.fields, rowCount: result.rowCount };
}
case 'sqlite': {
const stmt = (conn.client as Database).prepare(sql);
if (sql.trim().toUpperCase().startsWith('SELECT')) {
const rows = stmt.all();
return { rows, rowCount: rows.length };
} else {
const info = stmt.run();
return { affectedRows: info.changes, lastInsertRowid: info.lastInsertRowid };
}
}
default:
throw new Error(`不支持的数据库类型: ${conn.config.dbType}`);
}
}

async getDatabases(connectionId: string): Promise<string[]> {
const conn = this.connections.get(connectionId);
if (!conn) throw new Error('连接不存在或已断开');

switch (conn.config.dbType) {
case 'mysql': {
const [rows] = await (conn.client as mysql.Connection).query('SHOW DATABASES');
return (rows as any[]).map((r) => r.Database);
}
case 'postgresql': {
const result = await (conn.client as PgPool).query(
"SELECT datname FROM pg_database WHERE datistemplate = false"
);
return result.rows.map((r) => r.datname);
}
case 'sqlite':
return ['main']; // SQLite 单数据库
default:
return [];
}
}
}

4.2 密码安全存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 密码加密工具
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32;
const IV_LENGTH = 16;
const TAG_LENGTH = 16;

export function encryptPassword(password: string, key: Buffer): string {
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);

let encrypted = cipher.update(password, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');

// 格式: iv:authTag:encryptedData
return `${iv.toString('hex')}:${authTag}:${encrypted}`;
}

export function decryptPassword(encryptedData: string, key: Buffer): string {
const [ivHex, authTagHex, data] = encryptedData.split(':');
const decipher = createDecipheriv(
ALGORITHM,
key,
Buffer.from(ivHex, 'hex')
);
decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));

let decrypted = decipher.update(data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}

5. 渲染进程 React 架构

5.1 应用入口与路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/renderer/src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { HashRouter, Routes, Route, Navigate } from 'react-router-dom';
import { ConfigProvider, theme } from 'antd';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ConfigProvider
theme={{
algorithm: theme.darkAlgorithm,
token: {
colorPrimary: '#1677ff',
borderRadius: 6,
},
}}
>
<HashRouter>
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Navigate to="/connections" replace />} />
<Route path="connections" element={<ConnectionList />} />
<Route path="query/:connectionId" element={<QueryEditor />} />
<Route path="table/:connectionId/:tableName" element={<TableBrowser />} />
</Route>
</Routes>
</HashRouter>
</ConfigProvider>
</React.StrictMode>
);

5.2 数据库连接管理组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// src/renderer/src/components/ConnectionForm.tsx
import { useState } from 'react';
import { Form, Input, Select, Button, Card, message, Space, Typography } from 'antd';
import { PlusOutlined, DatabaseOutlined } from '@ant-design/icons';

const { Text } = Typography;

const DB_TYPES = [
{ value: 'mysql', label: 'MySQL' },
{ value: 'postgresql', label: 'PostgreSQL' },
{ value: 'sqlite', label: 'SQLite' },
{ value: 'mssql', label: 'SQL Server' },
];

interface ConnectionFormProps {
onConnected?: (connectionId: string) => void;
}

export function ConnectionForm({ onConnected }: ConnectionFormProps) {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [dbType, setDbType] = useState('mysql');

const handleConnect = async () => {
try {
const values = await form.validateFields();
setLoading(true);

const result = await window.electronAPI.db.connect(values);
if (result.success) {
message.success('连接成功!');
onConnected?.(result.connectionId);
} else {
message.error(`连接失败: ${result.error}`);
}
} catch (error: any) {
if (error.errorFields) return; // 表单验证错误
message.error(`连接失败: ${error.message}`);
} finally {
setLoading(false);
}
};

return (
<Card
title={
<Space>
<DatabaseOutlined />
<Text>新建数据库连接</Text>
</Space>
}
style={{ maxWidth: 500, margin: '24px auto' }}
>
<Form
form={form}
layout="vertical"
initialValues={{ dbType: 'mysql', host: 'localhost', port: 3306 }}
>
<Form.Item name="dbType" label="数据库类型">
<Select options={DB_TYPES} onChange={(v) => setDbType(v)} />
</Form.Item>

{dbType !== 'sqlite' && (
<>
<Form.Item
name="host"
label="主机地址"
rules={[{ required: true, message: '请输入主机地址' }]}
>
<Input placeholder="localhost" />
</Form.Item>

<Form.Item name="port" label="端口">
<Input type="number" />
</Form.Item>

<Form.Item
name="user"
label="用户名"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input placeholder="root" />
</Form.Item>

<Form.Item name="password" label="密码">
<Input.Password placeholder="输入密码" />
</Form.Item>

<Form.Item
name="database"
label="默认数据库"
>
<Input placeholder="可选" />
</Form.Item>
</>
)}

{dbType === 'sqlite' && (
<Form.Item
name="filePath"
label="数据库文件路径"
rules={[{ required: true, message: '请选择 SQLite 数据库文件' }]}
>
<Input placeholder="/path/to/database.db" />
</Form.Item>
)}

<Form.Item>
<Button
type="primary"
icon={<PlusOutlined />}
loading={loading}
onClick={handleConnect}
block
>
连接
</Button>
</Form.Item>
</Form>
</Card>
);
}

5.3 查询编辑器集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 在查询页面中集成 SQL 编辑器
import { SqlEditor } from '../components/SqlEditor';
import { executeQuery } from '../services/queryService';

export function QueryEditor() {
const { connectionId } = useParams();
const [sql, setSql] = useState('SELECT * FROM users LIMIT 10;');
const [results, setResults] = useState<any>(null);
const [loading, setLoading] = useState(false);

const handleExecute = async () => {
if (!connectionId || !sql.trim()) return;
setLoading(true);
try {
const result = await window.electronAPI.db.executeQuery(connectionId, sql);
if (result.success) {
setResults(result);
} else {
message.error(result.error);
}
} catch (error: any) {
message.error(error.message);
} finally {
setLoading(false);
}
};

return (
<div style={{ padding: 16 }}>
<SqlEditor
value={sql}
onChange={setSql}
height="200px"
placeholder="输入 SQL 语句..."
/>
<Button
type="primary"
icon={<PlayCircleOutlined />}
loading={loading}
onClick={handleExecute}
style={{ marginTop: 8 }}
>
执行 (Ctrl+Enter)
</Button>
</div>
);
}

6. 打包与发布

6.1 electron-builder 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# electron-builder.yml
appId: com.dbview.app
productName: DBView
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'

win:
executableName: dbview
target:
- target: nsis
arch:
- x64

nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always

mac:
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
notarize: false
target:
- target: dmg
arch:
- x64
- arm64

dmg:
artifactName: ${name}-${version}.${ext}

linux:
target:
- AppImage
- target: deb
arch:
- x64
category: Development

6.2 构建脚本

1
2
3
4
5
6
7
8
9
10
{
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:win": "npm run build && electron-builder --win --config electron-builder.yml",
"build:mac": "npm run build && electron-builder --mac --config electron-builder.yml",
"build:linux": "npm run build && electron-builder --linux --config electron-builder.yml"
}
}

6.3 平台特定注意事项

macOS:

  • 使用 titleBarStyle: 'hiddenInset' 实现原生风格标题栏
  • 打包前需要配置 entitlements.mac.plist
  • 如需发布到 App Store,需要启用 hardenedRuntime 并配置 Apple 开发者证书

Windows:

  • NSIS 安装包默认支持自动更新
  • 可使用 sign 配置添加数字签名
  • 注意路径中的反斜杠转义

Linux:

  • AppImage 格式通用性最好
  • deb 包适合 Debian/Ubuntu 系
  • 注意 Wayland 兼容性(Electron 35 已大幅改善)

7. 性能优化

7.1 窗口懒加载

1
2
3
4
5
6
7
8
9
10
// 延迟加载渲染进程资源
app.whenReady().then(() => {
// 先注册所有 IPC 处理器
registerIpcHandlers();

// 延迟 200ms 再创建窗口,让主进程先完成初始化
setTimeout(() => {
createWindow();
}, 200);
});

7.2 数据库查询优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 查询结果分页
async function executeQueryWithPagination(
connectionId: string,
baseSql: string,
page: number,
pageSize: number
): Promise<QueryResult> {
const conn = connectionManager.getConnection(connectionId);
if (!conn) throw new Error('连接不存在');

switch (conn.config.dbType) {
case 'mysql': {
const offset = (page - 1) * pageSize;
const paginatedSql = `${baseSql} LIMIT ${pageSize} OFFSET ${offset}`;
const countSql = `SELECT COUNT(*) as total FROM (${baseSql}) AS _count`;
return await connectionManager.executeQuery(connectionId, paginatedSql);
}
case 'postgresql': {
const offset = (page - 1) * pageSize;
const paginatedSql = `${baseSql} LIMIT ${pageSize} OFFSET ${offset}`;
return await connectionManager.executeQuery(connectionId, paginatedSql);
}
case 'sqlite': {
const offset = (page - 1) * pageSize;
const paginatedSql = `${baseSql} LIMIT ${pageSize} OFFSET ${offset}`;
return await connectionManager.executeQuery(connectionId, paginatedSql);
}
default:
return await connectionManager.executeQuery(connectionId, baseSql);
}
}

8. 调试技巧

8.1 开发工具

1
2
3
4
5
6
7
8
# 打开 Electron 的 DevTools
mainWindow.webContents.openDevTools();

# 使用 Chrome DevTools Protocol 远程调试
# 启动时添加 --inspect 参数
electron . --inspect=5858

# 在 Chrome 中打开 chrome://inspect 连接调试

8.2 常见问题排查

问题 原因 解决方案
白屏闪烁 窗口创建时机过早 使用 ready-to-show 事件
IPC 调用无响应 预加载脚本未正确暴露 API 检查 contextBridge.exposeInMainWorld
数据库连接超时 防火墙或网络问题 增加 connectTimeout 配置
打包后无法启动 依赖未正确打包 检查 electron-builderfiles 配置
SQLite 文件锁定 多进程并发访问 启用 WAL 模式

总结

本文以 DBView 数据库管理工具为案例,完整讲解了 Electron 35 + React 19 + Vite 技术栈的桌面应用开发流程。关键点总结:

  1. 项目架构:使用 electron-vite 脚手架,主进程 / 预加载脚本 / 渲染进程三分离
  2. 安全通信:通过 contextBridge + ipcRenderer.invoke 实现安全的进程间通信
  3. 多数据库支持ConnectionManager 统一管理 MySQL、PostgreSQL、SQLite 等多种数据库连接
  4. 密码安全:使用 AES-256-GCM 加密存储数据库密码
  5. 渲染进程:React 19 + Ant Design + Zustand 构建界面
  6. 打包发布:electron-builder 支持 Windows / macOS / Linux 三平台打包

这套架构已经在 DBView 项目中落地使用,如需了解 SQL 编辑器实现细节,请参考 CodeMirror 6 深度集成与 SQL 编辑器定制