一、决策结论
Markdown 编辑器选 CodeMirror 6,加载方式选 ESM importmap + esm.sh。
涉及的备选方案:
| 方案 | 评估 | 结论 | |------|------|------| | CodeMirror 6 + importmap | 官方推荐路线,HackMD 同款 | ✅ 采用 | | CodeMirror 6 + 社区 UMD bundle | 简单但生命线交给小项目 | ❌ 否决 | | CodeMirror 5 + EasyMDE | 现成工具栏,CDN 友好 | ❌ 否决(迁移成本) | | Monaco Editor | 移动端不可用,体积过大 | ❌ 否决 | | Block 编辑器(TipTap/ProseMirror) | 违背 MD 第一公民 | ❌ 永不启用 | | Toast UI / Vditor | 思源同款,移动端好但是 Block 模型 | ❌ 否决 |
二、为什么否决 Monaco Editor
Monaco 是 VS Code 同款,工程上很强,但和 MDMS 不匹配:
- 移动端不可用——Microsoft 官方文档明确说不支持移动端。MDMS 已确立「移动端可用性」公理,Monaco 直接出局
- 体积过大——核心 3MB+,gzip 后 ~700KB,比 CodeMirror 大十倍。MDMS 后台编辑器作者每次打开都要加载,对中国服务器和弱网用户不友好
- 为写代码设计,不为写作——IntelliSense、TypeScript 服务、多光标这些是亮点,但 MDMS 写 Markdown 用不上。Markdown 渲染、表格对齐、列表续行这些写作细节 Monaco 反而不如 CodeMirror
- Yjs 集成相对小众——
y-monaco主要用于远程结对编程而非协同文档。HackMD/Notion-clone 类项目都用 CodeMirror + Yjs 这套
Monaco 适合做的事 MDMS 都不做:在线 IDE(CodeSandbox)、复杂代码审查(GitHub PR)、配置文件 schema 校验等。
三、为什么否决 Block 编辑器
Block 编辑器(TipTap、ProseMirror、Editor.js 等)的数据真相是 JSON 块树,不是 Markdown。这违背 MDMS 两条核心原则:
- MD 第一公民——MD 文件作为数据真相是 v2.0 七正交原则第 4 条
- 智能体友好——智能体读 MD 写 MD 的工作流要求 MD 是真相,Block 反序列化为 MD 有损
虽然 Block 编辑器体验在桌面上很爽(Notion 风格的 / 斜杠命令、拖拽块),但桌面爽 = 移动端拉。Notion 在手机上至今体验不达标。MDMS 既要 MD 真相,又要移动端可用,Block 编辑器双不满足。
例外条件:除非未来证明 Block ↔ MD 双向序列化无损往返,且移动端体验对标 HackMD,否则永不启用。
四、为什么否决 CodeMirror 5
CM5 现在能用,但是踩在已经开始萎缩的栈上:
- HackMD 已在迁离 CM5——HackMD next 版本用 CM6
- Yjs 主推方向是 y-codemirror.next(CM6 版),y-codemirror(CM5 版)维护不积极
- CM5 模块化差——加扩展是改全局 CodeMirror 对象,CM6 是树摇友好的 ES module
- 三五年后再迁的成本远高于现在直接上 CM6
唯一让 CM5 显得诱人的是 EasyMDE 这种现成工具栏包装。但工具栏可以自己加,迁移成本不能省。
五、为什么 importmap 而非 UMD bundle
CodeMirror 6 官方不提供 UMD 单文件 bundle,因为它设计时就只支持 ESM。社区有人打了 UMD bundle 传到 npm/CDN,但风险高:
- 生命线交给小项目——这些 bundle 项目 star 数都不高,最后更新可能在半年前。如果哪天它不维护了或 CDN 挂了,MDMS 要自己打包 CM6 ——回到 importmap 方式或更糟
- 跟不上 CM6 新版本——升级 CM6 要等社区 bundle 跟进
- 加 CM6 插件受限——bundle 里没打的插件用不了
- Yjs 接入受限——bundle 不带 y-codemirror.next 集成,Yjs 上线时被迫退化为 importmap
importmap 是 CM6 官方推荐路线,浏览器原生支持,看起来不熟悉但本质就是一段 JSON 配置,写一次复制到模板里就好。这是一次性认知成本,换来后续所有升级、插件、Yjs 接入的顺畅。
六、esm.sh 的 CDN 风险
esm.sh 是个第三方 CDN。可能的故障模式:
- esm.sh 抽风某个文件加载失败(5-10 个文件并行加载,任何一个挂都失败)
- 整个 esm.sh 服务挂了(罕见但可能)
- 网络抽风(中国大陆访问 esm.sh 偶尔会慢)
降级路径(已实现):CM6 加载失败时,原 textarea 自动兜底,作者依然可编辑。仅失去语法高亮和 [[ 自动补全。这是「降级保完备性」原则的具体落地。
未来加固方案:
- 把 esm.sh 那几个 CM6 文件下载到本地
/static/codemirror/自己服务,彻底脱离外部依赖 - importmap 同时声明多个 fallback URL(语法繁琐但可行)
- 切换到 jsdelivr 的 ESM CDN 备选
七、移动端公理的影响
「移动端可用性」从 v2.0.4 起作为 PHILOSOPHY 公理写入。这条公理对编辑器的具体要求:
- 触屏选择必须工作(CM6 ✅)
- 虚拟键盘出现时编辑器不被挤压(v2.0.4 用视口 55% 高度策略)
- 不能依赖鼠标悬停的功能(CM6 自动补全用键盘和触摸都能选 ✅)
- 不能依赖键盘快捷键的核心功能(CM6 默认所有命令都有触摸路径 ✅)
- 字体大小至少 13px(v2.0.4 设了 fontSize: "13px")
- 编辑器加载时间不能太长(CM6 首次 ~500KB,后续走浏览器缓存)
八、未来演进路径
v2.0.5:Yjs 实时协同
- 新增 import:
y-codemirror.next - 编辑器扩展数组里加
yCollab(ytext, awareness) - 后端加 WebSocket 通道(Gorilla WebSocket)
- 接同款编辑器栈上的协同——HackMD 这套已经验证到生产级别
v2.0.6+:CSS 编辑器和 HTML 模板编辑器
- css_edit.html / template_edit.html 也升级到 CM6
- 引入
@codemirror/lang-css和@codemirror/lang-html - importmap 加几行即可
v2.0.7+:编辑器辅助功能
- 表格快速插入快捷键
- 图片上传集成(依赖 v2.0.x 的图片管理插件)
- Mermaid 图表预览
- 数学公式渲染(KaTeX)
每一条都是在 CM6 基础上加扩展,不需要重写编辑器底层。这就是「保留可回退起点」的复利。
九、技术实现位置
- 文章编辑:
templates/admin/article_edit.html第 60+ 行 - 文档编辑:
templates/admin/doc_edit.html末尾 - 插件市场标记:
handler/handler.go的 plugins 数据 - 文章列表 API(供
[[补全):GET /api/articles/list,handler 在handler/handler.go - 兜底原则:textarea 始终保留在 DOM 里,CM6 加载成功才隐藏
设计原则:编辑器是体验放大器,不是阻塞项。CM6 失败不应阻止作者写文章。