技术文档中心
首页
React
Vue
TypeScript
Kotlin
React Native
Electron
Android
首页
React
Vue
TypeScript
Kotlin
React Native
Electron
Android
  • 基础入门

    • Electron 学习指南
    • 窗口管理
    • 进程通信
    • 对话框
  • 进阶内容

    • 菜单和托盘
    • 打包发布
    • Electron 自动更新
  • 框架集成

    • React + Electron
    • Vue + Electron

Electron 学习指南

什么是 Electron

Electron 是使用 Web 技术构建跨平台桌面应用的框架。

核心特点

  • 跨平台:Windows、macOS、Linux
  • Web 技术:HTML、CSS、JavaScript
  • Node.js:完整的 Node.js 环境
  • 原生能力:系统 API 访问

快速开始

安装

mkdir my-electron-app
cd my-electron-app
yarn init -y
yarn add -D electron

创建主进程

// main.js
const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: false,
      contextIsolation: true
    }
  });

  win.loadFile('index.html');
}

app.whenReady().then(() => {
  createWindow();

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

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

创建渲染进程

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hello Electron</title>
</head>
<body>
  <h1>Hello Electron!</h1>
  <script src="renderer.js"></script>
</body>
</html>

配置 package.json

{
  "name": "my-electron-app",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  }
}

运行

yarn start

主进程与渲染进程

主进程

// main.js
const { app, BrowserWindow, ipcMain } = require('electron');

ipcMain.handle('get-data', async () => {
  return { message: 'Hello from main process' };
});

预加载脚本

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  getData: () => ipcRenderer.invoke('get-data')
});

渲染进程

// renderer.js
async function loadData() {
  const data = await window.electronAPI.getData();
  console.log(data);
}

loadData();

窗口管理

const { BrowserWindow } = require('electron');

// 创建窗口
const win = new BrowserWindow({
  width: 800,
  height: 600,
  title: '我的应用',
  icon: 'icon.png',
  resizable: true,
  minimizable: true,
  maximizable: true,
  frame: true,
  transparent: false,
  alwaysOnTop: false
});

// 加载页面
win.loadFile('index.html');
win.loadURL('https://example.com');

// 窗口事件
win.on('ready-to-show', () => {
  win.show();
});

win.on('closed', () => {
  console.log('窗口关闭');
});

菜单

const { Menu } = require('electron');

const template = [
  {
    label: '文件',
    submenu: [
      {
        label: '新建',
        accelerator: 'CmdOrCtrl+N',
        click: () => {
          console.log('新建');
        }
      },
      { type: 'separator' },
      { role: 'quit', label: '退出' }
    ]
  },
  {
    label: '编辑',
    submenu: [
      { role: 'undo', label: '撤销' },
      { role: 'redo', label: '重做' },
      { type: 'separator' },
      { role: 'cut', label: '剪切' },
      { role: 'copy', label: '复制' },
      { role: 'paste', label: '粘贴' }
    ]
  }
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

对话框

const { dialog } = require('electron');

// 打开文件
const result = await dialog.showOpenDialog({
  properties: ['openFile', 'multiSelections'],
  filters: [
    { name: 'Images', extensions: ['jpg', 'png', 'gif'] },
    { name: 'All Files', extensions: ['*'] }
  ]
});

// 保存文件
const savePath = await dialog.showSaveDialog({
  defaultPath: 'untitled.txt'
});

// 消息框
dialog.showMessageBox({
  type: 'info',
  title: '提示',
  message: '操作成功',
  buttons: ['确定']
});

学习路径

  1. 基础概念 → 主进程、渲染进程
  2. 进程通信 → IPC
  3. 窗口管理 → BrowserWindow
  4. 菜单和托盘 → Menu、Tray
  5. 文件系统 → dialog、fs
  6. 打包发布 → electron-builder
  7. 自动更新 → electron-updater
  8. 性能优化 → 内存管理、启动优化

完整示例:记事本应用

项目结构

notepad-app/
├── main.js           # 主进程
├── preload.js        # 预加载脚本
├── index.html        # 渲染进程
├── renderer.js       # 渲染进程逻辑
├── styles.css        # 样式
└── package.json      # 配置

main.js

const { app, BrowserWindow, ipcMain, dialog, Menu } = require('electron');
const path = require('path');
const fs = require('fs').promises;

let mainWindow;
let currentFilePath = null;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1000,
    height: 700,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: false,
      contextIsolation: true
    }
  });

  mainWindow.loadFile('index.html');
  createMenu();
}

function createMenu() {
  const template = [
    {
      label: '文件',
      submenu: [
        {
          label: '新建',
          accelerator: 'CmdOrCtrl+N',
          click: () => {
            mainWindow.webContents.send('file-new');
          }
        },
        {
          label: '打开',
          accelerator: 'CmdOrCtrl+O',
          click: async () => {
            const result = await dialog.showOpenDialog(mainWindow, {
              properties: ['openFile'],
              filters: [
                { name: 'Text Files', extensions: ['txt', 'md'] },
                { name: 'All Files', extensions: ['*'] }
              ]
            });

            if (!result.canceled && result.filePaths.length > 0) {
              const filePath = result.filePaths[0];
              const content = await fs.readFile(filePath, 'utf-8');
              currentFilePath = filePath;
              mainWindow.webContents.send('file-opened', { filePath, content });
            }
          }
        },
        {
          label: '保存',
          accelerator: 'CmdOrCtrl+S',
          click: () => {
            mainWindow.webContents.send('file-save');
          }
        },
        {
          label: '另存为',
          accelerator: 'CmdOrCtrl+Shift+S',
          click: () => {
            mainWindow.webContents.send('file-save-as');
          }
        },
        { type: 'separator' },
        { role: 'quit', label: '退出' }
      ]
    },
    {
      label: '编辑',
      submenu: [
        { role: 'undo', label: '撤销' },
        { role: 'redo', label: '重做' },
        { type: 'separator' },
        { role: 'cut', label: '剪切' },
        { role: 'copy', label: '复制' },
        { role: 'paste', label: '粘贴' },
        { role: 'selectAll', label: '全选' }
      ]
    },
    {
      label: '查看',
      submenu: [
        { role: 'reload', label: '重新加载' },
        { role: 'toggleDevTools', label: '开发者工具' },
        { type: 'separator' },
        { role: 'resetZoom', label: '实际大小' },
        { role: 'zoomIn', label: '放大' },
        { role: 'zoomOut', label: '缩小' }
      ]
    }
  ];

  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);
}

// 保存文件
ipcMain.handle('save-file', async (event, { filePath, content }) => {
  try {
    let saveFilePath = filePath || currentFilePath;

    if (!saveFilePath) {
      const result = await dialog.showSaveDialog(mainWindow, {
        filters: [
          { name: 'Text Files', extensions: ['txt', 'md'] },
          { name: 'All Files', extensions: ['*'] }
        ]
      });

      if (result.canceled) {
        return { success: false, canceled: true };
      }

      saveFilePath = result.filePath;
    }

    await fs.writeFile(saveFilePath, content, 'utf-8');
    currentFilePath = saveFilePath;

    return { success: true, filePath: saveFilePath };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

// 另存为
ipcMain.handle('save-file-as', async (event, content) => {
  try {
    const result = await dialog.showSaveDialog(mainWindow, {
      filters: [
        { name: 'Text Files', extensions: ['txt', 'md'] },
        { name: 'All Files', extensions: ['*'] }
      ]
    });

    if (result.canceled) {
      return { success: false, canceled: true };
    }

    await fs.writeFile(result.filePath, content, 'utf-8');
    currentFilePath = result.filePath;

    return { success: true, filePath: result.filePath };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

app.whenReady().then(() => {
  createWindow();

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

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

preload.js

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  saveFile: (filePath, content) => ipcRenderer.invoke('save-file', { filePath, content }),
  saveFileAs: (content) => ipcRenderer.invoke('save-file-as', content),
  onFileNew: (callback) => ipcRenderer.on('file-new', callback),
  onFileOpened: (callback) => ipcRenderer.on('file-opened', (event, data) => callback(data)),
  onFileSave: (callback) => ipcRenderer.on('file-save', callback),
  onFileSaveAs: (callback) => ipcRenderer.on('file-save-as', callback)
});

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>记事本</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div class="container">
    <div class="header">
      <div class="file-info">
        <span id="fileName">未命名</span>
        <span id="modified" class="modified hidden">●</span>
      </div>
      <div class="stats">
        <span id="charCount">0 字符</span>
        <span id="lineCount">1 行</span>
      </div>
    </div>
    
    <textarea id="editor" placeholder="开始输入..."></textarea>
  </div>
  
  <script src="renderer.js"></script>
</body>
</html>

renderer.js

const editor = document.getElementById('editor');
const fileName = document.getElementById('fileName');
const modified = document.getElementById('modified');
const charCount = document.getElementById('charCount');
const lineCount = document.getElementById('lineCount');

let currentFilePath = null;
let isModified = false;
let originalContent = '';

// 更新统计信息
function updateStats() {
  const content = editor.value;
  const chars = content.length;
  const lines = content.split('\n').length;
  
  charCount.textContent = `${chars} 字符`;
  lineCount.textContent = `${lines} 行`;
}

// 标记为已修改
function markAsModified() {
  if (editor.value !== originalContent) {
    isModified = true;
    modified.classList.remove('hidden');
  } else {
    isModified = false;
    modified.classList.add('hidden');
  }
}

// 编辑器输入事件
editor.addEventListener('input', () => {
  updateStats();
  markAsModified();
});

// 新建文件
window.electronAPI.onFileNew(() => {
  if (isModified) {
    if (!confirm('当前文件未保存,确定要新建吗?')) {
      return;
    }
  }
  
  editor.value = '';
  currentFilePath = null;
  originalContent = '';
  isModified = false;
  fileName.textContent = '未命名';
  modified.classList.add('hidden');
  updateStats();
});

// 打开文件
window.electronAPI.onFileOpened((data) => {
  editor.value = data.content;
  currentFilePath = data.filePath;
  originalContent = data.content;
  isModified = false;
  fileName.textContent = data.filePath.split('/').pop();
  modified.classList.add('hidden');
  updateStats();
});

// 保存文件
window.electronAPI.onFileSave(async () => {
  const result = await window.electronAPI.saveFile(currentFilePath, editor.value);
  
  if (result.success) {
    currentFilePath = result.filePath;
    originalContent = editor.value;
    isModified = false;
    fileName.textContent = result.filePath.split('/').pop();
    modified.classList.add('hidden');
  } else if (!result.canceled) {
    alert('保存失败: ' + result.error);
  }
});

// 另存为
window.electronAPI.onFileSaveAs(async () => {
  const result = await window.electronAPI.saveFileAs(editor.value);
  
  if (result.success) {
    currentFilePath = result.filePath;
    originalContent = editor.value;
    isModified = false;
    fileName.textContent = result.filePath.split('/').pop();
    modified.classList.add('hidden');
  } else if (!result.canceled) {
    alert('保存失败: ' + result.error);
  }
});

// 初始化
updateStats();

styles.css

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  height: 100vh;
  overflow: hidden;
}

.container {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 20px;
  background: #f5f5f5;
  border-bottom: 1px solid #ddd;
}

.file-info {
  display: flex;
  align-items: center;
  gap: 8px;
}

#fileName {
  font-weight: 500;
  font-size: 14px;
}

.modified {
  color: #007AFF;
  font-size: 20px;
}

.modified.hidden {
  display: none;
}

.stats {
  display: flex;
  gap: 20px;
  font-size: 12px;
  color: #666;
}

#editor {
  flex: 1;
  padding: 20px;
  border: none;
  outline: none;
  font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
  font-size: 14px;
  line-height: 1.6;
  resize: none;
}

#editor::placeholder {
  color: #999;
}

package.json

{
  "name": "notepad-app",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "build": "electron-builder"
  },
  "devDependencies": {
    "electron": "^28.0.0",
    "electron-builder": "^24.0.0"
  },
  "build": {
    "appId": "com.example.notepad",
    "productName": "记事本",
    "directories": {
      "output": "dist"
    },
    "files": [
      "**/*",
      "!node_modules/**/*",
      "node_modules/electron/**/*"
    ],
    "mac": {
      "target": ["dmg"],
      "icon": "build/icon.icns"
    },
    "win": {
      "target": ["nsis"],
      "icon": "build/icon.ico"
    }
  }
}

实战:Todo 应用

使用 SQLite 数据库

yarn add better-sqlite3

main.js (数据库部分)

const Database = require('better-sqlite3');
const path = require('path');

// 初始化数据库
const db = new Database(path.join(app.getPath('userData'), 'todos.db'));

// 创建表
db.exec(`
  CREATE TABLE IF NOT EXISTS todos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    text TEXT NOT NULL,
    completed INTEGER DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);

// 获取所有待办
ipcMain.handle('get-todos', () => {
  const stmt = db.prepare('SELECT * FROM todos ORDER BY created_at DESC');
  return stmt.all();
});

// 添加待办
ipcMain.handle('add-todo', (event, text) => {
  const stmt = db.prepare('INSERT INTO todos (text) VALUES (?)');
  const result = stmt.run(text);
  return { id: result.lastInsertRowid, text, completed: 0 };
});

// 切换完成状态
ipcMain.handle('toggle-todo', (event, id) => {
  const stmt = db.prepare('UPDATE todos SET completed = NOT completed WHERE id = ?');
  stmt.run(id);
  return true;
});

// 删除待办
ipcMain.handle('delete-todo', (event, id) => {
  const stmt = db.prepare('DELETE FROM todos WHERE id = ?');
  stmt.run(id);
  return true;
});

// 应用退出时关闭数据库
app.on('will-quit', () => {
  db.close();
});

调试技巧

主进程调试

# 使用 VSCode 调试
# .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": ["."],
      "outputCapture": "std"
    }
  ]
}

渲染进程调试

// 开发环境自动打开 DevTools
if (process.env.NODE_ENV === 'development') {
  mainWindow.webContents.openDevTools();
}

日志输出

// 主进程
console.log('主进程日志');

// 渲染进程
console.log('渲染进程日志');

// 使用 electron-log
const log = require('electron-log');
log.info('应用启动');
log.error('发生错误');

性能优化

1. 延迟加载

// 延迟显示窗口
const win = new BrowserWindow({ show: false });
win.once('ready-to-show', () => {
  win.show();
});

2. 预加载优化

// 只暴露必要的 API
contextBridge.exposeInMainWorld('api', {
  // 只暴露需要的方法
  saveFile: (data) => ipcRenderer.invoke('save-file', data)
});

3. 内存管理

// 及时清理不用的窗口
win.on('closed', () => {
  win = null;
});

// 限制窗口数量
if (BrowserWindow.getAllWindows().length > 5) {
  // 关闭旧窗口
}

学习路径

第一阶段:基础(1周)

  1. 主进程和渲染进程概念
  2. 创建窗口
  3. 加载页面
  4. 基础 IPC 通信

第二阶段:进阶(2周)

  1. 菜单和托盘
  2. 对话框
  3. 文件系统操作
  4. 数据库集成

第三阶段:实战(2周)

  1. 完整应用开发
  2. 打包配置
  3. 自动更新
  4. 性能优化

第四阶段:发布(1周)

  1. 代码签名
  2. 应用商店发布
  3. 持续集成
  4. 用户反馈收集

推荐资源

  • Electron 官方文档
  • Electron Fiddle - 在线实验
  • Awesome Electron - 资源列表
  • Electron Builder - 打包工具文档
最近更新: 2026/2/6 15:39
Contributors: hailong
Next
窗口管理