Electron笔记应用:Markdown编辑与云同步
还在为跨平台笔记应用的选择而烦恼?想要一个既支持Markdown实时预览又能自动同步的桌面应用?本文将带你从零构建一个功能完整的Electron笔记应用,实现Markdown编辑、实时预览和云同步功能。
你将学到什么
- Electron应用基础架构搭建
- Markdown编辑器与实时预览实现
- 进程间通信(IPC)最佳实践
- 文件系统操作与数据持久化
- 云存储API集成与同步策略
- 应用打包与分发
技术栈概览
项目初始化与基础架构
1. 创建项目结构
mkdir electron-note-app
cd electron-note-app
npm init -y
npm install electron --save-dev
npm install marked highlight.js --save
2. 基础package.json配置
{
"name": "electron-note-app",
"version": "1.0.0",
"description": "A Markdown note-taking app with cloud sync",
"main": "src/main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder",
"dev": "electron . --dev"
},
"devDependencies": {
"electron": "^27.0.0",
"electron-builder": "^24.6.4"
},
"dependencies": {
"marked": "^5.1.1",
"highlight.js": "^11.9.0"
}
}
3. 主进程入口文件
// src/main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const fs = require('fs').promises;
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: path.join(__dirname, 'preload.js')
},
titleBarStyle: 'hiddenInset',
show: false
});
mainWindow.loadFile('src/renderer/index.html');
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
// 开发环境下打开DevTools
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools();
}
}
// 应用生命周期管理
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// IPC处理函数
ipcMain.handle('save-note', async (event, { content, filename }) => {
try {
const notesDir = path.join(app.getPath('userData'), 'notes');
await fs.mkdir(notesDir, { recursive: true });
const filePath = path.join(notesDir, filename);
await fs.writeFile(filePath, content, 'utf-8');
return { success: true, path: filePath };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('load-note', async (event, filename) => {
try {
const filePath = path.join(app.getPath('userData'), 'notes', filename);
const content = await fs.readFile(filePath, 'utf-8');
return { success: true, content };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('list-notes', async () => {
try {
const notesDir = path.join(app.getPath('userData'), 'notes');
await fs.mkdir(notesDir, { recursive: true });
const files = await fs.readdir(notesDir);
const notes = files.filter(file => file.endsWith('.md'));
return { success: true, notes };
} catch (error) {
return { success: false, error: error.message };
}
});
预加载脚本与安全通信
// src/preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// 文件操作API
saveNote: (content, filename) =>
ipcRenderer.invoke('save-note', { content, filename }),
loadNote: (filename) =>
ipcRenderer.invoke('load-note', filename),
listNotes: () =>
ipcRenderer.invoke('list-notes'),
// 系统对话框
showSaveDialog: (options) =>
ipcRenderer.invoke('show-save-dialog', options),
showOpenDialog: (options) =>
ipcRenderer.invoke('show-open-dialog', options),
// 应用事件
onMenuAction: (callback) =>
ipcRenderer.on('menu-action', callback),
removeAllListeners: (channel) =>
ipcRenderer.removeAllListeners(channel)
});
Markdown编辑器实现
1. 编辑器界面结构
<!-- src/renderer/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Electron笔记应用</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="app-container">
<header class="app-header">
<h1>📝 Electron笔记</h1>
<div class="header-actions">
<button id="new-note">新建</button>
<button id="save-note">保存</button>
<button id="sync-notes">同步</button>
</div>
</header>
<div class="main-content">
<aside class="sidebar">
<div class="sidebar-header">
<h3>我的笔记</h3>
<button id="refresh-notes">刷新</button>
</div>
<ul id="notes-list" class="notes-list"></ul>
</aside>
<main class="editor-container">
<div class="editor-toolbar">
<input type="text" id="note-title" placeholder="笔记标题">
<div class="toolbar-actions">
<button data-action="bold">粗体</button>
<button data-action="italic">斜体</button>
<button data-action="link">链接</button>
<button data-action="code">代码</button>
</div>
</div>
<div class="editor-panels">
<div class="editor-panel">
<textarea id="markdown-editor" placeholder="开始编写你的Markdown笔记..."></textarea>
</div>
<div class="preview-panel">
<div id="markdown-preview" class="markdown-preview"></div>
</div>
</div>
</main>
</div>
<footer class="app-footer">
<span id="status-text">就绪</span>
<span id="word-count">0字</span>
</footer>
</div>
<script src="renderer.js"></script>
</body>
</html>
2. 样式设计
/* src/renderer/styles.css */
:root {
--primary-color: #2c3e50;
--secondary-color: #34495e;
--accent-color: #3498db;
--text-color: #2c3e50;
--bg-color: #ecf0f1;
--sidebar-bg: #2c3e50;
--sidebar-text: #ecf0f1;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
}
.app-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.app-header {
background: var(--primary-color);
color: white;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-actions button {
background: var(--accent-color);
border: none;
padding: 0.5rem 1rem;
margin-left: 0.5rem;
border-radius: 4px;
color: white;
cursor: pointer;
}
.main-content {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 250px;
background: var(--sidebar-bg);
color: var(--sidebar-text);
padding: 1rem;
}
.notes-list {
list-style: none;
margin-top: 1rem;
}
.notes-list li {
padding: 0.5rem;
cursor: pointer;
border-radius: 4px;
margin-bottom: 0.25rem;
}
.notes-list li:hover {
background: rgba(255, 255, 255, 0.1);
}
.editor-container {
flex: 1;
display: flex;
flex-direction: column;
}
.editor-toolbar {
padding: 1rem;
background: white;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
}
#note-title {
border: none;
font-size: 1.2rem;
font-weight: bold;
outline: none;
flex: 1;
}
.editor-panels {
flex: 1;
display: flex;
overflow: hidden;
}
.editor-panel, .preview-panel {
flex: 1;
padding: 1rem;
}
#markdown-editor {
width: 100%;
height: 100%;
border: none;
resize: none;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 14px;
line-height: 1.6;
outline: none;
}
.markdown-preview {
height: 100%;
overflow-y: auto;
padding: 1rem;
background: white;
border-radius: 4px;
}
.app-footer {
padding: 0.5rem 1rem;
background: white;
border-top: 1px solid #ddd;
display: flex;
justify-content: space-between;
font-size: 0.9rem;
color: #666;
}
3. 渲染进程逻辑
// src/renderer/renderer.js
class NoteApp {
constructor() {
this.currentNote = null;
this.isDirty = false;
this.init();
}
async init() {
await this.loadNotesList();
this.setupEventListeners();
this.setupEditor();
this.updateStatus('应用已就绪');
}
setupEventListeners() {
// 编辑器输入监听
document.getElementById('markdown-editor').addEventListener('input', (e) => {
this.isDirty = true;
this.updatePreview(e.target.value);
this.updateWordCount(e.target.value);
});
// 保存按钮
document.getElementById('save-note').addEventListener('click', () => {
this.saveCurrentNote();
});
// 新建笔记
document.getElementById('new-note').addEventListener('click', () => {
this.createNewNote();
});
// 同步按钮
document.getElementById('sync-notes').addEventListener('click', () => {
this.syncNotes();
});
// 刷新列表
document.getElementById('refresh-notes').addEventListener('click', () => {
this.loadNotesList();
});
// 标题输入
document.getElementById('note-title').addEventListener('input', (e) => {
this.isDirty = true;
});
}
setupEditor() {
// 初始化Markdown解析器
this.marked = window.marked;
this.marked.setOptions({
highlight: function(code, lang) {
if (window.hljs && lang) {
return window.hljs.highlight(code, { language: lang }).value;
}
return code;
}
});
}
async loadNotesList() {
try {
const result = await window.electronAPI.listNotes();
if (result.success) {
this.renderNotesList(result.notes);
} else {
this.showError('加载笔记列表失败: ' + result.error);
}
} catch (error) {
this.showError('加载笔记列表时出错: ' + error.message);
}
}
renderNotesList(notes) {
const listElement = document.getElementById('notes-list');
listElement.innerHTML = '';
notes.forEach(note => {
const li = document.createElement('li');
li.textContent = note.replace('.md', '');
li.addEventListener('click', () => {
this.loadNote(note);
});
listElement.appendChild(li);
});
}
async loadNote(filename) {
if (this.isDirty) {
if (!confirm('当前笔记未保存,是否继续?')) {
return;
}
}
try {
const result = await window.electronAPI.loadNote(filename);
if (result.success) {
this.currentNote = filename;
document.getElementById('note-title').value = filename.replace('.md', '');
document.getElementById('markdown-editor').value = result.content;
this.updatePreview(result.content);
this.updateWordCount(result.content);
this.isDirty = false;
this.updateStatus(`已加载: ${filename}`);
} else {
this.showError('加载笔记失败: ' + result.error);
}
} catch (error) {
this.showError('加载笔记时出错: ' + error.message);
}
}
async saveCurrentNote() {
const title = document.getElementById('note-title').value.trim();
const content = document.getElementById('markdown-editor').value;
if (!title) {
this.showError('请输入笔记标题');
return;
}
const filename = `${title}.md`;
try {
const result = await window.electronAPI.saveNote(content, filename);
if (result.success) {
this.currentNote = filename;
this.isDirty = false;
this.updateStatus(`已保存: ${filename}`);
this.loadNotesList(); // 刷新列表
} else {
this.showError('保存失败: ' + result.error);
}
} catch (error) {
this.showError('保存时出错: ' + error.message);
}
}
createNewNote() {
if (this.isDirty) {
if (!confirm('当前笔记未保存,是否继续?')) {
return;
}
}
this.currentNote = null;
document.getElementById('note-title').value = '';
document.getElementById('markdown-editor').value = '';
document.getElementById('markdown-preview').innerHTML = '';
document.getElementById('word-count').textContent = '0字';
this.isDirty = false;
this.updateStatus('新建笔记');
}
updatePreview(markdown) {
const previewElement = document.getElementById('markdown-preview');
previewElement.innerHTML = this.marked.parse(markdown);
}
updateWordCount(text) {
const wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
document.getElementById('word-count').textContent = `${wordCount}字`;
}
updateStatus(message) {
document.getElementById('status-text').textContent = message;
}
showError(message) {
this.updateStatus('错误: ' + message);
console.error(message);
}
async syncNotes() {
this.updateStatus('同步中...');
// 云同步逻辑将在后续实现
setTimeout(() => {
this.updateStatus('同步完成');
}, 2000);
}
}
// 应用启动
document.addEventListener('DOMContentLoaded', () => {
new NoteApp();
});
云同步功能实现
1. 云存储服务接口
// src/cloud/syncService.js
class CloudSyncService {
constructor() {
this.isSyncing = false;
this.lastSyncTime = null;
}
async initialize() {
// 初始化云存储配置
this.config = await this.loadConfig();
}
async syncAllNotes() {
if (this.isSyncing) return;
this.isSyncing = true;
try {
// 获取本地笔记列表
const localNotes = await this.getLocalNotes();
// 获取云端笔记列表
const cloudNotes = await this.getCloudNotes();
// 执行同步逻辑
await this.performSync(localNotes, cloudNotes);
this.lastSyncTime = new Date();
return { success: true, timestamp: this.lastSyncTime };
} catch (error) {
console.error('同步失败:', error);
return { success: false, error: error.message };
} finally {
this.isSyncing = false;
}
}
async performSync(localNotes, cloudNotes) {
const operations = [];
// 检测需要上传的笔记
for (const localNote of localNotes) {
const cloudNote = cloudNotes.find(n => n.filename === localNote.filename);
if (!cloudNote || localNote.modified > cloudNote.modified) {
operations.push(this.uploadNote(localNote));
}
}
// 检测需要下载的笔记
for (const cloudNote of cloudNotes) {
const localNote = localNotes.find(n => n.filename === cloudNote.filename);
if (!localNote || cloudNote.modified > localNote.modified) {
operations.push(this.downloadNote(cloudNote));
}
}
// 执行所有操作
await Promise.all(operations);
}
async uploadNote(note) {
// 实现上传逻辑
console.log('上传笔记:', note.filename);
}
async downloadNote(note) {
// 实现下载逻辑
console.log('下载笔记:', note.filename);
}
async getLocalNotes() {
// 获取本地笔记信息
const notesDir = path.join(app.getPath('userData'), 'notes');
const files = await fs.readdir(notesDir);
const notes = [];
for (const file of files.filter(f => f.endsWith('.md'))) {
const stats = await fs.stat(path.join(notesDir, file));
notes.push({
filename: file,
modified: stats.mtime,
size: stats.size
});
}
return notes;
}
async getCloudNotes() {
// 从云端获取笔记列表
// 这里需要实现具体的云存储API调用
return [];
}
async loadConfig() {
// 加载云存储配置
const configPath = path.join(app.getPath('userData'), 'cloud-config.json');
try {
const configData = await fs.readFile(configPath, 'utf-8');
return JSON.parse(configData);
} catch {
return {
provider: 'webdav',
enabled: false,
syncInterval: 300000 // 5分钟
};
}
}
async saveConfig(config) {
const configPath = path.join(app.getPath('userData'), 'cloud-config.json');
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
this.config = config;
}
}
module.exports = CloudSyncService;
2. 同步配置界面
<!-- src/renderer/sync-settings.html -->
<div class="sync-settings">
<h2>云同步设置</h2>
<div class="setting-group">
<label>
<input type="checkbox" id="sync-enabled"> 启用云同步
</label>
</div>
<div class="setting-group">
<label>同步服务提供商</label>
<select id="sync-provider">
<option value="webdav">WebDAV</option>
<option value="dropbox">Dropbox</option>
<option value="googledrive">Google Drive</option>
</select>
</div>
<div class="setting-group" id="webdav-settings">
<label>WebDAV服务器地址</label>
<input type="url" id="webdav-url" placeholder="https://your-webdav-server.com">
<label>用户名</label>
<input type="text" id="webdav-username">
<label>密码</label>
<input type="password" id="webdav-password">
</div>
<div class="setting-group">
<label>同步间隔</label>
<select id="sync-interval">
<option value="300000">5分钟</option>
<option value="900000">15分钟</option>
<option value="1800000">30分钟</option>
<option value="3600000">1小时</option>
</select>
</div>
<div class="setting-actions">
<button id="test-connection">测试连接</button>
<button id="save-settings">保存设置</button>
<button id="sync-now">立即同步</button>
</div>
<div class="sync-status">
<h3>同步状态</h3>
<p>最后同步时间: <span id="last-sync-time">从未同步</span></p>
<p>同步状态: <span id="sync-status">未启用</span></p>
</div>
</div>
应用打包与分发
1. 构建配置
// package.json 构建配置
{
"build": {
"appId": "com.yourcompany.electron-note-app",
"productName": "Electron笔记",
"directories": {
"output": "dist"
},
"files": [
"src/**/*",
"node_modules/**/*"
],
"mac": {
"category": "public.app-category.productivity",
"icon": "assets/icon.icns"
},
"win": {
"target": "nsis",
"icon": "assets/icon.ico"
},
"linux": {
"target": "AppImage",
"icon": "assets/icon.png"
}
}
}
2. 构建脚本
# 安装electron-builder
npm install electron-builder --save-dev
# 构建应用
npm run build
# 构建特定平台
npm run build -- --mac
npm run build -- --win
npm run build -- --linux
性能优化与最佳实践
1. 内存管理
// 优化大型文件处理
class MemoryManager {
static optimizeLargeFileHandling(content) {
// 分块处理大文件
const CHUNK_SIZE = 1024 * 1024; // 1MB
if (content.length > CHUNK_SIZE) {
return this.processInChunks(content, CHUNK_SIZE);
}
return content;
}
static processInChunks(content, chunkSize) {
const chunks = [];
for (let i = 0; i < content.length; i += chunkSize) {
chunks.push(content.slice(i, i + chunkSize));
}
return chunks;
}
}
2. 错误处理与日志
// 错误处理中间件
class ErrorHandler {
static setupGlobalHandlers() {
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error);
// 记录到文件或发送到监控服务
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
});
}
static logError(error, context = {}) {
const logEntry = {
timestamp: new Date().toISOString(),
error: error.message,
stack: error.stack,
context
};
// 写入日志文件
this.writeToLogFile(logEntry);
}
static async writeToLogFile(entry) {
const logDir = path.join(app.getPath('userData'), 'logs');
await fs.mkdir(logDir, { recursive: true });
const logFile = path.join(logDir, 'error.log');
await fs.appendFile(logFile, JSON.stringify(entry) + '\n');
}
}
总结与展望
通过本教程,你已成功构建了一个功能完整的Electron笔记应用,具备以下特性:
核心功能
- ✅ Markdown实时编辑与预览
- ✅ 本地文件存储与管理
- ✅ 进程间安全通信
- ✅ 响应式用户界面
进阶特性
- 🔄 云同步架构设计
- 📦 应用打包与分发
- 🚀 性能优化策略
- 🛡️ 错误处理机制
后续扩展方向
- 插件系统:支持第三方插件扩展功能
- 协作编辑:实现多用户实时协作
- AI辅助:集成AI写作助手功能
- 移动端同步:开发配套移动应用
这个Electron笔记应用不仅展示了现代桌面应用开发的最佳实践,更为你提供了进一步探索桌面应用开发的坚实基础。现在就开始构建你的跨平台笔记应用吧!
立即行动:
- 克隆项目代码并运行体验
- 根据需求自定义功能扩展
- 分享你的开发经验和改进建议
期待看到你基于这个模板创造的精彩应用!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



