各位做笔记、写博客、攒技术文档的朋友,应该都对 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 版本测试。
- npm:Node.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来打包成单文件可执行程序。
先安装pkg:
npm 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 class="container"> <view class="header">Markdown 阅读</view> <view wx:if="{{loading}}" class="loading">加载中...</view> <view wx:for="{{fileList}}" wx:key="path" class="file-item" bindtap="onTapFile"><!-- 文件路径:miniprogram-md-reader/miniprogram/pages/detail/detail.wxml --> <view class="detail"> <towxml nodes="{{article}}" /> </view>detail.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 | 用户名或应用密码错误 | 确认使用的是坚果云应用密码,不是登录密码 |
上传返回403 | WebDAV 目录不存在或者没有权限 | 在脚本中先创建远程目录,再上传文件 |
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,把阅读端做成桌面应用。
既然已经走到了这一步,不妨直接动手把自己的笔记目录交给这套方案。先跑通最小闭环,再逐步增加功能,整个过程本身就是一次很好的全栈实践。
希望这篇文章能帮到你。有问题欢迎在评论区交流,我在搭建过程中踩过的坑,大概率也是你会遇到的那些坑。