Electron笔记应用:Markdown编辑与云同步

Electron笔记应用:Markdown编辑与云同步

【免费下载链接】electron 使用Electron构建跨平台桌面应用程序,支持JavaScript、HTML和CSS 【免费下载链接】electron 项目地址: https://gitcode.com/GitHub_Trending/el/electron

还在为跨平台笔记应用的选择而烦恼?想要一个既支持Markdown实时预览又能自动同步的桌面应用?本文将带你从零构建一个功能完整的Electron笔记应用,实现Markdown编辑、实时预览和云同步功能。

你将学到什么

  • Electron应用基础架构搭建
  • Markdown编辑器与实时预览实现
  • 进程间通信(IPC)最佳实践
  • 文件系统操作与数据持久化
  • 云存储API集成与同步策略
  • 应用打包与分发

技术栈概览

mermaid

项目初始化与基础架构

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实时编辑与预览
  • ✅ 本地文件存储与管理
  • ✅ 进程间安全通信
  • ✅ 响应式用户界面

进阶特性

  • 🔄 云同步架构设计
  • 📦 应用打包与分发
  • 🚀 性能优化策略
  • 🛡️ 错误处理机制

后续扩展方向

  1. 插件系统:支持第三方插件扩展功能
  2. 协作编辑:实现多用户实时协作
  3. AI辅助:集成AI写作助手功能
  4. 移动端同步:开发配套移动应用

这个Electron笔记应用不仅展示了现代桌面应用开发的最佳实践,更为你提供了进一步探索桌面应用开发的坚实基础。现在就开始构建你的跨平台笔记应用吧!

立即行动

  1. 克隆项目代码并运行体验
  2. 根据需求自定义功能扩展
  3. 分享你的开发经验和改进建议

期待看到你基于这个模板创造的精彩应用!

【免费下载链接】electron 使用Electron构建跨平台桌面应用程序,支持JavaScript、HTML和CSS 【免费下载链接】electron 项目地址: https://gitcode.com/GitHub_Trending/el/electron

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值