1. 为什么公众号排版这么折腾以及我为什么干脆写了个在线工具做技术写作的人大概都有这种体验本地用 Typora、VS Code 或 Obsidian 把 Markdown 写得好好的代码块、表格、加粗、列表全都舒服得很一旦要发公众号排版就变成了一场灾难。公众号编辑器不识别 Markdown 语法标题得手动调字号代码块粘贴过去缩进全丢表格干脆变成一团乱麻。更麻烦的是如果文章里嵌了图片你还得一张张上传传完还要重新核对位置。我见过不少博主写文章花 40 分钟排版花 1 小时这时间分配本身就很有问题。后来市面出现了一些“Markdown 转公众号”的工具比如 md.openwrite、mdnice 这类在线服务确实解决了一部分问题。但它们大多是 SaaS 平台你要注册登录、要绑定公众号、要把内容传到别人的服务器上。有些同事因为内容保密要求根本不敢用这类在线服务还有些人单纯嫌注册流程烦或者担心哪天平台挂了、换域名了自己积累的排版习惯就全废了。所以我自己做了一个“在线一键 Markdown 文章转公众号文章”的小工具。逻辑很简单选好主题样式把 Markdown 内容粘贴进去右侧实时预览点一下“复制”直接粘贴到公众号编辑器里就是排版好的效果。不需要注册不需要上传到任何服务器纯浏览器本地处理。这篇文章就把完整实现思路、技术选型、实操步骤和踩坑记录都分享出来。先给不知道这东西能干嘛的朋友一个定位它适合经常写公众号的技术博主、产品经理、运营同学也适合那些公司内部有内容发布需求、但又不方便用外部 SaaS 平台的团队。只要你会写 Markdown就能在 10 秒内得到一份排版合格的公众号文章。2. 整体设计思路为什么选纯前端方案而不是架个后端服务2.1 核心需求拆解动手之前我先把需求列清楚只有明确了边界技术选型才不会纠结。工具要解决的核心问题有三个把 Markdown 语法解析成 HTML这是所有转换工具的基本功。把 HTML 套上一层适合公众号展示的 CSS 样式包括字体、行高、代码块配色、表格边框、引用块左侧色条等。提供“复制到公众号”的能力。这一步比想象中麻烦因为公众号编辑器对粘贴内容的过滤规则很严格直接复制 HTML 源码是不行的需要复制带格式的富文本内容。除此之外还有一些不那么起眼但实际使用中很重要的需求图片怎么处理、代码高亮是否引入、表格是否支持、主题样式是否可切换、移动端预览效果是否正常。2.2 技术选型为什么不用后端这个工具我一开始就没打算做后端理由很实际不需要数据存储。用户粘贴 Markdown得到 HTML过程无状态没有任何需要持久化的用户数据。不需要账号体系。绑定公众号、保存文章这种功能本质上是为了平台留存用户但对一个自用工具来说完全是负担。不存在跨域问题。因为不调用任何第三方 API所有解析和样式渲染都在浏览器里完成。部署成本低到可以忽略。一个静态页面丢到 GitHub Pages 或者对象存储上就能用没有服务器也就没有维护成本。纯前端方案的另一个好处是隐私性。文章内容从头到尾只在浏览器内存里走一遍不会经过任何服务器。对很多公司内部文档来说这是个非常实在的卖点。我在给团队内部做分享时就特意强调了这点你贴进去的草稿不会出现在任何数据库里关掉页面就什么都没了。技术栈上我选择了 Marked highlight.js 手写 CSS 主题而不是直接用现成的 mdnice 那套开源代码改。原因很简单Marked 足够轻量解析速度快API 清晰highlight.js 的代码高亮配色成熟支持语言多剩下的排版样式部分自己写 CSS 反而更灵活不受既有框架束缚。2.3 方案对比自研 vs 现有工具市面上已有的 Markdown 转公众号工具不算少我列个表格对比一下方便你理解我在设计上的取舍对比维度自研纯前端工具mdnice 等在线平台手动在公众号编辑器排版是否需要注册不需要多数需要不需要但受罪内容是否经过服务器否纯本地是不适用排版速度秒级秒级10-30 分钟主题可定制性自己改 CSS完全可控受平台限制完全可控但费时代码高亮支持支持几乎不支持离线可用可以不行不适用长期可用性静态页面随时自托管依赖平台存续不适用我并不是说现有平台不好它们做得挺成熟。但“纯前端、零依赖、可自托管”这件事对于喜欢掌控感的技术人来说吸引力太大了。你想想一个 HTML 文件存本地双击打开就能用不依赖任何外网资源这感觉完全不一样。3. 核心功能拆解Markdown 解析、代码高亮、样式引擎3.1 Markdown 解析器选型Marked 为什么够用Markdown 解析器有很多marked、markdown-it、remark、showdown 等等。我最终选了 Marked原因有三个。第一体积和速度。Marked 的压缩后体积只有几十 KB解析速度在同类库里属于第一梯队。公众号文章单篇基本在几千到一万字Marked 解析耗时可以忽略不计实际测试中 1 万字的 Markdown 文档解析加渲染不超过 50 毫秒。第二配置足够灵活。Marked 支持自定义渲染器renderer这意味着我可以拦截特定节点的输出。比如图片节点我可以在渲染时注入自定义处理逻辑代码块节点我可以决定是否交给 highlight.js 处理。第三生态成熟。Marked 的社区用户量大遇到问题基本搜一下就有答案。相比 markdown-it 那种插件化架构Marked 更轻也更契合我这种“不太需要扩展、只要核心转换”的场景。实际使用中我用的是 marked 的经典用法import { marked } from marked; marked.setOptions({ gfm: true, breaks: true }); const html marked.parse(markdownContent);这里有两个选项要特别说明。gfm开启 GitHub 风格 Markdown这样表格、删除线、任务列表这些语法都能被正确解析。breaks开启换行转换这个对公众号写作非常重要因为很多人在 Markdown 里习惯用单换行来分段breaks: true后单个换行就会渲染成br避免出现“写的时候明明换行了渲染出来全挤在一起”的问题。3.2 代码高亮highlight.js 的接入细节公众号文章的读者有很大比例会把代码块截图保存所以代码高亮做得好不好直接影响文章的观感。我选了 highlight.js接入流程比较简单npm install highlight.js然后在入口文件里引入样式和注册语言import hljs from highlight.js; import highlight.js/styles/github.css; // 按需注册常用语言没必要全量引入 import javascript from highlight.js/lib/languages/javascript; import typescript from highlight.js/lib/languages/typescript; import python from highlight.js/lib/languages/python; import bash from highlight.js/lib/languages/bash; import json from highlight.js/lib/languages/json; hljs.registerLanguage(javascript, javascript); hljs.registerLanguage(typescript, typescript); hljs.registerLanguage(python, python); hljs.registerLanguage(bash, bash); hljs.registerLanguage(json, json);这里有个细节值得注意全量引入 highlight.js 会加载所有语言包体积一下多出几百 KB。对一个纯前端页面来说这个体积虽然不算致命但没必要。按需注册语言包只留自己常用的几种加载速度和页面性能都会更好。关键的接入逻辑在 Marked 的自定义渲染器里我拦截了code节点的渲染const renderer { code(code, infostring, escaped) { const lang (infostring || ).match(/\S*/)[0]; const validLang hljs.getLanguage(lang) ? lang : plaintext; if (validLang ! plaintext) { const highlighted hljs.highlight(code, { language: validLang }).value; return pre classhljs code-blockcode${highlighted}/code/pre; } return pre classhljs code-blockcode${escapeHtml(code)}/code/pre; } }; marked.use({ renderer });这里要注意的坑是如果代码块标注的语言 hljs 不认识一定要 fallback 到plaintext并且把代码内容做 HTML 转义。否则用户写了div这种标签会直接被浏览器解析页面上什么都看不清甚至可能被注入脚本这是安全红线。3.3 样式引擎一套 CSS 如何支撑多主题切换工具支持多套主题样式切换这是提升使用体验的一个重要功能。有人喜欢浅色简洁风格有人喜欢深色酷炫风格还有人喜欢类似知乎、掘金的特定配色。我的实现方式不是为每个主题写一套完整 CSS 文件而是用 CSS 变量统一管理。思路大概是这样的.theme-default { --primary-color: #2f80ed; --bg-color: #ffffff; --text-color: #333333; --code-bg: #f6f8fa; --border-color: #e1e4e8; --quote-bg: #f0f7ff; } .theme-dark { --primary-color: #58a6ff; --bg-color: #0d1117; --text-color: #c9d1d9; --code-bg: #161b22; --border-color: #30363d; --quote-bg: #1f2937; }然后在具体样式中引用这些变量.markdown-body { background-color: var(--bg-color); color: var(--text-color); } .markdown-body blockquote { background: var(--quote-bg); border-left: 4px solid var(--primary-color); } .markdown-body pre code { background: var(--code-bg); border: 1px solid var(--border-color); }切主题的时候只需要切换根容器的 class 名所有颜色自动变化。这个方案的优点是不用维护多份重复的 CSS 文件改一个主题的配色只需要替换变量值新主题的创建成本也低加一组变量就行。4. 实操过程从搭建页面到完成复制到公众号的完整链路4.1 页面布局与交互设计页面布局我没有搞得很花哨核心就是左右两栏结构左边是 Markdown 输入区右边是预览区。输入区放一个textarea预览区放一个div两者联动。交互逻辑相当简单const textarea document.getElementById(markdown-input); const preview document.getElementById(preview); textarea.addEventListener(input, () { const html marked.parse(textarea.value); preview.innerHTML html; });监听输入事件每次内容变化都重新解析和渲染。由于 Marked 速度足够快不需要做防抖也能流畅运行。实测输入过程中不会出现卡顿或闪烁。顶部放了一排操作按钮主题切换、复制到公众号、清空内容。这里最核心的是“复制到公众号”按钮它的实现原理决定了整个工具能不能在公众号里真正用起来。4.2 复制到公众号的浏览器兼容方案这是整个工具里技术含量最高、坑最多的一部分。公众号编辑器本质上是一个富文本编辑器它接受的是带格式的 HTML 粘贴。浏览器在复制富文本时依赖的是ClipboardEvent里的clipboardData我们手动触发复制时要用document.execCommand(copy)或者新版navigator.clipboard.write()。要实现“复制富文本”关键思路是把预览区的 HTML 内容放入一个隐藏的、可编辑的容器中。选中这个容器里的内容。执行复制命令。浏览器会把选中内容的 HTML 格式一起放进剪贴板。具体实现如下function copyToClipboard() { const previewContent document.getElementById(preview); const range document.createRange(); range.selectNodeContents(previewContent); const selection window.getSelection(); selection.removeAllRanges(); selection.addRange(range); const success document.execCommand(copy); selection.removeAllRanges(); if (success) { showToast(已复制去公众号粘贴即可); } else { showToast(复制失败请手动选中预览区内容复制); } }document.execCommand(copy)虽然已经被标记为废弃 API但至今在 Chrome、Safari 里依然是复制富文本最可靠的方式。新版navigator.clipboard.write()想写入 HTML 格式会比较绕需要构造ClipboardItem而且对text/html的 MIME 类型支持在不同浏览器里不一致。这里我踩过一个具体的坑如果不把预览区内容放进一个富文本可编辑区域而是直接选中普通div的内容Chrome 复制出来的内容可能丢失部分格式。解决办法是给预览容器加上contenteditabletrue属性但这样用户在预览区点击时就会触发编辑又不好。最终我做了个折中复制时临时给预览容器加上contenteditable复制完立刻移除。4.3 复制后的样式细节微信的特殊处理逻辑复制到公众号编辑器后有几个细节是必须处理的不然排版会出问题。第一个是图片宽度。公众号编辑器对图片的默认样式有特殊要求如果你在 HTML 里写了img src... stylewidth: 100%粘贴后可能被过滤或变形。所以在渲染 Markdown 中的图片时我额外加了一层包裹const renderer { image(href, title, text) { return p classimg-containerimg src${href} alt${text} title${title || }/p; } };用p标签包裹图片并设置了图片最大宽度 100%、高度自适应这样在公众号里显示不会超出屏幕宽度。第二个是字体族。公众号编辑器默认字体是系统字体如果你不在 CSS 里指定粘贴后可能显示异常。我在样式里明确设置了.markdown-body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif; font-size: 15px; line-height: 1.75; word-break: break-word; }15px 字号是公众号阅读比较舒服的尺寸行高 1.75 也是经过多次测试比较满意的值太密读者看着累太疏又显得浪费版面。第三个是链接颜色。公众号默认链接是蓝色但如果你文章里大量使用链接最好在样式里统一指定颜色避免某些主题下链接颜色和正文颜色区分度不够。我用了主题色变量控制.markdown-body a { color: var(--primary-color); text-decoration: none; }4.4 代码块的微信粘贴适配代码块在公众号里是最容易出问题的部分。我实测过很多种方案最终采用了“复制时保留高亮 背景色用内联样式”的组合策略。公众号编辑器粘贴时会保留大部分内联样式但会丢失一些外部样式表里的规则。这就是为什么很多在线工具的代码高亮在预览时很好看复制到公众号后却变成黑色纯文本。解决方案是代码高亮之后的样式不能只依赖外部 CSS 类名必须把关键颜色转成内联 style。我在复制前做了一步预处理遍历预览区里所有.hljs和.hljs-keyword、.hljs-string这类高亮 span把对应的颜色值通过 JavaScript 计算出来然后写入内联样式。function inlineCodeStyles(container) { const codeSpans container.querySelectorAll(span[class*hljs-]); codeSpans.forEach(span { const color getComputedStyle(span).color; const bgColor getComputedStyle(span).backgroundColor; if (color) span.style.color color; if (bgColor bgColor ! rgba(0, 0, 0, 0)) span.style.backgroundColor bgColor; }); const codeBlocks container.querySelectorAll(pre code); codeBlocks.forEach(block { const bg getComputedStyle(block).backgroundColor; block.style.backgroundColor bg; }); }这一步非常关键不加这个预处理你辛辛苦苦选的代码主题在公众号里基本会失效。5. 我在实战中踩过的坑和总结的排查技巧5.1 Markdown 表格在公众号里的兼容性问题Markdown 表格在公众号里是一个老大难。原因在于微信的富文本编辑器对table标签支持很差粘贴后表格样式经常错乱宽度一会儿撑破页面一会儿又缩成一团。我试过几种方案纯table标签、div模拟表格、复制时转成图片。最终选择了兼容性最好的做法渲染时给表格加固定结构div classtable-wrapper table theadtrth列1/thth列2/th/tr/thead tbodytrtd数据/tdtd数据/td/tr/tbody /table /div然后 CSS 里写.table-wrapper { overflow-x: auto; margin: 16px 0; } .table-wrapper table { width: 100%; border-collapse: collapse; font-size: 14px; } .table-wrapper th { background: #f0f0f0; font-weight: 600; padding: 8px 12px; border: 1px solid #dfe2e5; } .table-wrapper td { padding: 8px 12px; border: 1px solid #dfe2e5; }即使这样在公众号里表格的表现也不能说完美窄表格可能出现挤压。我的建议是表格列数不要超过 5 列数据内容尽量简短这样在手机上浏览效果最舒服。如果有大量数据要展示更建议做成截图图片。另外Markdown 表格转换成 Excel 这个需求我一开始没考虑但后来有朋友问我能不能支持。这个从技术上说跟公众号排版完全两个方向如果要实现得引入 SheetJS 之类库。我没在工具里做但如果你有类似需求思路是把 Markdown 表格先解析成二维数组再用 SheetJS 生成 xlsx 文件网上有现成项目可以参考。5.2 图片路径问题本地图片怎么处理很多新手在本地写 Markdown 时图片使用的是相对路径比如![](./images/1.png)。这种图片在本地预览没问题但粘贴到公众号后图片是显示不出来的因为公众号编辑器的图片是上传到你公众号素材库后才有的 URL。我在工具里对图片做了两种处理如果是完整的网络 URL直接渲染显示。如果是相对路径或本地路径预览区显示一个占位提示提示用户先上传图片到图床或者把图片直接粘贴进公众号后再调整位置。这个限制在纯前端方案里无法解决因为浏览器出于安全限制不能读取本地文件的完整路径并展示。我的经验是正经写公众号文章的人早晚都要用图床比如七牛云、阿里云 OSS、腾讯云 COS或者 GitHub jsDelivr 这类免费图床。把图片传到图床拿到 URL再写进 Markdown整个流程就畅通了。5.3 换行和段落间距的细节调整Markdown 里换行规则经常让新用户困惑。GFM 里两个空格加换行才是真正的换行而单独一个换行会被忽略。但公众号写作场景里很多人根本没这个概念他们就是习惯写完一行敲个回车。这就是为什么我在 Marked 里开启了breaks: true让单个换行也能渲染成br。但开启这个选项也会带来一个小问题列表项里的换行会被额外拆分导致列表看起来间距过大。解决办法是在 CSS 中对列表项的段间距做归一化处理.markdown-body li p { margin: 4px 0; }这样即使列表里包含换行间距也不会失控。这个细节我自己用的时候才意识到最初版本的列表排版间距确实有问题。5.4 移动端预览和公众号手机端的对齐写公众号的人必须考虑手机阅读体验超过 70% 的用户是用手机打开的。所以我给预览区加了一个“移动端模式”开关切换后预览宽度变成 375px模拟手机屏幕。这个功能实现起来很简单function toggleMobilePreview() { const previewWrap document.getElementById(preview-wrapper); previewWrap.style.maxWidth previewWrap.style.maxWidth 375px ? 100% : 375px; previewWrap.style.margin 0 auto; }别小看这个功能很多排版问题在电脑上看根本不明显一切换到移动端立马暴露表格超宽、代码块横向滚动、图片撑破容器、标题字太大。我在实际写文章时几乎每次都要切到移动端预览检查一遍才敢复制去公众号。5.5 常见问题速查我把自己用这个工具过程中遇到的典型问题整理成一张表方便你直接对照解决现象可能原因解决办法复制到公众号后代码没有高亮高亮颜色没有内联化检查复制前是否执行了inlineCodeStyles()预处理表格在公众号里错乱表格列数太多或内容过长限制列数在 5 列以内内容尽量精简图片显示不出来使用了本地相对路径换成图床 URL 或直接粘贴图片到公众号换行全部消失未开启breaks: true在 Marked 配置中开启breaks选项复制时格式丢失预览容器缺少contenteditable复制前临时添加contenteditable属性深色主题下公众号页面异常背景色被内联化到了整个容器复制时仅内联代码块的背景色不要内联容器背景色代码块横向滚动条不出现缺少overflow-x: auto样式给pre添加overflow-x: auto并设置white-space: pre5.6 离线可用和部署技巧这个工具我最终打成了一个纯静态项目HTML CSS JavaScript所有依赖都通过 npm 打包进产物没有使用 CDN 链接。这样做的最大好处是文件下载到本地也能用离线状态完全可用。如果你也希望部署一份给自己团队用我推荐三种方式GitHub Pages仓库 push 后自动发布免费且稳定。对象存储静态网站托管阿里云 OSS / 腾讯云 COS 都有静态网站托管功能要点是设置好 Index 文档为index.html。本地局域网直接把dist目录用python -m http.server 8080起个服务团队内网就能访问。如果你是纯个人使用更简单把打包后的index.html双击打开浏览器直接运行。由于没有用到任何需要服务器环境的功能这个工具天然支持 file:// 协议。6. 这个工具还能怎么扩展做完这个工具后我意识到它的架构其实可以延展到很多方向。给你几个我验证过可行的扩展思路第一接入更丰富的主题。目前我内置了四套主题默认浅色、深色、知乎风格、极简风格。你完全可以根据自己公众号的视觉定位定制专属主题改 CSS 变量就行。我建议至少准备两套一篇技术文章用深色代码块主题一篇行业观点文章用浅色简洁主题。第二增加“导出为图片”功能。公众号文章封面图、Twitter 分享卡片、朋友圈配图这些场景都需要把文章部分内容转成图片。技术上可以使用 html2canvas 这类库把预览区渲染成 canvas再导出为图片。我试验过效果还不错但要注意长文章截图的性能问题可以先对内容分页再逐页截图。第三接入 AI 辅助写作。最近大家都在用 AI 写文章Markdown 格式天然适合作为 AI 输出的载体。你可以把 AI 生成的 Markdown 直接粘贴到这个工具里获得公众号排版也可以用 Coze、Dify 这类工作流编排平台把 Markdown 转 Word 或转公众号排版的步骤自动化。我看到不少关于“Markdown 转 Word 工作流”、“Dify Markdown 转 Word 自动编号”的讨论本质上就是把这套转换逻辑嵌入到更大的流程里。第四增加字数统计和阅读时长估算。公众号后台有自己的统计但写作时实时看到字数变化和预计阅读时长对控制文章篇幅还是有帮助的。实现不算复杂统计中文字符数和空白分隔的英文单词数再按每分钟 300-400 字的阅读速度估算即可。7. 关于 Markdown 编辑器的一点题外话既然标题和热搜词里都出现了很多 Markdown 编辑器的相关内容我也顺手聊几句自己的看法毕竟这个工具的源头就是从 Markdown 写作场景来的。Typora 是最流行的 Markdown 编辑器之一所见即所得的特性让它上手几乎没有门槛。网上能找到所谓“Typora 中文破解版”的下载资源但我的建议是如果是常用的工具几十块钱买份正版省心也安全。“破解版”文件来源不明插入恶意代码的风险不是没有没必要为这点钱去赌。其他我实际用过的编辑器里VS Code 加 Markdown Preview Enhanced 插件适合程序员支持 Mermaid 图表预览、导出 PDF、自定义 CSS功能很强Obsidian 适合做知识库和双链笔记它的 Markdown 文件和插件生态都做得很成熟Notion 虽然也支持 Markdown 语法输入但底层并不是标准的 Markdown 文件系统迁移性略差。“对 deepseek 提问用自然语言还是 Markdown 更容易让 AI 明白指令”这个话题也有不少人讨论。我的经验是对于普通对话自然语言足够对于复杂的多步骤任务Markdown 的结构化格式对 AI 理解约束条件有帮助比如用标题、列表、表格清晰列出输入、约束、输出格式时AI 犯迷糊的概率显著降低。这和写文章是一个道理——结构清晰的内容解析端和消费端的体验都会更好。我在实际使用这套工具时最大的体会是排版的本质不是“把工具用熟”而是“把写作和发布之间的摩擦降到最低”。当你不再需要为了换个字体大小或调个行间距打断思路写作的流畅度完全不一样。这篇文章里分享的每一个细节都是我自己从“临时用一下”到“每天离不开”的过程中踩出来的。你现在按着这个思路做遇到的坑大概率会比我少一些。最后再分享一个小技巧复制到公众号之后如果发现某些细节不对别急着回到工具里改。先看看是不是公众号编辑器自身的样式覆盖问题——比如字体颜色、行间距、列表缩进这些公众号编辑器有自己的默认规则。确认是样式被覆盖了再回到工具里用内联样式调整往往一次就能解决。排版这件事很多时候差的就是这点耐心。