Electron 实战项目 - 构建完整的笔记应用
前言
理论学习很重要,但实战更能巩固知识。这篇文章将带你从零开始,构建一个功能完整的 Markdown 笔记应用,涵盖文件管理、编辑器集成、数据持久化等核心功能。
项目规划
功能需求
核心功能:
├── 笔记管理
│ ├── 创建笔记
│ ├── 编辑笔记
│ ├── 删除笔记
│ └── 搜索笔记
├── Markdown 编辑
│ ├── 实时预览
│ ├── 语法高亮
│ └── 快捷键支持
├── 文件系统
│ ├── 保存到本地
│ ├── 导入导出
│ └── 文件夹管理
└── 界面功能
├── 分栏布局
├── 主题切换
└── 全屏模式技术栈
前端:
- React
- Marked.js(Markdown 解析)
- CodeMirror(编辑器)
- TailwindCSS(样式)
后端:
- Electron
- Node.js fs 模块
- lowdb(本地数据库)项目搭建
bash
# 创建项目
npm create @quick-start/electron my-notes
# 安装依赖
cd my-notes
npm install marked codemirror lowdb@5.0.0
# 启动开发
npm run dev数据模型
javascript
// src/main/database.js
const { Low } = require('lowdb')
const { JSONFile } = require('lowdb/node')
const path = require('path')
const { app } = require('electron')
class Database {
constructor() {
const dbPath = path.join(app.getPath('userData'), 'notes.json')
const adapter = new JSONFile(dbPath)
this.db = new Low(adapter)
this.init()
}
async init() {
await this.db.read()
this.db.data = this.db.data || { notes: [], settings: {} }
await this.db.write()
}
async getNotes() {
await this.db.read()
return this.db.data.notes
}
async createNote(note) {
await this.db.read()
const newNote = {
id: Date.now().toString(),
title: note.title,
content: note.content,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
this.db.data.notes.push(newNote)
await this.db.write()
return newNote
}
async updateNote(id, updates) {
await this.db.read()
const note = this.db.data.notes.find(n => n.id === id)
if (note) {
Object.assign(note, updates, {
updatedAt: new Date().toISOString()
})
await this.db.write()
return note
}
throw new Error('Note not found')
}
async deleteNote(id) {
await this.db.read()
const index = this.db.data.notes.findIndex(n => n.id === id)
if (index !== -1) {
this.db.data.notes.splice(index, 1)
await this.db.write()
return true
}
return false
}
}
module.exports = new Database()主进程实现
javascript
// src/main/main.js
const { app, BrowserWindow, ipcMain, Menu } = require('electron')
const path = require('path')
const database = require('./database')
let mainWindow
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
mainWindow.loadFile('index.html')
createMenu()
}
function createMenu() {
const template = [
{
label: '文件',
submenu: [
{
label: '新建笔记',
accelerator: 'CmdOrCtrl+N',
click: () => {
mainWindow.webContents.send('menu:new-note')
}
},
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
click: () => {
mainWindow.webContents.send('menu:save')
}
},
{ type: 'separator' },
{
label: '退出',
accelerator: 'CmdOrCtrl+Q',
click: () => app.quit()
}
]
},
{
label: '编辑',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' }
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
// IPC 处理器
ipcMain.handle('notes:getAll', async () => {
return await database.getNotes()
})
ipcMain.handle('notes:create', async (event, note) => {
return await database.createNote(note)
})
ipcMain.handle('notes:update', async (event, id, updates) => {
return await database.updateNote(id, updates)
})
ipcMain.handle('notes:delete', async (event, id) => {
return await database.deleteNote(id)
})
app.whenReady().then(createWindow)前端实现
jsx
// src/renderer/App.jsx
import React, { useState, useEffect } from 'react'
import { marked } from 'marked'
import NoteList from './components/NoteList'
import Editor from './components/Editor'
import Preview from './components/Preview'
function App() {
const [notes, setNotes] = useState([])
const [currentNote, setCurrentNote] = useState(null)
const [content, setContent] = useState('')
useEffect(() => {
loadNotes()
window.electronAPI.onMenuCommand('new-note', handleNewNote)
window.electronAPI.onMenuCommand('save', handleSave)
}, [])
const loadNotes = async () => {
const allNotes = await window.electronAPI.getNotes()
setNotes(allNotes)
}
const handleNewNote = async () => {
const note = await window.electronAPI.createNote({
title: '新笔记',
content: ''
})
setNotes([...notes, note])
setCurrentNote(note)
setContent('')
}
const handleSave = async () => {
if (!currentNote) return
await window.electronAPI.updateNote(currentNote.id, {
content,
title: content.split('\n')[0].replace(/^#\s*/, '') || '无标题'
})
await loadNotes()
}
const handleSelectNote = (note) => {
setCurrentNote(note)
setContent(note.content)
}
const handleDeleteNote = async (id) => {
await window.electronAPI.deleteNote(id)
await loadNotes()
if (currentNote?.id === id) {
setCurrentNote(null)
setContent('')
}
}
return (
<div className="app">
<NoteList
notes={notes}
currentNote={currentNote}
onSelect={handleSelectNote}
onDelete={handleDeleteNote}
onNew={handleNewNote}
/>
<Editor
value={content}
onChange={setContent}
/>
<Preview
content={marked(content)}
/>
</div>
)
}
export default App编辑器组件
jsx
// src/renderer/components/Editor.jsx
import React, { useEffect, useRef } from 'react'
import { EditorState } from '@codemirror/state'
import { EditorView, keymap } from '@codemirror/view'
import { defaultKeymap } from '@codemirror/commands'
import { markdown } from '@codemirror/lang-markdown'
function Editor({ value, onChange }) {
const editorRef = useRef(null)
const viewRef = useRef(null)
useEffect(() => {
if (!editorRef.current) return
const startState = EditorState.create({
doc: value,
extensions: [
keymap.of(defaultKeymap),
markdown(),
EditorView.updateListener.of(update => {
if (update.docChanged) {
onChange(update.state.doc.toString())
}
})
]
})
viewRef.current = new EditorView({
state: startState,
parent: editorRef.current
})
return () => {
viewRef.current?.destroy()
}
}, [])
return (
<div className="editor">
<div ref={editorRef} />
</div>
)
}
export default Editor样式设计
css
/* src/renderer/styles.css */
.app {
display: grid;
grid-template-columns: 250px 1fr 1fr;
height: 100vh;
overflow: hidden;
}
.note-list {
border-right: 1px solid #e5e5e5;
overflow-y: auto;
background: #f8f9fa;
}
.note-item {
padding: 12px;
border-bottom: 1px solid #e5e5e5;
cursor: pointer;
transition: background 0.2s;
}
.note-item:hover {
background: #e9ecef;
}
.note-item.active {
background: #dee2e6;
}
.editor, .preview {
padding: 20px;
overflow-y: auto;
}
.preview {
border-left: 1px solid #e5e5e5;
background: white;
}打包配置
json
// package.json
{
"name": "my-notes",
"version": "1.0.0",
"main": "src/main/main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder"
},
"build": {
"appId": "com.example.my-notes",
"productName": "My Notes",
"directories": {
"output": "dist"
},
"mac": {
"category": "public.app-category.productivity"
},
"win": {
"target": "nsis"
},
"linux": {
"target": ["AppImage", "deb"]
}
}
}总结
通过这个实战项目,我们学习了:
- 完整的 Electron 项目结构
- 数据持久化方案
- Markdown 编辑器集成
- 文件管理实现
相关文章推荐: