用 Electron + Express 一周末做了个本地 Markdown 桌面笔记应用
记录从零到打包发布的完整过程,以及一些踩过的坑。
最近整理笔记的需求变得越来越强烈——Notion 网速偶尔抽风、Obsidian 功能强大但插件生态需要学习曲线、Typora 又不能管理目录树。于是决定自己动手做一个刚刚好的工具:简单笔记 SimpleNote。
目标
几条核心原则,从一开始就确定:
- 纯本地,不上云:文件完全在自己硬盘,永远不依赖服务器
- 文件夹即工作区:任意本地目录即可成为知识库,原始文件格式(
.md)不锁定 - 轻量,不过度设计:能用标准 Node.js 完成的,绝不引入复杂框架
技术选型
| 层次 | 技术 |
|---|---|
| 桌面容器 | Electron 28 |
| 后端服务 | Express 4(Node.js) |
| 前端渲染 | 原生 HTML / CSS / JS |
| Markdown 解析 | marked.js |
| 代码高亮 | highlight.js |
| 打包工具 | electron-builder |
为什么选 Electron + Express?
这个组合看起来”重”,但对于这个场景其实是最简单的:
- 所有文件读写操作(
fs模块)放在 Express 里,通过 REST API 暴露给前端 - Electron 只做一件事:把 Express 服务在后台跑起来,再打开一个 BrowserWindow 加载它
- 前端无需任何框架,纯 JS 调用
fetch即可
这意味着整个架构对任何写过 Node.js 的人来说都非常透明,没有黑盒。
架构设计
main.js(Electron 主进程)
└─ startServer() ← 来自 server.js
└─ BrowserWindow.loadURL("http://localhost:{port}")
server.js(Express API 服务)
├─ GET /api/tree — 获取目录树
├─ GET /api/file — 读取文件内容
├─ POST /api/file — 新建文件
├─ PUT /api/file — 保存文件
├─ DELETE /api/file — 删除文件
├─ POST /api/folder — 新建目录
├─ DELETE /api/folder — 删除目录
├─ POST /api/rename — 重命名
├─ GET /api/search — 全文搜索
├─ GET /api/browse — 浏览本地文件系统(工作区切换器)
└─ POST /api/config/root — 切换工作区目录
public/
├─ index.html
└─ js/
├─ app.js — 全局状态、事件绑定、Modal、Toast
├─ sidebar.js — 文件树渲染、右键菜单
├─ editor.js — Markdown 编辑器、格式工具栏
└─ search.js — 全文搜索 Spotlight 面板
关键实现:动态端口
Electron 应用和普通 Web 不同,端口 3000 完全可能被系统其他服务占用。解决方案是让 Express 监听 0(由操作系统分配空闲端口),再通过 server.address().port 拿到实际端口:
// server.js
function startServer(port = 3000) {
return new Promise((resolve) => {
const server = app.listen(port, () => {
resolve(server.address().port);
});
});
}
// Electron 通过 require.main === module 判断是否直接运行,
// 避免 Electron 加载时也执行 listen(3000)
if (require.main === module) startServer(3000);
module.exports = { startServer };
// main.js
const { startServer } = require('./server');
async function createWindow() {
const port = await startServer(0); // 0 = 随机空闲端口
mainWindow.loadURL(`http://localhost:${port}`);
}
功能亮点
格式化工具栏
在编辑器的 textarea 上方放了一排格式按钮,点击后通过 selectionStart / selectionEnd 在光标位置精确插入 Markdown 语法,并自动选中占位文字:
insertFormat(type) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
const prefix = '**', suffix = '**';
const innerText = selectedText || '加粗文本';
const replacement = prefix + innerText + suffix;
textarea.value = text.substring(0, start) + replacement + text.substring(end);
textarea.setSelectionRange(start + prefix.length, start + prefix.length + innerText.length);
}
本地文件系统浏览器
工作区切换器需要让用户在客户端内浏览本地磁盘目录,不能直接用浏览器的 <input type="file"> webkitdirectory(限制太多)。所以在后端提供了 /api/browse 接口:
- 无参数→返回所有可用盘符(Windows 枚举 A-Z)
- 带
?dir=<path>参数→返回该目录下的子目录列表
前端做成双击进入、单击选中的交互,体验接近系统原生的文件选择对话框。
侧边栏拖拽的一个坑
拖拽调整侧边栏宽度时,CSS 的 transition 会产生明显的延迟感(拖动不跟手)。解决方法很简单:拖拽开始时临时禁用 transition,松开后恢复:
handle.addEventListener('mousedown', () => {
sidebar.style.transition = 'none'; // 拖拽时禁动画
});
document.addEventListener('mouseup', () => {
sidebar.style.transition = ''; // 松开后恢复
});
打包
使用 electron-builder,先用 electron-icon-maker 将设计好的 1024×1024 PNG 图标转换为 ICO / ICNS 格式:
npx electron-icon-maker --input=build/icon.png --output=build
然后在 package.json 配置图标路径:
"build": {
"appId": "com.simplenote.app",
"productName": "简单笔记 SimpleNote",
"win": {
"icon": "build/icons/win/icon.ico",
"target": ["nsis", "portable"]
}
}
运行 npm run dist 后,输出目录将包含安装包(.exe)和绿色便携版,可直接分发。
总结
整个项目代码量不大(后端约 420 行、前端共约 1000 行),但日常使用完全够用。设计上坚持「能不引入的依赖就不引入」,所有文件操作直接使用 Node.js 内置 fs 模块,无编译步骤,修改即生效。
免费下载地址:https://wuyouge.lanzout.com/ibZjw3lidpne
如果你也对”刚刚好”的工具感兴趣,不妨自己动手试一试——从 Express 到 Electron 的距离,比你想象的要近很多。
更多AI编程开发技巧,请关注本站,想与作者交流学习,可➕微信:wuyouseo









