Electron 实战:构建跨平台桌面端 Markdown 编辑器(含实时预览、文件操作、快捷键)
·
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 桌面应用的核心开发流程,覆盖:
- 进程模型:主进程负责原生能力,渲染进程负责 UI 交互,通过 IPC 实现通信;
- 原生 API:文件操作、窗口管理、快捷键、系统对话框等桌面端核心能力;
- 跨平台特性:一套代码适配 Windows/macOS/Linux,仅需少量平台适配代码;
- 用户体验:实时预览、状态提示、快捷键等符合桌面应用的交互逻辑。
Electron 的优势在于前端开发者零成本迁移到桌面开发,无需学习 C++/Objective-C 等原生语言,即可快速构建功能丰富的跨平台桌面应用。本文案例可作为 Electron 入门的基础模板,扩展后可应用于文档编辑、代码编辑器、管理后台等多种桌面场景。
更多推荐




所有评论(0)