我给自己写了一个本地 Markdown 知识库:双链、全文搜索与 UI 全自定义实录
目录
- 缘起:为什么我不想要任何云端笔记软件
- 需求拆解:从"能记笔记"到"能找到笔记"
- 技术选型:浏览器 + 一个文件夹就够了
- 核心实现一:双链与反向引用
- 核心实现二:本地全文搜索
- UI 定制:知识库也是"自己的房间"
- 三个让我卡了一阵的坑
- 结尾:工具型软件的"长期主义"
1. 缘起:为什么我不想要任何云端笔记软件
我有几百篇 Markdown 笔记,散落在一个文件夹里——读书笔记、技术复盘、灵感片段、项目文档。它们都是我的,但我经常找不到。
试过几个主流方案,体验都让我皱眉:
- 云端笔记要登录、要同步、要担心哪天收费策略变了;
- 双链笔记软件功能强大,但语法学习成本高,UI 也改不动;
- 代码编辑器 + 文件树能看,但没有"反向链接"“全文搜索”"关系图"这些知识管理该有的能力。
我的诉求其实很朴素:让一堆 Markdown 文件,像一个知识库那样能被检索、能被关联、能被我自己定义样子。既然现成的都不顺手,那就自己写一个。
2. 需求拆解:从"能记笔记"到"能找到笔记"
和写任何工具一样,先把"做一个知识库"拆成可验收清单:
| 诉求(人话) | 拆解后(可验收) |
|---|---|
| 能看笔记 | 选文件夹 → 列出所有 .md → 点击渲染预览 |
| 能编辑 | 内置编辑器,保存即写回原文件 |
| 能互链 | [[笔记名]] 语法可跳转,并能查看"哪些笔记引用了当前这篇" |
| 能搜索 | 全文搜索,支持高亮关键词、按修改时间排序 |
| 能看关系 | 简单的关系图:节点是笔记,边是引用 |
| UI 我说了算 | 整体视觉风格、编辑/预览布局可自定义 |
这张表决定了知识库的骨架。功能不多,但每一项都做到位——这是我对所有"自己用的工具"的一贯要求。
3. 技术选型:浏览器 + 一个文件夹就够了
我犹豫过要不要上 Electron 或 Tauri,但仔细想需求——我不需要系统级文件监听、不需要托盘、不需要原生能力,浏览器 + File System Access API 完全够用:
| 能力 | 浏览器方案 |
|---|---|
| 选文件夹 | showDirectoryPicker()(File System Access API) |
| 读文件 | file.getFile() + text() |
| 写回 | createWritable() + write() |
| 渲染 Markdown | marked 或 markdown-it |
| 全文搜索 | 自建内存索引,几百篇规模毫秒级 |
整个知识库就是一个 HTML 文件,双击就能开。没有安装包、没有更新提示、没有后台进程——文件还是文件,软件只是"看文件的一个视角"。
心得:选型时先问"我到底需要什么",而不是"什么技术更高级"。本地小工具的最高境界是双击即用、关掉即走。
4. 核心实现一:双链与反向引用
双链的核心是两件事:正向跳转和反向引用。
正向跳转:渲染 Markdown 时,把 [[笔记名]] 替换成指向该笔记的链接:
html = html.replace(/\[\[(.+?)\]\]/g, (_, name) => {
const target = notes.find(n => n.title === name);
return target
? `<a class="wikilink" data-id="${target.id}">${name}</a>`
: `<span class="wikilink-broken">${name}</span>`;
});
反向引用:对每篇笔记,扫描所有其他笔记里出现的 [[本篇名]],建立反向索引:
const backlinks = notes.filter(n =>
n.content.includes(`[[${currentNote.title}]]`)
);
两个细节值得说:
- 断链要可视化。链接指向不存在的笔记时,用不同样式标红——一眼看出"我引用了一篇还没写的笔记",这本身就是知识管理的提示;
- 反向引用是知识库的灵魂。正向链接谁都会加,但"哪些笔记提到了我"才是发现隐性关联的关键,这是普通文件夹给不了的能力。
5. 核心实现二:本地全文搜索
几百篇笔记的全文搜索,不需要 Elasticsearch,一个内存索引就够:
// 建索引:每篇笔记的标题 + 正文分词后存进 Map
const index = new Map();
for (const note of notes) {
const tokens = tokenize(note.title + ' ' + note.content);
for (const token of tokens) {
if (!index.has(token)) index.set(token, new Set());
index.get(token).add(note.id);
}
}
// 查询:取所有 token 命中的笔记求交集
function search(query) {
const tokens = tokenize(query);
return tokens
.map(t => index.get(t) || new Set())
.reduce((a, b) => new Set([...a].filter(x => b.has(x))));
}
几个让搜索"好用"的细节:
- 标题命中权重更高,排序时优先返回标题匹配的笔记;
- 预览片段:结果里显示关键词前后各 30 字的上下文,并高亮关键词;
- 支持中文:中文不分词,直接按字符建索引,避免引入分词库的复杂度。
心得:搜索体验的好坏,不在"能不能搜到",而在"搜到后能不能一眼判断要不要点进去"。预览片段和高亮,比搜索算法本身更影响体验。
6. UI 定制:知识库也是"自己的房间"
本地工具最大的特权,是 UI 完全我说了算。我给自己定了几个商业软件不会这么做的设计选择:
- 三栏布局:文件树 / 编辑器 / 预览,比例可拖拽,默认 2:3:3;
- 关系图是入口,不是装饰。首页不是文件列表,而是笔记关系图——打开就看见"我的知识长什么样";
- 暗色为主,跟随系统。长时间读写笔记,暗色更护眼;
- 每篇笔记顶部显示反向引用数。一眼看出"这篇被多少其他笔记引用",是被引用多的核心笔记还是孤立片段。
这些设计在商业产品里几乎不可能出现——它们太"个人化"了。但本地工具的意义,恰恰就是让软件贴合你的思维习惯,而不是让你迁就软件的功能预设。
7. 三个让我卡了一阵的坑
坑一:File System Access API 的权限持久化。 每次刷新都要重新授权选文件夹,体验很差。解决办法是用 IndexedDB 把 directory handle 序列化存下来,下次启动直接恢复——浏览器允许这种"权限记忆",但需要用户之前授权过。
坑二:Markdown 渲染与双链的优先级。 marked 会把 [[xxx]] 里的方括号当特殊字符处理,导致双链语法被破坏。解决办法是在 Markdown 渲染之前先替换 [[xxx]] 为占位符,渲染后再替换回链接——顺序不能反。
坑三:大笔记的编辑卡顿。 一篇几千行的笔记,每次输入都重新渲染预览会卡。改成预览防抖(debounce 300ms) + 只渲染可视区域后丝滑。教训:任何"输入即渲染"的场景,防抖是必须的,不是可选的。
8. 结尾:工具型软件的"长期主义"
做完这个知识库,我对"工具型产品"有了一种更深的体会:
- 它和你一起成长。UI、快捷键、字段结构都能随你的思维习惯调整,软件不会"卡"在你的工作流之外;
- 它尊重你的数据。笔记就是硬盘里的
.md文件,知识库只是"看它们的一个视角"——换工具也不丢数据; - 它尊重你的注意力。没有推荐位、没有红点、没有"今日总结"弹窗,打开就是为了读写。
在这个所有软件都在抢用户时长的时代,一个"打开就做事、关掉就消失"的本地工具,反而成了一种奢侈。
如果你也有一堆 Markdown 笔记、也想要一个"自己的知识库",我强烈建议自己写一个。技术门槛比想象中低,一个 HTML 文件就够;真正难的是克制住加功能的冲动——不做社区、不做分享、不做"发现",就做一个老老实实的本地知识库。
工具小而准,价值往往不在多,而在"它永远在那里,永远贴合你"。
- 点赞
- 收藏
- 关注作者
评论(0)