简介这是一套基于 Python、Flask 与 editor.md 构建的在线 Markdown 编辑工具源码面向具备 Flask 基础、希望学习或直接搭建在线编辑平台的开发者。项目整合了 flask-sqlalchemy、flask-login 与 sm.ms 图床实现登录注册、文章编辑、文章列表三个页面并支持自动保存与图片上传至图床等功能对想深入理解 Web 应用完整链路的读者有较高参考价值。压缩包共 559 个文件约 15.07MB其中 js 与 html 文件占主体用于前端交互与页面渲染css、scss 负责样式另有 14 个 py 文件承载后端逻辑并附带 readme 安装教程与多份 md 说明文档。目前已有 447 人学习下载。通过阅读源码读者可掌握用户认证、数据持久化、编辑器集成与图床对接等实现思路并借鉴其目录组织与模块划分方式用于二次开发或课程设计参考。1. 在线 Markdown 编辑工具从「能写」到「能交付」的那条分界线很多人第一次接触 Markdown 是在某个在线编辑器里敲下# 标题看着右侧实时渲染出加粗大字觉得这东西不过是个轻量记事本。直到某天需要把一份带表格、代码块、图片和 Mermaid 流程图的文档交给同事才发现「能写」和「能交付」之间隔着一整条工具链。在线 Markdown 编辑工具要解决的核心问题不是语法高亮而是让写作者在一个浏览器标签页里完成从草稿到可发布 HTML、可导出 Word、可粘贴进公众号的全过程。它适合三类人需要频繁输出技术文档的工程师、要维护知识库的内容运营、以及不想在本地装一堆插件的前端开发者。选在线方案还是本地方案取决于你是否需要多人协作、是否受限于公司电脑的安装权限、以及文档里是否包含必须实时预览的图表。这一章先把边界划清楚后面几章拆实现。2. 在线编辑器的核心架构从 textarea 到分栏预览2.1 为什么不能直接用 contenteditable 做编辑器很多新手的第一反应是拿一个div加上contenteditabletrue觉得这样就能所见即所得。实际做下去会发现三个致命问题浏览器对富文本的 DOM 结构处理不一致粘贴进来的 Word 内容会带一堆内联样式光标位置在重新渲染后经常跳到开头。在线 Markdown 编辑器的正确做法是「源码区 预览区」分离源码区用textarea或 CodeMirror 这类编辑器组件预览区用 Markdown 解析库把文本转成 HTML 再渲染。这样做的好处是源码始终是纯文本用户对格式有完全控制权不会出现「我想改一个加粗结果整段样式崩了」的情况。CodeMirror 6 是目前比较主流的选择它按需加载语言包体积比 Monaco 小很多适合在线场景。如果只是做一个内部工具直接用textarea加等宽字体也能跑但会失去行号、括号匹配和语法高亮。选型时看两个指标首屏加载时间和输入延迟。CodeMirror 6 在中等配置下首屏可以控制在 200KB 以内输入延迟在低端安卓机上也能保持 50ms 以下。2.2 最小可运行的分栏预览实现下面这段代码用原生 JavaScript 加 marked 库做一个最小可用的在线 Markdown 编辑器。marked 负责解析DOMPurify 负责过滤 XSS两者配合才能安全地把用户输入渲染到页面上。// 引入 marked 和 DOMPurify实际项目中通过 npm 或 CDN 引入 import { marked } from marked; import DOMPurify from dompurify; const editor document.getElementById(editor); const preview document.getElementById(preview); // 配置 marked开启 GFM 支持表格和删除线开启换行转 br marked.setOptions({ gfm: true, // 支持表格、任务列表等 GitHub 风格语法 breaks: true, // 单个换行也渲染成 br符合中文写作习惯 pedantic: false, // 不严格遵循原始 Markdown 规范容错更好 }); function render() { const raw editor.value; // 先解析成 HTML再用 DOMPurify 清洗防止脚本注入 const html marked.parse(raw); preview.innerHTML DOMPurify.sanitize(html, { ADD_ATTR: [target], // 允许链接带 target 属性 }); } // 输入时防抖 150ms避免每敲一个字符就全量重渲染 let timer null; editor.addEventListener(input, () { clearTimeout(timer); timer setTimeout(render, 150); }); // 首次加载时渲染一次 render();这段代码的逻辑很直白监听input事件防抖后调用marked.parse把 Markdown 文本转成 HTML 字符串再交给 DOMPurify 过滤掉script等危险标签最后塞进预览区的innerHTML。参数方面breaks: true是中文写作场景下最常被忽略的一项——默认的 Markdown 规范里单个换行不产生br但中文用户习惯用单换行分段不开这个选项预览效果会和预期差很多。gfm: true则决定了表格、任务列表、自动链接这些扩展语法是否生效做在线工具时基本必开。注意DOMPurify 的配置不要随意放开ALLOW_UNKNOWN_PROTOCOLS否则javascript:伪协议链接可能绕过过滤。2.3 实时预览的性能边界与优化手段当文档超过 5000 字或者包含大量代码块时每次输入都全量重新解析会明显卡顿。我实测过一份 8000 字的文档在 Chrome 上全量解析加渲染大约需要 120ms防抖 150ms 的情况下用户连续输入时预览区会「追不上」光标。优化手段有三个层次第一层是防抖时间从 150ms 调到 300ms代价是预览延迟感变强第二层是只重新渲染变化的那一段但这需要 diff 算法实现复杂度高第三层是用 Web Worker 把解析放到后台线程主线程只负责把 HTML 字符串贴到预览区。Web Worker 方案的关键是把 marked 的解析逻辑放进 worker 文件通过postMessage传递文本和结果。这样做主线程的输入响应始终流畅代价是每次通信有约 5ms 的序列化开销。对于在线编辑器来说这个开销完全可以接受。如果不想引入 worker还有一个取巧的办法在用户输入时先隐藏预览区停止输入 300ms 后再显示并渲染用视觉上的「闪一下」换取输入流畅度。这个方案在内部工具里够用但对外产品不建议。3. 语法细节落地表格、换行、图片路径与 Mermaid3.1 表格转换与对齐方式的处理Markdown 表格是在线编辑器里最容易出问题的语法之一。标准写法要求表头下方有一行分隔线用冒号控制对齐。很多用户从 Excel 复制表格过来粘贴的是制表符分隔的文本直接贴进编辑器不会自动变成 Markdown 表格。一个实用的功能是在编辑器里加一个「从剪贴板导入表格」的按钮读取text/plain里的制表符按行拆分后拼成 Markdown 表格语法。// 把制表符分隔的文本转成 Markdown 表格 function tsvToMarkdown(tsv) { const rows tsv.trim().split(\n).map(row row.split(\t)); if (rows.length 0) return ; const header rows[0]; // 分隔行默认左对齐数字列可以后续手动改成右对齐 const separator header.map(() ---); const body rows.slice(1); const lines [ | header.join( | ) |, | separator.join( | ) |, ...body.map(row | row.join( | ) |), ]; return lines.join(\n); }对齐方式在 Markdown 里用冒号表示:---左对齐:---:居中---:右对齐。在线编辑器可以在预览区的表格单元格上加点击切换对齐的功能但实现起来要操作源码区的文本容易把光标搞乱。我一般建议用户在源码里手动改或者提供一个「表格格式化」按钮把当前光标所在表格的每一列统一成指定对齐方式。表格复制到 Excel 时Markdown 源码里的|分隔符会被 Excel 识别为列分隔符但表头下方的---行会变成一行无意义的数据导出前记得删掉。3.2 换行、方框与特殊符号的输入Markdown 换行有两种方式行尾加两个空格再换行或者直接用一个空行分段。在线编辑器里行尾空格很难看见用户经常困惑「为什么我换了行预览里没换」。开启breaks: true后单个换行就生效这是最省心的做法。如果不想全局开启可以在工具栏加一个「软换行」按钮点击后在光标处插入两个空格加换行符。方框符号在任务列表里用- [ ]和- [x]表示渲染出来就是复选框。有些用户想要的是纯文本方框字符比如☐和☑这属于 Unicode 字符直接复制粘贴即可和 Markdown 语法无关。圈1到圈19这类符号① 到 ⑲也是 Unicode 字符在中文技术文档里常用来做步骤编号。在线编辑器不需要特殊处理但要注意字体支持——某些等宽字体不包含这些字符预览区会显示成方块。解决办法是在 CSS 里给预览区指定一个包含这些符号的字体回退链比如PingFang SC, Microsoft YaHei, monospace。3.3 图片路径与 Mermaid 图表的渲染图片路径是在线编辑器最容易翻车的地方。用户从本地拖一张图片进来编辑器如果只是插入预览区在浏览器里根本加载不出来。正确的做法是拦截拖拽事件把图片文件转成 Base64 或者上传到图床后返回 URL。Base64 的优点是简单缺点是文档体积会膨胀约 33%一张 200KB 的截图会让 Markdown 源码多出 270KB 的字符编辑器输入会变卡。我一般建议超过 50KB 的图片走上传小图标可以用 Base64。Mermaid 图表的渲染需要额外引入 mermaid 库并且在预览区渲染完成后扫描language-mermaid的代码块把内容交给 mermaid 渲染成 SVG。这里有个时序问题marked 解析出的 HTML 里代码块是precode classlanguage-mermaid需要先找到这些元素替换成div classmermaid再调用mermaid.init。如果文档里有多个 Mermaid 图要等所有图都渲染完再更新预览区高度否则会出现滚动条跳动。// 在预览渲染完成后处理 Mermaid 代码块 import mermaid from mermaid; mermaid.initialize({ startOnLoad: false, theme: neutral }); async function renderMermaid() { const blocks preview.querySelectorAll(code.language-mermaid); for (const block of blocks) { const graphDef block.textContent; const container document.createElement(div); container.className mermaid; container.textContent graphDef; block.parentElement.replaceWith(container); } // 渲染所有 .mermaid 元素 await mermaid.run({ querySelector: .mermaid }); }这段代码先收集所有 Mermaid 代码块把它们的文本内容搬到新的div里替换掉原来的pre结构最后统一调用mermaid.run。参数theme: neutral是中性配色适合大多数文档背景。如果文档背景是深色要改成dark主题否则图表里的文字会看不清。4. 导出与工作流Markdown 转 Word、HTML 和公众号格式4.1 Markdown 转 Word 的序号自动编号问题把 Markdown 转成 Word 时有序列表的编号经常出问题。Markdown 源码里写的是1.2.3.转成 Word 后如果直接映射成普通段落文本编号是写死的插入或删除条目后不会自动更新。正确的做法是在转换时生成 Word 的编号列表结构让 Word 自己管理序号。用docx这个 npm 库可以在 Node.js 环境生成带编号的 Word 文档核心是给段落设置numbering属性。// 使用 docx 库生成带自动编号的有序列表 import { Document, Paragraph, TextRun, Packer, Numbering } from docx; const doc new Document({ numbering: { config: [{ reference: ordered-list, levels: [{ level: 0, format: decimal, // 十进制编号 text: %1., alignment: start, }], }], }, sections: [{ children: [ new Paragraph({ text: 第一步安装依赖, numbering: { reference: ordered-list, level: 0 }, }), new Paragraph({ text: 第二步配置参数, numbering: { reference: ordered-list, level: 0 }, }), ], }], }); // 导出为 Buffer 后写入文件 Packer.toBuffer(doc).then(buffer { fs.writeFileSync(output.docx, buffer); });这里的关键是numbering.config里定义的reference要和段落里的numbering.reference对应上。format: decimal表示阿拉伯数字如果要中文的「一、二、三」改成chineseCounting。text: %1.里的%1是层级占位符表示第一级编号。这样生成的 Word 文档用户在 Word 里增删条目时编号会自动重排不会出现「删了第二条结果编号还是 1、3、4」的尴尬。4.2 公众号格式的粘贴兼容处理把 Markdown 渲染后的 HTML 直接粘贴进公众号编辑器样式经常丢失尤其是代码块和表格。原因是公众号编辑器会过滤掉大部分 CSS 类和外部样式表。可行的方案是在复制时把关键样式内联到每个元素上用juice这类库做 CSS 内联或者手动给代码块加上stylebackground:#f6f8fa;padding:12px;border-radius:4px;font-family:monospace。表格则要加上border-collapse:collapse和单元格边框否则粘贴过去没有框线。另一个坑是图片。公众号不允许外链图片粘贴过去的img srchttps://...会显示「此图片来自微信公众平台未经允许不可引用」。解决办法是在复制前把图片转成 Base64 内联或者提示用户手动上传到公众号素材库。Base64 内联的图片在公众号编辑器里可以正常显示但文档体积会变大适合图片数量少的场景。4.3 与自动化工作流的衔接有些团队会把在线编辑器和自动化工具串起来比如在编辑器里写完 Markdown 后一键推送到 Coze 或 Dify 的工作流自动转成 Word 并归档。这种场景下编辑器需要提供一个「导出 JSON」的接口把 Markdown 源码和元数据标题、作者、标签一起输出工作流那边再调用转换服务。关键是要约定好字段名和转义规则避免 Markdown 里的特殊字符破坏 JSON 结构。我一般会在导出前把源码里的反斜杠和引号做一次转义接收端再反转义。5. 避坑与排查在线编辑器最常见的五个翻车现场5.1 预览区代码块没有语法高亮现象代码块渲染出来了但全是黑色文字没有颜色区分关键字和字符串。原因marked 只负责把代码块转成precode classlanguage-xxx语法高亮需要额外引入 highlight.js 或 Prism.js并且在每次预览渲染后重新调用高亮函数。解决在render函数末尾加preview.querySelectorAll(pre code).forEach(block hljs.highlightElement(block))注意要在 DOMPurify 清洗之后执行否则高亮生成的span标签可能被过滤掉。5.2 表格粘贴到 Excel 后多出一行分隔线现象从预览区选中表格复制到 Excel第一行是表头第二行是---|---|---第三行才是数据。原因复制的是渲染后的 HTML 表格但某些浏览器会把 Markdown 源码里的分隔行也带进剪贴板。解决在复制事件里拦截只取table元素的outerHTML或者提供一个「复制为 TSV」的按钮把表格转成制表符分隔的纯文本再写入剪贴板。5.3 图片路径在本地能看分享给别人就裂了现象自己电脑上预览正常把 Markdown 文件发给同事后图片全部显示不出来。原因图片路径是本地绝对路径C:\Users\...或相对路径./images/xxx.png对方没有对应的文件。解决在线编辑器应该强制图片走上传或 Base64禁止插入本地路径。如果用户手动输入了本地路径在预览时检测src是否以file://或盘符开头是的话显示一个占位提示「图片未上传」。5.4 Mermaid 图表在 Safari 上渲染错位现象Chrome 里正常的流程图在 Safari 里节点重叠或者文字溢出。原因Safari 对 SVG 的foreignObject支持有差异Mermaid 默认用foreignObject渲染 HTML 标签Safari 下计算尺寸会偏小。解决在mermaid.initialize里加flowchart: { htmlLabels: false }让 Mermaid 用纯 SVG 文本渲染标签牺牲一点样式灵活性换取跨浏览器一致性。5.5 输入中文时预览区闪烁现象用拼音输入法打字时每按一个字母预览区就重新渲染一次屏幕不停闪。原因输入法的组合输入过程中也会触发input事件防抖时间太短或者没有区分compositionstart和compositionend。解决监听compositionstart时设一个标志位compositionend时清除标志位在input事件里判断如果标志位为真就跳过渲染。这样拼音输入过程中预览区不动选词确认后才更新。6. 进阶技巧用 IndexedDB 做本地草稿与版本回溯在线编辑器最让人没有安全感的一点是刷新页面或者网络断开刚写的内容就没了。用localStorage做自动保存是常见做法但它有 5MB 的大小限制而且存储的是字符串文档多了会撑爆。更稳妥的方案是用 IndexedDB它支持结构化存储可以按时间戳保存多个版本实现「后悔药」功能。// 用 IndexedDB 保存文档版本 const DB_NAME markdown-editor; const STORE_NAME drafts; function openDB() { return new Promise((resolve, reject) { const request indexedDB.open(DB_NAME, 1); request.onupgradeneeded () { const db request.result; // 以自增 id 为主键timestamp 建索引方便按时间查询 const store db.createObjectStore(STORE_NAME, { keyPath: id, autoIncrement: true }); store.createIndex(timestamp, timestamp); }; request.onsuccess () resolve(request.result); request.onerror () reject(request.error); }); } async function saveDraft(content) { const db await openDB(); const tx db.transaction(STORE_NAME, readwrite); tx.objectStore(STORE_NAME).add({ content, timestamp: Date.now(), }); // 只保留最近 50 个版本超出的删掉 const count await new Promise(resolve { const req tx.objectStore(STORE_NAME).count(); req.onsuccess () resolve(req.result); }); if (count 50) { const index tx.objectStore(STORE_NAME).index(timestamp); const cursorReq index.openCursor(); cursorReq.onsuccess () { const cursor cursorReq.result; if (cursor) { cursor.delete(); cursor.continue(); } }; } }这段代码做了两件事每次保存时往 IndexedDB 里追加一条记录包含内容和时间戳当记录数超过 50 时按时间戳索引删除最旧的记录。参数autoIncrement: true让主键自动生成不需要手动管理 id。timestamp索引用于按时间排序和清理。实际使用时可以在编辑器顶部加一个「历史版本」下拉菜单读取最近 10 个版本的内容点击后恢复到编辑器里。这个功能在用户误删段落或者想对比修改前后时特别有用。我自己的习惯是每 30 秒自动保存一次同时在用户手动按下 CtrlS 时强制保存并弹一个轻提示。这样即使浏览器崩溃最多也只丢 30 秒的内容。另外IndexedDB 的数据是按域名隔离的换域名或者清浏览器数据都会丢所以重要的文档还是要提供导出为.md文件的按钮让用户自己留一份底。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
5年开发避坑:aecc2018手写实现拆解 5年开发避坑:aecc2018手写实现拆解 刚入行时,我盯着屏幕上的 for 循环发呆,语法背得滚瓜烂熟,但一动手搭项目就脑子一片空白。这种“学会语法却不知怎么搭项目”的无力感,是每个程序员都经历过的至暗时刻。很多人以为这是逻辑问题,其实是… · 2026/9/23 17:33:22
夏普ccd源码解析:3类报错对比与选型实战指南 夏普ccd源码解析:3类报错对比与选型实战指南 线上夏普ccd模块一启动,控制台直接吐出一长串红色Stack Trace, NullPointerException 连着 IOException… · 2026/9/23 17:33:10
Maya最新踩坑实录:3个报错完整示例教你搞定 Maya最新踩坑实录:3个报错完整示例教你搞定 控制台红屏一片,StackTrace 长得像天书,鼠标滚轮都快转断了也找不到头。这种时刻,谁还没在 Maya 里被 AttributeError 或者 TypeError… · 2026/9/23 17:33:09
Fn键本质是硬件级键位映射切换开关 1. Fn键不是“隐藏功能”,而是被系统刻意设计的交互分层机制Fn键,全称Function Key,中文常被叫作“功能键”或“组合键开关”,但它既不是快捷键,也不是传统意义上的修饰键(Modifier Key)——它和… · 2026/9/23 18:14:03
5个坑搞定盛大网络热血传奇官网性能优化 5个坑搞定盛大网络热血传奇官网性能优化 看了一堆教程还是不会写项目?别慌,这不是你的错,是教程太“理想化”了。很多老手在 掘金技术社区… · 2026/9/23 18:13:57
Win11任务栏秒针显示:系统级时间精度增强指南 1. 这不是“隐藏彩蛋”,而是Win11真内置功能:任务栏秒针显示的来龙去脉你有没有在某个深夜加班时,盯着右下角那个跳动的时钟,突然发现——咦?它居然在动?不是每分钟跳一下,而是实实在在的“滴、… · 2026/9/23 18:13:51
4v1选型避坑指南:新手别再乱抄代码了 4v1选型避坑指南:新手别再乱抄代码了 刚接手项目,从网上抄了一段 4v1 数据聚合代码,结果一跑就报错?别急,这坑我踩过,你也别急。很多新手一上来就找“通用模板”,结果发现根本跑不通,连报错信息都看不懂,更别提怎么调了。 做 4v1… · 2026/9/23 18:13:50
学生党变声整活实测|4 款变声器横评,手机电脑全都有,一次搞定 最近刷短视频总能刷到变声整活,不管是联机游戏语音、和室友线上开玩笑,还是给自己短视频配趣味旁白,变声器直接把氛围感拉满。很多同学来问,市面上这么多变声软件,到底该选哪一个?我陆续试了 4 款热门工具&… · 2026/9/23 18:13:44
JSP+Servlet+JavaBean老项目拆解:从源码结构到二次开发 简介:面向全国计算机等级考试二级Office辅导答疑场景,这套基于JSP与Java的完整项目源代码,适合Web开发学习者、毕业设计者以及需要搭建在线练习答疑平台的开发者。压缩包共1568个文件、约38.12MB,主要包含jsp页面、Java类、jar依赖… · 2026/9/23 18:13:44
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29