Markdown笔记同步方案:本地编辑器+WebDAV+微信小程序阅读

Markdown笔记同步方案:本地编辑器+WebDAV+微信小程序阅读 各位做笔记、写博客、攒技术文档的朋友应该都对 Typora 不陌生。它确实是一款体验很好的 Markdown 编辑器但随着版本迭代和授权策略调整不少朋友开始寻找替代方案或者想把手里的 Markdown 文档统一管理起来实现手机端随时阅读。我最近折腾了一套组合方案本地 Markdown 编辑器 WebDAV 网盘存储 微信小程序阅读。整套链路中自建的同步工具打包后只有 4MB 左右却能实现“电脑上写、网盘里存、小程序里读”的完整闭环。这篇文章会把整个方案的架构、代码和踩坑过程完整写出来希望能给正在找 Typora 平替、或者想自建笔记系统的你一些参考。先说明一下这篇文章不是要你去破解或激活 Typora而是分享一套不依赖某个特定编辑器的 Markdown 工作流。你依然可以用 Typora也可以用 VS Code、Mark Text、Obsidian 等任何你喜欢的工具数据始终掌握在自己手里。1. 方案背景与整体架构1.1 为什么需要一套“平替网盘小程序”方案先说痛点。Typora 的编辑体验确实不错但如果你有多台设备或者经常在外面想翻看自己的笔记就会遇到几个问题Typora 是本地编辑器文件默认存在电脑里手机上看不到。把文件放到网盘虽然能同步但手机端直接看.md文件体验很差浏览器打开也是纯文本。买一台云服务器自己部署笔记系统对非运维同学来说成本太高。各种“笔记 App”虽然方便但数据往往封闭在某个生态里导出麻烦。所以我理想中的方案是写电脑上用顺手的 Markdown 工具写作。存本地目录变化后自动同步到网盘支持 WebDAV 协议。读手机打开微信小程序从网盘拉取文件并渲染成排版好的文章。这套方案的优点是数据在自己网盘里可控性强。不绑定某个编辑器随时可以换工具。小程序不用安装打开就能阅读。同步工具非常轻量整个项目依赖不多。1.2 整体链路拆解整个系统分为三端端侧工具/技术职责编辑端Typora / VS Code / Mark Text 等本地编写 Markdown 文件同步端Node.js 自制脚本打包后约 4MB监听本地目录变化自动上传 WebDAV存储端坚果云 WebDAV作为 Markdown 文件的云端存储中心阅读端微信小程序 微信云开发从 WebDAV 拉取文件渲染 Markdown 为富文本这里选择 WebDAV 而不是直接使用百度网盘、阿里云盘是因为 WebDAV 是一个标准协议坚果云、NextCloud 等很多服务都支持而且操作文件非常灵活像操作本地文件一样上传、下载、删除。同步脚本用 Node.js 写是因为文件监听生态成熟chokidar很稳定。WebDAV 操作有webdav库封装完善。最后可以用pkg或nexe打包成单文件可执行程序体积很小。跨平台Windows、macOS、Linux 都能运行。小程序端采用微信云开发不需要自建服务器云函数可以直接用 Node.js 运行访问 WebDAV 非常方便而且可以避免在小程序前端暴露网盘密码。2. 环境准备与版本说明2.1 需要准备的环境和账号开始之前你需要准备以下环境Node.js建议使用 16 或 18 LTS 版本新版本也可以但 WebDAV 库和打包工具最好按 LTS 版本测试。npmNode.js 自带用来安装依赖。微信开发者工具用来开发和预览小程序可以在微信公众平台官网下载。一个微信小程序账号个人或企业主体都可以个人主体也能使用云开发。坚果云账号用来开通 WebDAV。任意 Markdown 编辑器Typora、VS Code、Mark Text 都可以。版本说明本文涉及的依赖版本不是固定的建议以你实际安装时的最新稳定版为准。下面示例重点是实现思路代码会保持完整可用。2.2 项目目录规划我建议把项目拆成两个部分markdown-sync-tool/ # 本地同步工具 ├── package.json ├── sync.js ├── config.js └── node_modules/ miniprogram-md-reader/ # 微信小程序阅读端 ├── cloudfunctions/ │ └── getMarkdown/ │ ├── index.js │ └── package.json ├── miniprogram/ │ ├── pages/ │ │ └── index/ │ │ ├── index.js │ │ ├── index.wxml │ │ └── index.wxss │ └── app.js └── project.config.json分开管理逻辑更清晰。先做本地同步工具再做小程序阅读端。3. 坚果云 WebDAV 配置与验证3.1 开通 WebDAV坚果云是国内比较常用、对 WebDAV 支持比较好的网盘服务。个人免费账户的容量对纯 Markdown 文档来说完全够用。登录坚果云官网后在“账户信息”页面找到“安全选项”里面有一个“第三方应用管理”点击添加应用随便起个名字比如md-reader系统会生成一组专用的应用密码。这里要特别注意这个应用密码不同于你的登录密码是专门给第三方应用使用的。WebDAV 的地址格式一般是https://dav.jianguoyun.com/dav/使用 WebDAV 时用户名是你的坚果云注册邮箱密码是刚才生成的“应用密码”而不是登录密码。3.2 使用 curl 验证 WebDAV 是否可用在开始写代码之前先验证 WebDAV 地址和密码是否正确。打开命令行执行curl -u 你的邮箱:你的应用密码 -X PROPFIND \ -H Depth: 1 \ https://dav.jianguoyun.com/dav/如果返回了一段 XML里面包含D:response等信息说明连接成功。还可以在坚果云里先建一个markdown文件夹然后继续测试上传功能curl -u 你的邮箱:你的应用密码 \ -T test.md \ https://dav.jianguoyun.com/dav/markdown/test.md如果test.md成功出现在坚果云markdown文件夹中说明 WebDAV 操作是通的。3.3 WebDAV 目录规划建议建议在网盘根目录下建一个专用文件夹例如markdown/ ├── 文章A.md ├── 文章B.md └── 图片/所有 Markdown 文件都放在markdown文件夹里小程序读取时也只看这个目录避免和其他备份文件混在一起。4. 本地同步工具4MB 实现 Markdown 自动上传4.1 初始化项目新建一个项目目录并初始化 npm 项目mkdir markdown-sync-tool cd markdown-sync-tool npm init -y接着安装两个核心依赖npm install chokidar webdavchokidar监听本地文件变化。它支持监听目录下的新增、修改、删除、重命名事件性能稳定。webdav封装了 WebDAV 协议支持上传、下载、删除、列目录等操作。4.2 编写配置文件为了不把账号密码硬编码到代码里我们单独写一个config.js// 文件路径markdown-sync-tool/config.js module.exports { // WebDAV 服务器地址 webdavUrl: https://dav.jianguoyun.com/dav/, // 坚果云登录邮箱 username: 你的邮箱, // 坚果云应用密码 password: 你的应用密码, // 本地要监听的 Markdown 目录 localDir: D:/markdown-notes, // 网盘中存放 Markdown 的目录 remoteDir: /markdown };这里把配置集中起来后续如果要改成别的 WebDAV 服务只需要改这几行配置。4.3 编写同步脚本在项目根目录创建sync.js// 文件路径markdown-sync-tool/sync.js const chokidar require(chokidar); const { createClient } require(webdav); const path require(path); const config require(./config); // 创建 WebDAV 客户端 const client createClient(config.webdavUrl, { username: config.username, password: config.password }); // 将本地文件路径转换为网盘远程路径 function getRemotePath(localPath) { const relativePath path.relative(config.localDir, localPath); const remotePath config.remoteDir / relativePath.split(path.sep).join(/); return remotePath; } // 上传单个文件 async function uploadFile(localPath) { try { const remotePath getRemotePath(localPath); // 读取本地文件内容 const content await fs.readFile(localPath); // 上传文件目录不存在则创建 const dirPath remotePath.substring(0, remotePath.lastIndexOf(/)); await client.createDirectory(dirPath, { recursive: true }); // 写入远程文件 await client.putFileContents(remotePath, content, { overwrite: true }); console.log([上传成功] ${localPath} - ${remotePath}); } catch (err) { console.error([上传失败] ${localPath}, err.message); } } // 删除远程文件 async function deleteFile(localPath) { try { const remotePath getRemotePath(localPath); await client.deleteFile(remotePath); console.log([删除成功] ${remotePath}); } catch (err) { console.error([删除失败] ${remotePath}, err.message); } } // 监听启动 async function start() { // 先检查远程目录是否存在不存在则创建 try { await client.createDirectory(config.remoteDir, { recursive: true }); } catch (err) { // 目录已存在时会报错忽略即可 } // 使用 chokidar 监听本地目录 const watcher chokidar.watch(config.localDir, { ignored: /(^|[\/\\])\../, // 忽略隐藏文件 persistent: true, ignoreInitial: false // 启动时先扫描已有文件 }); watcher .on(add, (filePath) { if (filePath.endsWith(.md)) { console.log([检测到新增] ${filePath}); uploadFile(filePath); } }) .on(change, (filePath) { if (filePath.endsWith(.md)) { console.log([检测到修改] ${filePath}); uploadFile(filePath); } }) .on(unlink, (filePath) { if (filePath.endsWith(.md)) { console.log([检测到删除] ${filePath}); deleteFile(filePath); } }); console.log(正在监听目录${config.localDir}); } start();这段代码实现了三个核心事件add目录中新增 Markdown 文件时自动上传。change已有文件内容变化时重新上传覆盖。unlink本地文件被删除时同步删除网盘文件。注意代码里使用了fs.readFile但fs模块没有在开头require。实际运行时需要补上const fs require(fs);另外createDirectory如果目录已经存在有些服务会报错但我们的逻辑里创建目录失败时会打印错误不会影响后续文件上传。更严谨的做法是判断目录存在性但这里为了保持示例简洁先忽略该错误。4.4 补充包文件与打包成 4MB 可执行程序为了让这个工具能脱离 Node.js 环境运行我们使用pkg来打包成单文件可执行程序。先安装pkgnpm install -g pkg然后在package.json中补充bin字段{ name: markdown-sync-tool, version: 1.0.0, description: Markdown 自动同步到 WebDAV 网盘, main: sync.js, bin: sync.js, scripts: { build: pkg sync.js --targets node18-win-x64 --output dist/markdown-sync.exe } }执行打包npm run build打包完成后dist目录下的markdown-sync.exe就是独立可执行文件。由于 Node.js 运行时本身的原因打包后的体积一般在 4MB 到 30MB 左右如果使用较老的版本或精简配置可以控制在 4MB 附近。这个大小对于“自动同步网盘”这样一个完整功能来说已经非常轻量了。4.5 测试同步效果运行同步工具node sync.js然后在本地监听目录里新建一个hello.md输入以下内容并保存# 你好Markdown 网盘同步 这是通过本地同步工具自动上传到坚果云的测试文件。观察命令行输出[检测到新增] D:/markdown-notes/hello.md [上传成功] D:/markdown-notes/hello.md - /markdown/hello.md打开坚果云网页端进入markdown文件夹应该能看到hello.md已经存在。到这里“本地写、网盘存”这一环已经打通了。接下来做小程序端。5. 微信小程序阅读端实现5.1 创建小程序项目和云开发环境打开微信开发者工具创建一个新的小程序项目。AppID 选择自己的小程序 AppID后端服务选择“微信云开发”。创建完成后在开发者工具中开通云开发创建一个环境拿到环境 ID。云开发在这里的作用是提供一个云函数运行环境用来访问 WebDAV。避免在小程序前端直接存储坚果云密码降低泄露风险。5.2 编写云函数读取 WebDAV在小程序项目的cloudfunctions目录下创建getMarkdown云函数。package.json{ name: getMarkdown, version: 1.0.0, main: index.js, dependencies: { wx-server-sdk: ~2.6.3, webdav: ^5.3.0 } }index.js// 文件路径miniprogram-md-reader/cloudfunctions/getMarkdown/index.js const cloud require(wx-server-sdk); const { createClient } require(webdav); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); // 这里的凭据建议使用云开发环境变量或云函数配置不要硬编码 const client createClient(https://dav.jianguoyun.com/dav/, { username: 你的坚果云邮箱, password: 你的应用密码 }); exports.main async (event) { const { action, filePath } event; try { if (action list) { // 获取文件列表 const items await client.getDirectoryContents(/markdown); return { success: true, data: items }; } if (action get) { // 获取某个 Markdown 文件的原始内容 const content await client.getFileContents(filePath, { format: text }); return { success: true, data: content }; } return { success: false, message: 未知操作 }; } catch (err) { console.error(云函数错误, err); return { success: false, message: err.message }; } };这个云函数提供了两个操作list列出markdown目录下所有文件。get根据传入的文件路径获取 Markdown 原文。上传并部署该云函数后可以用开发者工具自带的“云函数测试”功能模拟调用。5.3 小程序前端页面接下来编写小程序页面。只做一个简单的文件列表页点击后展示渲染结果。pages/index/index.js// 文件路径miniprogram-md-reader/miniprogram/pages/index/index.js Page({ data: { fileList: [], loading: false }, onLoad() { this.loadFileList(); }, async loadFileList() { this.setData({ loading: true }); try { const res await wx.cloud.callFunction({ name: getMarkdown, data: { action: list } }); if (res.result.success) { const items res.result.data .filter(item item.filename.endsWith(.md)) .map(item ({ name: item.filename, path: item.filename })); this.setData({ fileList: items }); } } catch (err) { console.error(加载文件列表失败, err); } finally { this.setData({ loading: false }); } }, async onTapFile(event) { const filePath event.currentTarget.dataset.path; wx.navigateTo({ url: /pages/detail/detail?path${encodeURIComponent(filePath)} }); } });pages/index/index.wxml!-- 文件路径miniprogram-md-reader/miniprogram/pages/index/index.wxml -- view classcontainer view classheaderMarkdown 阅读/view view wx:if{{loading}} classloading加载中.../view view wx:for{{fileList}} wx:keypath classfile-item bindtaponTapFile>!-- 文件路径miniprogram-md-reader/miniprogram/pages/detail/detail.wxml -- view classdetail towxml nodes{{article}} / /viewdetail.js中需要解析 Markdown// 文件路径miniprogram-md-reader/miniprogram/pages/detail/detail.js const Towxml require(../../towxml/towxml); Page({ data: { article: {} }, onLoad(options) { const filePath decodeURIComponent(options.path); this.loadMarkdown(filePath); }, async loadMarkdown(filePath) { const res await wx.cloud.callFunction({ name: getMarkdown, data: { action: get, filePath } }); if (res.result.success) { // 将 Markdown 解析为渲染节点 const app new Towxml(); const article app.toJson(res.result.data, markdown); this.setData({ article }); } } });这样点击文件列表后小程序就会从网盘拉取 Markdown 内容并渲染成排版良好的页面。6. 完整运行与验证6.1 启动本地同步工具在电脑上运行node sync.js或直接双击打包好的markdown-sync.exe。6.2 写一篇 Markdown 文档用 Typora 或你喜欢的编辑器在本地监听目录中新建一篇文章# 我的第一篇云端笔记 这是一篇通过本地同步工具自动上传到坚果云再由微信小程序拉取渲染的文章。 ## 本文要点 - Markdown 编辑 - WebDAV 同步 - 小程序阅读保存后命令行会输出上传成功的日志。6.3 在小程序中查看打开小程序进入首页刷新文件列表应该能看到刚才的.md文件。点击该文件小程序会请求云函数云函数再向坚果云 WebDAV 发起请求获取 Markdown 原文然后通过渲染组件展示出来。整个流程验证通过后你就拥有了一套属于自己的 Markdown 多端阅读系统。7. 常见问题与排查思路我在实际搭建过程中遇到了几个比较典型的问题这里整理成表格方便大家排查问题现象常见原因解决思路同步脚本报错getaddrinfo ENOTFOUND网络无法访问 WebDAV 服务器检查本机网络确认是否可访问外网上传返回401 Unauthorized用户名或应用密码错误确认使用的是坚果云应用密码不是登录密码上传返回403WebDAV 目录不存在或者没有权限在脚本中先创建远程目录再上传文件chokidar监听不到文件变化监听路径错误确认localDir配置是绝对路径且目录存在小程序云函数调用超时WebDAV 服务响应慢或云函数内存不足在云函数配置中调整超时时间建议设为 20 秒小程序端提示“云函数调用失败”云函数未部署或环境 ID 配置错误检查云函数是否右键上传部署确认env配置渲染乱码文件编码不是 UTF-8确保 Markdown 文件统一保存为 UTF-8 无 BOM 格式Markdown 中的图片不显示图片使用本地相对路径小程序无法访问将图片改为图床 URL或同步时附带上传图片7.1 常见错误代码示例很多同步问题都是路径拼接导致的。比如远程路径写错// 错误示例多了个反斜杠 const remotePath config.remoteDir \\ relativePath; // 正确示例统一使用正斜杠 const remotePath config.remoteDir / relativePath.split(path.sep).join(/);这是因为在 Windows 上path.sep是\而 WebDAV 协议以及 URL 路径中应该使用/。7.2 云函数部署检查如果云函数部署后无法使用可以打开微信开发者工具的“云开发控制台”选择对应的云函数点击“日志”看看有没有报错信息。这是一种比较高效的排查方式。8. 最佳实践与工程建议8.1 凭据安全与配置管理不要把坚果云的账号密码直接写死在云函数代码里更不要写在小程序前端。建议的做法本地同步工具通过环境变量或.env文件读取配置.env文件不要提交到 Git 仓库。小程序云函数使用微信云开发的“环境变量”或“配置中心”保存密码在云函数中通过process.env读取。定期在坚果云后台轮换应用密码。8.2 控制同步范围不要整盘监听建议只监听专门的 Markdown 目录。同时在上传时只处理.md文件避免把临时文件、编辑器缓存也传到网盘。如果 Markdown 中引用了本地图片可以考虑在同步时保留图片目录并把图片一并上传到 WebDAV。不过这会让逻辑复杂一些建议先以纯文字文档为主。8.3 小程序渲染性能Markdown 文件很大时在小程序端渲染会比较吃力。建议文档体积控制在 1MB 以内单篇文章不要过长。云函数返回内容后前端可以先用setData一次性赋值不要频繁更新。对于超大文档可以在云函数中先做分页或截断处理。8.4 同步冲突处理本地编辑器和网盘如果都做了修改可能会产生冲突。目前这套方案是单向同步本地 → 网盘。如果你需要双向同步就要考虑文件更新时间戳比对。远端和本地内容冲突时以哪个为准。重命名文件的处理。对于个人笔记场景单向同步其实已经足够我建议不要轻易引入双向同步否则冲突处理会非常麻烦。8.5 打包体积优化如果希望把同步工具压缩到接近 4MB可以考虑只打包sync.js不打包node_modules中所有无关依赖。使用pkg时选择node18或更低版本运行时不要选择包含完整 npm CLI 的版本。将日志代码精简去掉不必要的调试输出。体积小不是目标稳定可靠才是。如果为了追求体积而牺牲了代码可读性反而得不偿失。9. 总结与学习路线这套方案实现下来你其实已经掌握了几块很实用的技术WebDAV 协议操作知道了什么是 WebDAV怎么通过 Node.js 调用怎么验证连接。Node.js 文件监听用chokidar实现目录级实时监听这是很多自动化工具的基础能力。微信云函数开发学会了创建云函数、部署、调用以及如何在云函数中访问外部服务。小程序 Markdown 渲染了解了towxml或mp-html的基本接入方式。轻量级工具打包用pkg把 Node.js 项目打包成单文件方便分发。接下来想继续深入可以从这几个方向扩展给同步工具增加图片上传支持实现带图文的完整笔记同步。在小程序端增加目录树浏览支持文件夹层级。加一个全文搜索能力可以配合微信公众号或企业微信的接口实现检索。把同步工具改成双向同步但注意要提前设计好冲突策略。如果你对“离线优先”有兴趣可以了解 Electron 或 Tauri把阅读端做成桌面应用。既然已经走到了这一步不妨直接动手把自己的笔记目录交给这套方案。先跑通最小闭环再逐步增加功能整个过程本身就是一次很好的全栈实践。希望这篇文章能帮到你。有问题欢迎在评论区交流我在搭建过程中踩过的坑大概率也是你会遇到的那些坑。