FEATURED · 精选文章

开源Markdown在线笔记工具:以Git仓库为后端的完整实现指南

发布时间 / 2026/9/16 21:06:48
来源 / 创域科博编辑部
栏目 / 资讯中心
开源Markdown在线笔记工具:以Git仓库为后端的完整实现指南 前阵子我把自己的笔记系统整个推倒重来了一遍。起因很朴素——按我这种程序员的工作习惯笔记要跨电脑、跨系统、随时访问还得能跟代码、文档、博客内容打通。挑来挑去最后入坑了一个思路用开源Markdown在线笔记工具让笔记全量存储在Git仓库里。简单说就是网页上写Markdown保存时直接提交推送到GitHub、GitLab或Gitee仓库版本历史、多端同步、数据导出天然就有了。这个项目适合谁呢如果你是写技术文档的人、搞知识管理的重度用户或者手上正好有几个开源项目想顺便把笔记也变成可追踪版本的内容资产那这套方式会很对你的胃口。全文没有平台绑定、没有私有格式仓库在你手里编辑器用开源组件数据永远属于你自己。这篇就按我做这个项目时的完整思路从需求分析、技术选型、核心实现到踩坑记录一次性梳理清楚。1. 为什么要做一个以Git仓库为存储后端的笔记工具1.1 主流在线笔记工具的四个坑先说我自己踩过的坑。以前我试过好几款在线笔记记录这件事看起来简单用久了问题就全冒出来了。第一个坑是数据锁定。笔记写得越多越难迁移。很多工具虽然支持导出但导出来的格式、附带资源文件、目录结构不是你原样就能用的换到另一个工具简直像搬家一样痛苦。真正长期用下来你才会发现那些标签、双向链接、附件关联导出后全部失效平台更像一个围城。第二个坑是格式不通用。富文本编辑器的底层是一堆私有JSON或者HTML片段普通文本编辑器根本打不开。我有时候想把一段笔记贴到代码注释里或者在终端环境里快速看一眼某条记录就只能干瞪眼。而Markdown本身就是纯文本任何环境、任何设备都能读。第三个坑是同步冲突。我同时用办公室台式机、家里的笔记本和手机记笔记多端同时改一个文档笔记平台经常把我的修改合并得莫名其妙。也不是没有版本历史但版本历史往往只停留在“某一天某一次修改”真正要找回几个星期前的某个版本翻起来特别费劲。第四个坑是隐私边界模糊。在线笔记平台的服务器上躺着我的全部内容虽然多数都有加密传输可没有哪个能让我完全放心。基于这几个痛点我给自己定了硬性需求笔记必须用纯文本格式存储位置必须由我掌控同步和版本管理必须做到可追溯、可回滚。1.2 为什么Git天然适合做笔记后端Git这套东西程序员再熟悉不过了可如果把“代码”换成“笔记”它的价值依然是成立的。第一版本历史是白送的。每一次保存都是一个新的commit关键词搜索、diff对比、随意回溯之前任意一天的版本全部原生支持不需要笔记工具额外设计任何复杂的版本系统。有一次我一篇技术笔记改了三版后来想对比前两版和最终版的差异直接在Git里看diff就能找到改动点比在线文档的历史记录好用太多了。第二多端同步天然多端。只要能访问远程Git仓库用手机、平板或者任何一台电脑都能把笔记clone下来改完了再push回去完全不需要一个中心化服务器来中转数据。你写代码怎么同步记笔记就怎么同步心智负担为零。第三仓库即备份。Git本身就是分布式存储你本地的clone、远程仓库的备份、云端镜像笔记在多个地方同时存在任何一处出问题都能恢复。这个特性在代码托管平台上是免费的我不需要额外购买任何网盘会员也不用信任某个笔记软件的备份机制。第四协作能力是现成的。如果你想把笔记分享给朋友或同事一起维护Git仓库的Issue、分支、Pull Request、权限管理都是现成的不用再去找一个支持多人协作的笔记服务。哪怕只是自己用这个机制也意味着将来如果你想开放某个笔记目录流程是顺滑的。1.3 哪些人适合这种笔记方案这个方案不一定适合所有人但对某些群体来说很合适。第一类就是技术从业者习惯命令行、熟悉Git笔记内容主要是Markdown技术文档、代码片段、项目记录。第二类是技术作者和知识博主笔记写出来以后可以直接通过Git仓库转成博客或文档站内容复用率很高。第三类是开源项目维护者项目根目录下的docs文档、变更记录、经验总结直接跟代码放在同一个仓库维护成本很低。不太适合的人群也比较明确如果你需要真正所见即所得、需要手写识别或大量富文本排版的笔记工具那纯Markdown加Git这套组合可能不够用如果你完全没有接触过Git也不想学习分支、提交这一套概念那这个工具的使用门槛会比普通笔记软件高一些。不过话说回来只要愿意花半天时间搞懂commit和push这俩操作这套方案的上手速度远比想象中快。2. 技术选型与整体架构设计2.1 编辑器内核怎么选在线笔记的第一步是选编辑器组件。前端做Markdown编辑器常见选项有CodeMirror、Monaco Editor和开箱即用的Markdown编辑套件。我最终选了CodeMirror 6。原因是它体积可控、模块化做得好适合按需加载。Monaco Editor就是VS Code背后的编辑器功能非常强但体积偏大一个在线笔记如果首屏就要加载几百KB的编辑器内核有点不划算。CodeMirror 6按官方模块拆分成多个npm包你可以只加载语法高亮、自动补全和快捷键这几个模块其余按需引入。对于Markdown的实时预览我采用的是“编辑区 预览区”左右分栏模式。代码和表格在分栏模式里看得最清楚相比Typora那种“所见即所得”模式分栏实现成本低、不容易出渲染兼容问题而且对技术类笔记更友好。写长文档的时候我习惯把预览区放到右侧四成宽度编辑区占六成这样正文信息和最终效果都能兼顾。2.2 后端框架和Git操作层后端框架我用了Node.js Express。选Node的原因很简单前端和后端同一门语言知识复用成本低生态里处理Git的库也比较多。在Git操作这一层我当时对比过几个方案直接调shell命令用child_process执行git命令简单直接但需要服务端环境装了git并且要自己处理输出解析、错误码、并发提交等细节。simple-git封装了常用git命令的Node库API是Promise风格上手快适合中小型项目。isomorphic-git纯JavaScript实现可以在浏览器和Node里跑但性能和兼容性不如原生git。nodegit绑定原生libgit2功能强但安装和编译问题较多跨平台比较折腾。最后我选了simple-git。因为服务端环境下可以直接用系统安装的git执行效率和功能完整度最高simple-git只是做了一层更友好的封装能省掉不少自己用child_process写边角逻辑的麻烦。实际用下来拉取、提交、推送、分支切这些高频操作都很稳定。2.3 仓库连接与认证方式的设计这里有个设计权衡。我没有做OAuth登录因为OAuth标准流程对这类小工具来说太重了。你需要注册应用、配置回调地址、处理授权码交换和token刷新而且每个Git平台的细则都有差异维护成本很高。尤其当你想同时支持GitHub、GitLab、Gitee多个平台时每个平台都要适配一套OAuth细节投入产出比太低。我采用的是Personal Access Token模式。用户在笔记工具的后台填入各自的平台用户名和访问令牌服务端直接使用HTTPS加token进行git remote操作。个人使用完全够想分享给团队用只要每个人配置好自己的token就行。这里要特别提醒token绝不能硬编码在前端页面里也不能明文存到localStorage。我实际用的是后端加密存储前端页面上只做“输入一次、提交到后端”的流程之后不再回显。如果只是本地跑着玩、不涉及团队共享至少在服务端把token放在权限为600的配置文件里这是底线不是推荐做法。2.4 笔记在仓库中的目录结构设计数据要长期维护目录结构就不能乱。我按自己的使用习惯设计了这套规则repo-root/ ├── README.md ├── notes/ │ ├── daily/ # 日记、流水记录按日期命名 │ ├── tech/ # 技术笔记按主题创建子目录 │ ├── project/ # 某项目的专项笔记 │ └── archive/ # 已归档的旧笔记 └── assets/ # 笔记中引用的图片、附件所有笔记正文统一用UTF-8编码的Markdown文件文件命名规范是“年月日-简短英文描述.md”比如20250603-git-backup-notes.md。这个命名方式在文件系统里排序稳定、可读性好也方便以后写脚本做批量处理。assets目录统一放图片和附件避免散落在各个笔记子目录里导致仓库结构混乱。3. 核心功能实现与关键流程3.1 初始化项目与Git仓库接入新建项目的时候我先把服务端最小骨架搭起来核心就是完成“连接远程仓库”这一个动作。关键的认证设计我用了Git自带的标准机制临时凭据文件加credential.helper用完即删。// server/git-service.js const { simpleGit } require(simple-git); const os require(os); const path require(path); const fs require(fs); const crypto require(crypto); class GitService { constructor(workspace) { this.workspace workspace; } async connect({ repoUrl, username, token, branch main }) { // 为这次操作生成一次性凭据文件用完马上删除 const credentialFile path.join(os.tmpdir(), git-cred-${crypto.randomUUID()}); fs.writeFileSync(credentialFile, https://${username}:${token}${new URL(repoUrl).host}\n, { mode: 0o600 }); const git simpleGit(this.workspace); await git.addConfig(credential.helper, store --file${credentialFile}); // 远程仓库已存在时先拉取一次 await git.pull(origin, branch).catch(() {}); // 用完即删除避免凭据残留在临时目录 fs.unlinkSync(credentialFile); return git; } }这段代码最重要的一个点是不要把token直接拼进remote URL里。很多教程会让你写https://user:tokengithub.com/repo.git虽然能用但每次执行git remote -v都会看到这个带token的地址如果后面不小心把项目信息打印到日志里token就泄露了。用credential.helper store加临时凭据文件的方式可以避免这个问题。3.2 保存与自动提交的流程设计笔记编辑过程我加了一个三秒防抖保存。核心原则不在每次按键时直接触发Git操作先让前端把内容写入本地草稿状态等用户停顿三秒后再统一做一次git commit和git push。这样既保证内容不丢又不会把commit历史刷得过于凌乱。实际流程是用户停笔三秒后前端把Markdown全文POST到后端接口。后端把内容写入工作区中对应的.md文件。调用git add加上这个文件再git commit生成一条提交记录。调用git push origin main推送远程。如果推送失败且原因是非快进则自动执行一次git pull --rebase后再重新推送。commit message我用的是固定模板docs: 更新笔记 xxx。因为纯笔记场景下不需要像代码评审那样写详细的提交说明固定模板让日志看起来干净且可搜索。GitHub上还能按关键词筛出所有笔记提交记录。3.3 核心代码笔记保存与推送保存接口的后端实现我简化成下面这样// server/note-service.js async function saveNote({ relPath, content, user }) { const workRoot path.join(WORKSPACE, String(user.id)); const fullPath path.resolve(workRoot, relPath); // 基础安全检查防止路径穿越跳出工作目录 if (!fullPath.startsWith(path.resolve(workRoot))) { throw new Error(invalid path); } await fs.ensureDir(path.dirname(fullPath)); await fs.writeFile(fullPath, content, utf8); const git simpleGit(workRoot); await git.add(relPath); await git.commit(docs: update ${relPath}); try { await git.push(origin, main); } catch (err) { // 常见情况远端有其他人提交导致推送被拒 // 先用 rebase 按顺序重放本地提交再重新推送 await git.pull(--rebase, origin, main); await git.push(origin, main); } return { ok: true }; }要注意的是如果执行git pull --rebase时出现了合并冲突说明你本地和远程对同一处做了修改这种极端情况代码不能擅自决定保留哪一个版本。我在项目里的处理是冲突时把当前内容备份成一个带时间戳的xxx.conflict.md文件再把远程版本覆盖到原位置保证Git状态立刻恢复干净用户的修改也没有被丢掉。3.4 多用户与仓库隔离如果只是给自己用工作目录里放一个默认仓库就够了。但如果你想部署给团队我建议一个用户一个工作目录每个用户还可以绑定多个远程仓库。我项目里是按workspace/{userId}/{repoId}/的路径来隔离的这样不同用户的笔记不会互相污染。这里多提一句多人同时共用一个后端时Git操作必须串行化。同一时刻两个请求同时执行git push会出现索引锁或者非快进推送的混乱。我的做法是加一个简单的互斥队列每次执行Git命令前先获取一个锁命令结束后再释放按顺序排队执行。别小看这个细节不处理的话团队用到第三天就会出现各种莫名其妙的Git报错。4. 部署运行与数据安全4.1 用Docker Compose一键起服务整个项目部署我写成了Docker Compose的方式。这样不管是在自己的服务器上还是在一台临时机器上体验都能做到一条命令启动不需要手动装Node、配环境非常省心。version: 3 services: note-app: build: . ports: - 8080:8080 volumes: - ./workspace:/app/workspace - ./data:/app/data environment: - NODE_ENVproduction - DATA_DIR/app/data restart: unless-stopped注意两个volume是要长期保留的workspace目录里是所有用户在本地的工作副本里面可能有尚未push到远程的最新内容data目录里存用户配置、加密过的token等元数据。如果把这两个目录放在容器内部容器一删除数据就全没了所以我用挂载卷固定到宿主机上。4.2 本地开发环境搭建步骤如果你打算clone代码在本地跑步骤非常简单安装Node.js 18以上版本和Git。在项目根目录执行npm install安装依赖。执行npm run dev启动开发服务。浏览器打开http://localhost:8080。在设置页填入你的Git平台用户名、访问令牌、仓库地址保存后就能开始写笔记。第一次保存时后端如果发现远程仓库是空的会自动创建一个初始commit并推送到远程分支。用户不需要提前在Git平台上创建好文件只需要有一个空仓库即可。4.3 数据备份要怎么做才真正放心有读者可能会想Git仓库本身就是分布式备份那我是不是不用管备份了实际不是。远程仓库、本地工作区、还有跑这个工具的服务器它们确实都是副本但如果你同时在几个地方做了破坏性操作比如误删了远程仓库又在本机做了force push那副本再多也救不回来。我的做法是加一个轻量定时任务每天凌晨把workspace目录里所有用户仓库打包成一个tar.gz文件传到另一个对象存储或者单独的备份磁盘里。这个备份不追求实时性一天一次足够但要保证它和运行环境不在同一台物理机器上。因为Git仓库大部分是文本文件压缩率很高几个月的笔记也就几十MB。4.4 服务端运行Git的安全注意事项在服务端跑Git命令跟在自己电脑上跑不一样有几个安全细节需要注意不要用root账号跑服务。给服务单独建一个系统用户权限只开放给workspace目录权限最小化。路径穿越必须校验。用户传上来的相对路径一定要经过path.resolve之后再做前缀判断防止有人传../../etc/passwd这种路径去读文件。Git命令执行时最好不要用--global配置所有配置都写到仓库目录下避免影响服务器上其他项目。token的存储和传输要做加密HTTP环境下要强制跳转HTTPS不能明文裸奔。5. 常见问题与排查技巧实录这个项目开发过程中我遇到的问题不少整理了最有代表性的几个方便你少走弯路。问题现象可能原因排查思路解决方案保存笔记后远程仓库没有更新推送失败被自动吞掉查看后端日志确认是否有git push报错检查token是否有推送权限网页一直转圈编辑器加载不出来前端资源加载失败查看浏览器控制台网络面板检查静态资源路径重新构建前端输入中文时保存的文档乱码文件编码写入错误查看后端存储的原始文件字节统一使用UTF-8编码写入和读取远程仓库收到多条空提交防抖没生效或重复提交审查前端保存事件触发逻辑增加标志位防止重复提交手机端编辑后PC端看不到更新push失败或拉取时机不对查看提交记录和远程HEAD强制刷新前先pull每次保存都会创建新的commit历史无意义提交过多查看commit message调整防抖时间合并短时间内的修改5.1 认证失败到底卡在哪一步认证失败是新手最容易碰到的问题而且报错信息五花八门。常见的是“Authentication failed”“Invalid username or password”“403”之类的提示。排查的时候先按这个顺序捋一遍第一确认token不是密码。GitHub和GitLab的Personal Access Token是访问令牌不是账号登录密码即使填了密码也不对。第二确认token的权限范围。GitHub的token需要勾选repo权限Gitee的私人令牌需要勾选projects权限少了推送权限就会403。第三确认仓库地址填的是HTTPS地址不是SSH地址也不要填成网页端的仓库主页URL。第四如果之前测试过失败先清空Git的凭据缓存再重试防止旧的错误token被记住。5.2 推送被拒与冲突处理多人使用或者多设备同时写的时候推送被拒几乎是必然的。报错信息通常是“failed to push some refs”或“fetch first”。我的处理策略是自动执行git pull --rebase再重试推送。但是要注意rebase过程中一旦出现冲突代码不能擅自解决就是前面说到的备份方案。为了减少冲突的概率我建议一个笔记文件尽量归属一个主要责任人或一台主要设备。如果你手机上只是查看、偶尔补充一两句那冲突的概率会低很多。真正常见的冲突场景是同一天在PC上写了一整篇又在手机上改了一个段落这种就需要备份手工合并一下。5.3 仓库体积膨胀怎么办用了一段时间后仓库会越变越大。主要是三个原因一是大图片直接塞进了assets目录二是某些中间版本的草稿文件没有清理三是有些用户把自己电脑上的node_modules或者编辑器缓存文件夹误加了进去。解决办法是控制内容类型图片建议先用图片压缩工具压一遍再放到assets目录单张图片尽量控制在500KB以内可以在项目根目录放一个.gitignore文件把.DS_Store、Thumbs.db、临时文件等排除掉如果仓库已经膨胀可以用git filter-branch或者git filter-repo来重写历史但这属于危险操作执行前一定导出完整备份。5.4 中文文件名与大小写问题还有一个很隐蔽的坑是文件名大小写。Linux服务器上的Git默认区分大小写但Windows和macOS默认不区分。如果你在macOS上把TechNotes.md改名成technotes.mdgit status可能不会正确识别推上去之后Linux服务器上会出现重复文件。解决方法是统一命名规范一律小写加短横线并且不依赖文件名大小写区分内容。中文文件名本身没问题Git支持UTF-8文件名但要注意两个细节一是Git默认会把非ASCII文件名转义成八进制显示需要设置core.quotepath false才能正常显示中文二是在Windows下执行脚本时文件名的编码要和脚本文件本身的编码一致不然正则匹配会乱。6. 扩展玩法笔记不只是笔记6.1 博客发布流笔记存在Git仓库里最大的好处是内容可以自由复用。我自己做了一个简单的发布脚本把notes/tech目录下的文章通过一个静态站点生成器直接渲染成博客。笔记里的图片路径改成相对路径后整个目录就是一套完整的博客源文件。这个流程最适合的技术写作者场景是平时在笔记工具里写草稿改到满意了加一个frontmatter头标题、日期、标签运行脚本自动生成博客页面并推送到托管平台内容管线完全自动化。相比过去在博客后台粘贴排版这省掉了大量整理成本。6.2 双链笔记玩法Markdown本身支持链接语法所以双链笔记的概念也可以在这个工具里实现。我约定了一个规则所有笔记的标题就是文件名笔记里写另一篇笔记的链接时直接用[笔记名](./tech/xxx.md)这种相对路径。配合全局搜索功能等于有了一个简易的个人Wiki。要做得更顺手可以加一个笔记间关系图的页面。Git仓库里的所有Markdown文件就是节点文件中的相对链接就是边前端解析一下就能渲染出一张个人知识图谱。虽然这个功能我没有做得很重但它验证了一个事只要数据是开放的上层玩法可以无限扩展。6.3 定时自动提交如果你担心有些灵感碎片忘了点保存可以加一个后台心跳任务每十分钟检测一次工作区是否有未提交的文件变更有就自动提交并推送。这样即使你在其他编辑器里直接改了工作区的文件也会被自动纳入版本管理不会漏掉任何修改。我实际使用时没把自动提交时间设得太短因为每十分钟一次完全够用commit历史也不会太碎。这个功能适合那些喜欢直接改文件而不是通过网页编辑器操作的人相当于给整个工作目录加了一层Git保险。6.4 与CI/CD联动构建文档站如果你想给别人分享笔记又不想暴露自己的私有仓库可以顺手接一套CI/CD在Git仓库里关联流水线任务当main分支有更新时自动执行一次Markdown转静态文档站的操作把生成的HTML部署到Web服务器。这样笔记工具的职责就是纯粹的编辑和存储发布动作完全交给自动化流程。这个模式跟我做过的文档站项目非常像等于用同一套基础设施同时支撑了笔记工具和文档站两个业务复用性极高。如果你已经有一些CI/CD平台的使用经验这一步几乎不用额外学习成本。最后分享一个我自己的体会做这个项目之前我也担心Git后端会不会让笔记操作变得很重但实际用了三个月发现负担几乎可以忽略。反而因为数据和版本都在自己手里心里特别踏实。这种踏实感来自于你不再依赖任何一家公司的产品策略和服务器稳定性也来自于你随时可以换编辑器、换部署方式、甚至用命令行直接操作笔记内容。对一个长期积累知识的人来说这种开放和安全的感觉比编辑器界面的流畅度重要得多。如果你也正在被在线笔记的数据锁定问题困扰真的很建议花一个傍晚折腾一下这套方案源码、文档、部署脚本我都放在项目仓库里了直接照着来就行。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻