Electron 是由 GitHub 开发的跨平台桌面应用开发框架,基于 Chromium 和 Node.js,允许开发者使用 HTML、CSS、JavaScript 构建 Windows/macOS/Linux 桌面应用。本文以Markdown 编辑器为场景,从零实现一个支持实时预览、本地文件读写、快捷键操作、自定义窗口的桌面应用,覆盖 Electron 核心开发能力:主进程与渲染进程通信、原生文件操作、窗口管理、快捷键监听等。

一、环境准备

1. 安装 Node.js

Electron 依赖 Node.js 环境,需先安装 Node.js(推荐 LTS 版本),安装完成后验证:

node -v # 输出 v18.x.x 或更高
npm -v  # 输出 9.x.x 或更高

2. 初始化项目

创建项目目录并初始化 npm 项目:

# 创建目录
mkdir electron-markdown-editor
cd electron-markdown-editor

# 初始化 npm 项目(一路回车即可)
npm init

# 安装 Electron 依赖
npm install electron@28.0.0 --save-dev
# 安装 Markdown 解析库
npm install marked@11.0.0 --save
# 安装跨平台文件对话框库
npm install dialog-node --save

3. 配置 package.json

修改 package.json,添加入口文件和启动脚本:

{
  "name": "electron-markdown-editor",
  "version": "1.0.0",
  "main": "main.js", // 主进程入口
  "scripts": {
    "start": "electron .", // 启动应用
    "package": "electron-packager . MarkdownEditor --platform=win32,darwin,linux --arch=x64 --out=dist" // 打包脚本(需先安装 electron-packager)
  },
  "devDependencies": {
    "electron": "^28.0.0",
    "electron-packager": "^17.1.2"
  },
  "dependencies": {
    "dialog-node": "^0.1.1",
    "marked": "^11.0.0"
  }
}

二、核心概念梳理

Electron 应用分为两个核心进程:

  • 主进程(Main Process):由 main.js 启动,负责管理窗口、原生 API 调用(文件、快捷键、系统对话框)、渲染进程通信;
  • 渲染进程(Renderer Process):每个窗口对应一个渲染进程,负责 UI 渲染和用户交互(基于 HTML/CSS/JS);
  • IPC 通信:主进程与渲染进程通过 ipcMain(主进程)和 ipcRenderer(渲染进程)实现双向通信。

三、完整代码实现

1. 主进程(main.js)

负责窗口创建、文件操作、快捷键监听、IPC 通信处理:

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

// 声明主窗口变量
let mainWindow;

// 创建应用窗口
function createWindow() {
  // 配置窗口参数
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600,
    webPreferences: {
      nodeIntegration: true, // 允许渲染进程使用 Node.js API
      contextIsolation: false, // 关闭上下文隔离(简化开发)
      preload: path.join(__dirname, 'preload.js') // 预加载脚本(可选)
    },
    title: 'Electron Markdown 编辑器'
  });

  // 加载渲染进程页面
  mainWindow.loadFile('index.html');

  // 打开开发者工具(开发阶段)
  // mainWindow.webContents.openDevTools();

  // 监听窗口关闭事件
  mainWindow.on('closed', () => {
    mainWindow = null;
  });

  // 注册全局快捷键
  registerShortcuts();
}

// 注册全局快捷键
function registerShortcuts() {
  // Ctrl+N 新建文件
  globalShortcut.register('Ctrl+N', () => {
    mainWindow.webContents.send('shortcut:new-file');
  });

  // Ctrl+O 打开文件
  globalShortcut.register('Ctrl+O', () => {
    openFile();
  });

  // Ctrl+S 保存文件
  globalShortcut.register('Ctrl+S', () => {
    saveFile();
  });
}

// 打开本地 Markdown 文件
function openFile() {
  // 弹出文件选择对话框
  const result = dialog.showOpenDialogSync(mainWindow, {
    title: '选择 Markdown 文件',
    filters: [
      { name: 'Markdown Files', extensions: ['md', 'markdown'] },
      { name: 'All Files', extensions: ['*'] }
    ],
    properties: ['openFile']
  });

  if (result && result.length > 0) {
    const filePath = result[0];
    // 读取文件内容
    fs.readFile(filePath, 'utf8', (err, content) => {
      if (err) {
        dialog.showErrorBox('读取失败', `无法读取文件:${err.message}`);
        return;
      }
      // 发送文件内容到渲染进程
      mainWindow.webContents.send('file:opened', {
        path: filePath,
        content: content
      });
      // 更新窗口标题
      mainWindow.setTitle(`${path.basename(filePath)} - Electron Markdown 编辑器`);
    });
  }
}

// 保存文件
function saveFile(data) {
  // 如果传入了文件内容(新文件),先弹出保存对话框
  if (data) {
    const filePath = dialog.showSaveDialogSync(mainWindow, {
      title: '保存 Markdown 文件',
      filters: [
        { name: 'Markdown Files', extensions: ['md'] },
        { name: 'All Files', extensions: ['*'] }
      ],
      defaultPath: 'untitled.md'
    });

    if (filePath) {
      fs.writeFile(filePath, data.content, 'utf8', (err) => {
        if (err) {
          dialog.showErrorBox('保存失败', `无法保存文件:${err.message}`);
          return;
        }
        mainWindow.webContents.send('file:saved', filePath);
        mainWindow.setTitle(`${path.basename(filePath)} - Electron Markdown 编辑器`);
      });
    }
  } else {
    // 如果是已打开的文件,直接请求渲染进程获取内容并保存
    mainWindow.webContents.send('file:request-save');
  }
}

// 监听渲染进程的 IPC 事件
function registerIpcHandlers() {
  // 监听渲染进程的「保存文件」请求(已打开文件)
  ipcMain.on('file:save', (event, data) => {
    if (data.path) {
      // 保存到已有路径
      fs.writeFile(data.path, data.content, 'utf8', (err) => {
        if (err) {
          dialog.showErrorBox('保存失败', `无法保存文件:${err.message}`);
          return;
        }
        event.reply('file:saved', data.path);
      });
    } else {
      // 新文件,调用保存逻辑
      saveFile(data);
    }
  });

  // 监听渲染进程的「新建文件」请求
  ipcMain.on('file:new', () => {
    mainWindow.webContents.send('file:newed');
    mainWindow.setTitle('未命名 - Electron Markdown 编辑器');
  });
}

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

  // macOS 下应用激活时重新创建窗口
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

// 所有窗口关闭时退出应用(macOS 除外)
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// 应用退出时注销快捷键
app.on('will-quit', () => {
  globalShortcut.unregisterAll();
});

2. 渲染进程(index.html)

负责 UI 渲染、Markdown 实时预览、用户交互:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Electron Markdown 编辑器</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      display: flex;
      height: 100vh;
      overflow: hidden;
    }
    .editor-container {
      flex: 1;
      display: flex;
      flex-direction: column;
      border-right: 1px solid #eee;
    }
    .preview-container {
      flex: 1;
      padding: 20px;
      overflow-y: auto;
      background-color: #f9f9f9;
    }
    textarea {
      flex: 1;
      width: 100%;
      padding: 20px;
      border: none;
      outline: none;
      font-size: 16px;
      font-family: 'Consolas', 'Monaco', monospace;
      resize: none;
    }
    .toolbar {
      padding: 10px 20px;
      background-color: #333;
      color: white;
      display: flex;
      gap: 15px;
      align-items: center;
    }
    .toolbar button {
      padding: 5px 10px;
      border: none;
      border-radius: 4px;
      background-color: #555;
      color: white;
      cursor: pointer;
      transition: background-color 0.2s;
    }
    .toolbar button:hover {
      background-color: #666;
    }
    .status-bar {
      padding: 5px 20px;
      background-color: #eee;
      font-size: 12px;
      color: #666;
    }
  </style>
</head>
<body>
  <!-- 左侧编辑区 -->
  <div class="editor-container">
    <div class="toolbar">
      <button id="newBtn">新建 (Ctrl+N)</button>
      <button id="openBtn">打开 (Ctrl+O)</button>
      <button id="saveBtn">保存 (Ctrl+S)</button>
    </div>
    <textarea id="editor" placeholder="请输入 Markdown 内容..."># Electron Markdown 编辑器

这是一个基于 Electron 开发的跨平台桌面应用:
- 支持实时预览
- 本地文件操作
- 全局快捷键
- 跨平台运行(Windows/macOS/Linux)</textarea>
    <div class="status-bar" id="statusBar">未保存</div>
  </div>

  <!-- 右侧预览区 -->
  <div class="preview-container" id="preview"></div>

  <script>
    const { ipcRenderer } = require('electron');
    const marked = require('marked');
    const fs = require('fs');

    // DOM 元素
    const editor = document.getElementById('editor');
    const preview = document.getElementById('preview');
    const newBtn = document.getElementById('newBtn');
    const openBtn = document.getElementById('openBtn');
    const saveBtn = document.getElementById('saveBtn');
    const statusBar = document.getElementById('statusBar');

    // 当前打开的文件路径
    let currentFilePath = null;
    // 内容是否修改
    let isModified = false;

    // 实时预览 Markdown
    function renderPreview() {
      const content = editor.value;
      preview.innerHTML = marked.parse(content);
      // 更新状态
      if (!isModified && content.trim() !== '') {
        isModified = true;
        statusBar.textContent = '已修改(未保存)';
      }
    }

    // 初始化预览
    renderPreview();

    // 监听编辑区内容变化
    editor.addEventListener('input', renderPreview);

    // 新建文件
    function newFile() {
      editor.value = '';
      currentFilePath = null;
      isModified = false;
      statusBar.textContent = '未保存';
      renderPreview();
    }

    // 保存文件
    function saveFile() {
      const content = editor.value;
      if (currentFilePath) {
        // 保存到已有路径
        ipcRenderer.send('file:save', {
          path: currentFilePath,
          content: content
        });
      } else {
        // 新文件,请求主进程弹出保存对话框
        ipcRenderer.send('file:save', { content: content });
      }
    }

    // 绑定按钮事件
    newBtn.addEventListener('click', () => {
      ipcRenderer.send('file:new');
    });

    openBtn.addEventListener('click', () => {
      // 触发主进程的打开文件逻辑
      ipcRenderer.send('shortcut:open-file');
    });

    saveBtn.addEventListener('click', saveFile);

    // 监听主进程的 IPC 事件
    // 打开文件成功
    ipcRenderer.on('file:opened', (event, data) => {
      currentFilePath = data.path;
      editor.value = data.content;
      isModified = false;
      statusBar.textContent = `已打开:${currentFilePath}`;
      renderPreview();
    });

    // 保存文件成功
    ipcRenderer.on('file:saved', (event, path) => {
      currentFilePath = path;
      isModified = false;
      statusBar.textContent = `已保存:${path}`;
    });

    // 新建文件成功
    ipcRenderer.on('file:newed', newFile);

    // 快捷键触发新建文件
    ipcRenderer.on('shortcut:new-file', newFile);

    // 主进程请求保存(Ctrl+S 触发)
    ipcRenderer.on('file:request-save', saveFile);

    // 监听窗口关闭前的确认(可选)
    window.addEventListener('beforeunload', (e) => {
      if (isModified) {
        e.returnValue = '内容已修改,是否确定退出?';
        return '内容已修改,是否确定退出?';
      }
    });
  </script>
</body>
</html>

3. 预加载脚本(preload.js,可选)

用于安全地暴露主进程 API 到渲染进程(生产环境推荐):

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

// 向渲染进程暴露安全的 API
contextBridge.exposeInMainWorld('electronAPI', {
  // 文件操作
  openFile: () => ipcRenderer.send('shortcut:open-file'),
  newFile: () => ipcRenderer.send('file:new'),
  saveFile: (data) => ipcRenderer.send('file:save', data),
  // 监听主进程事件
  onFileOpened: (callback) => ipcRenderer.on('file:opened', callback),
  onFileSaved: (callback) => ipcRenderer.on('file:saved', callback),
  onNewFile: (callback) => ipcRenderer.on('file:newed', callback)
});

四、核心功能解析

1. 窗口管理

  • 窗口配置:通过 BrowserWindow 配置窗口大小、最小尺寸、Web 偏好设置(如开启 Node.js 集成);
  • 跨平台适配:监听 activate 事件(macOS),保证应用激活时窗口重建;监听 window-all-closed 事件,非 macOS 平台关闭所有窗口时退出应用;
  • 窗口标题动态更新:文件打开 / 保存后更新窗口标题,符合桌面应用习惯。

2. 原生文件操作

  • 文件对话框:使用 dialog.showOpenDialogSync/dialog.showSaveDialogSync 弹出系统原生文件选择 / 保存对话框,支持过滤文件类型;
  • 文件读写:通过 Node.js 的 fs 模块实现本地文件的同步 / 异步读写,主进程负责文件操作(渲染进程也可直接调用,但主进程更安全);
  • 状态管理:渲染进程维护当前文件路径和修改状态,保存后更新状态提示。

3. 全局快捷键

  • 快捷键注册:通过 globalShortcut 注册 Ctrl+N/O/S 全局快捷键,即使应用未激活也可触发(生产环境需注意快捷键冲突);
  • 快捷键注销:应用退出时通过 will-quit 事件注销所有快捷键,避免内存泄漏;
  • 按钮提示:UI 按钮标注快捷键,提升用户体验。

4. IPC 通信

  • 主进程 → 渲染进程:通过 webContents.send 向渲染进程发送文件内容、保存状态等;
  • 渲染进程 → 主进程:通过 ipcRenderer.send 向主进程发起文件操作请求;
  • 双向通信:主进程通过 ipcMain.on 监听请求,处理完成后通过 event.reply 回复渲染进程。

5. Markdown 实时预览

  • marked 解析:使用 marked 库将 Markdown 文本解析为 HTML,渲染到预览区;
  • 实时更新:监听编辑区 input 事件,实时更新预览内容;
  • 状态提示:通过状态栏提示文件修改 / 保存状态,提升交互体验。

五、运行与打包

1. 运行应用

npm start

启动后将看到:

  • 左侧编辑区默认显示示例 Markdown 内容;
  • 右侧实时预览解析后的 HTML;
  • 点击「新建 / 打开 / 保存」按钮或使用快捷键可操作文件;
  • 编辑内容后状态栏提示「已修改(未保存)」,保存后更新为「已保存」。

2. 打包应用

先安装打包工具 electron-packager

npm install electron-packager --save-dev

执行打包命令:

# 打包全平台
npm run package

# 仅打包 Windows 平台
electron-packager . MarkdownEditor --platform=win32 --arch=x64 --out=dist

# 仅打包 macOS 平台
electron-packager . MarkdownEditor --platform=darwin --arch=x64 --out=dist

# 仅打包 Linux 平台
electron-packager . MarkdownEditor --platform=linux --arch=x64 --out=dist

打包产物将生成在 dist 目录下,可直接运行(Windows 为 .exe,macOS 为 .app,Linux 为可执行文件)。

六、进阶优化方向

1. 功能扩展

  • 代码高亮:集成 highlight.js 实现代码块语法高亮;
  • 自动保存:定时自动保存编辑内容到临时文件,防止意外关闭丢失数据;
  • 多标签页:使用 electron-tabs 实现多文件同时编辑;
  • 导出功能:集成 html-pdf 将 Markdown 导出为 HTML/PDF/ 图片;
  • 自定义主题:支持亮色 / 暗色主题切换,保存主题偏好到本地。

2. 性能优化

  • 防抖渲染:编辑内容变化时添加防抖(如 300ms),避免频繁解析 Markdown;
  • 进程通信优化:使用 ipcRenderer.invoke 替代同步通信,避免阻塞渲染进程;
  • 内存管理:关闭标签页时释放资源,监听 memory-pressure 事件优化内存占用。

3. 生产环境适配

  • 签名打包:为 Windows/macOS 应用添加数字签名,避免系统报毒;
  • 自动更新:集成 electron-updater 实现应用自动更新;
  • 错误监控:集成 sentry 捕获应用崩溃日志;
  • 安全加固:关闭 nodeIntegration,使用预加载脚本(preload.js)安全暴露 API。

4. 跨平台细节适配

  • 快捷键适配:macOS 下将 Ctrl 替换为 Command
  • 窗口行为适配:macOS 下支持窗口全屏、缩放,Windows 下支持任务栏图标;
  • 文件路径适配:使用 path 模块处理不同平台的路径分隔符(\//)。

七、总结

本文以 Markdown 编辑器为例,完整实现了 Electron 桌面应用的核心开发流程,覆盖:

  1. 进程模型:主进程负责原生能力,渲染进程负责 UI 交互,通过 IPC 实现通信;
  2. 原生 API:文件操作、窗口管理、快捷键、系统对话框等桌面端核心能力;
  3. 跨平台特性:一套代码适配 Windows/macOS/Linux,仅需少量平台适配代码;
  4. 用户体验:实时预览、状态提示、快捷键等符合桌面应用的交互逻辑。

Electron 的优势在于前端开发者零成本迁移到桌面开发,无需学习 C++/Objective-C 等原生语言,即可快速构建功能丰富的跨平台桌面应用。本文案例可作为 Electron 入门的基础模板,扩展后可应用于文档编辑、代码编辑器、管理后台等多种桌面场景。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐