不想用第三方在线编辑器?我用Vue3手搓了一个公众号专属Markdown
👩💻我是爱折腾的一名程序媛,喜欢研究全栈开发的各种实践,热爱分享踩坑后的收获与思考,也享受用代码写出各种实用小工具解决问题的快乐。
如果你也在技术这条路上向前走,关注我,愿我们能彼此陪伴,一起成为更好的自己 🌱
文章摘要
写Python全栈技术文章,每次写完都要复制到第三方在线编辑器调格式,粘贴到公众号还经常代码乱套、列表样式丑?
这篇文章我把自己手搓Vue3公众号编辑器的完整思路、踩过的坑都整理出来了,从编辑器工具栏到微信兼容细节,全是实战干货,看完你也能拥有一个完全属于自己的排版工具~
🤔 你有没有这种痛点?
写技术文章的时候,写完Markdown,复制到公众号后台直接乱成狗。找第三方在线编辑器吧,要么广告满天飞,要么导出还要开会员,代码块还经常抽风。
之前写一篇文章,光调格式就花了半小时,比写文章本身还累🤦♀️忍不了,干脆自己撸一个得了,反正需求也不复杂:能写能预览,直接复制就是公众号能用的格式,不用折腾第三方。
最近一直没更新,也是在折腾这个小工具,总算是能用了,分享给有需要的朋友们!今天这篇文章就是用这个编辑器写的哟!!!
先来个预览:

🎯 核心需求其实很简单
说白了,我们要做的就是这么一件事:
✨ 左边写Markdown,右边实时看公众号风格的预览
✨ 点击按钮一键复制,粘贴到公众号直接能用,不用二次改格式
✨ 常用编辑功能都有:加粗、斜线、插代码块、撤销、查找替换,甚至还能拖动改左右宽度
✨ 完全本地运行,不用怕会员,不用怕数据被爬
🛠️ 整体开发思路(5分钟就能搭完骨架)
好,咱们先来理清楚最核心的流程:
Markdown输入 → 解析成HTML → 把所有样式转成行内 → 复制到剪贴板
技术栈也不用搞复杂,我选的是Vue3 + Vite:
🔹 Markdown解析用 marked,体积小,自定义渲染方便
🔹 代码高亮用 highlight.js,提前把样式转成内联映射就行
🔹 剪贴板直接用浏览器原生API,不用装额外依赖
🔹 UI比较简单,就直接手搓不上框架了
这里说句经验之谈:做这种小工具,别堆一堆没用的依赖,能原生解决就原生解决,打包出来体积小,打开速度快,自己维护也省心~
💡 核心功能怎么实现?挑几个重点说
👉 工具栏:点一下就插入对应Markdown语法
这个功能不难,核心就是拿到编辑区当前选中的文本,前后包上语法就行。
这里踩过一个坑:之前每次点按钮,页面都会自动滚到底部,后来才发现,是修改数据后聚焦没恢复原来的滚动位置。
核心代码也就几行,给你们摘出来:
/** 获取当前光标位置和选中文本 */
functiongetTextareaSelection() {
const el = textareaRef.value;
if (!el) return { start: 0, end: 0, selected: “” };
return {
start: el.selectionStart,
end: el.selectionEnd,
selected: el.value.substring(el.selectionStart, el.selectionEnd),
scrollTop: el.scrollTop,
};
}
/** 替换选中文本(或插入新文本),并恢复光标 */
functioninsertMarkdown(
wrapper,
placeholder = “文本”,
cursorOffset = null,
) {
//…
const { start, end, selected, scrollTop } = getTextareaSelection();
const before = rawMarkdown.value.substring(0, start);
const after = rawMarkdown.value.substring(end);
let replacement, newCursorPos;
if (wrapper.includes(“$1”)) {
// 支持占位符 $1(用于包裹类:加粗、斜体等)
const content = selected || placeholder;
replacement = wrapper.replace(“$1”, content);
newCursorPos = start + replacement.length;
} else {
// 直接插入(标题前缀、列表、分割线等)
replacement = wrapper;
newCursorPos = start + replacement.length;
}
rawMarkdown.value = before + replacement + after;
nextTick(() => {
el.focus();
// 如果有选中文本被包裹,光标放在替换内容末尾
el.selectionStart = el.selectionEnd = newCursorPos;
// 强制恢复滚动条位置
el.scrollTop = scrollTop;
});
}
是不是很简单?像加粗就是前后插**,代码块就是前后插三个反引号,逻辑都是通的。
你可能会问:点插入代码块,光标怎么自动跑到中间?之前我也踩过这个坑,默认插完光标在外面,还要手动挪,特别烦。改完之后就舒服多了:
case“codeBlock”:
const lang = language || “python”;
if (selected) {
// 有选中文本:包裹在代码块中,光标停在选中文本末尾
insertMarkdown(`\n\`\`\`${lang}\n$1\n\`\`\`\n`, “”);
} else {
// 无选中文本:插入空代码块,光标自动定位到中间,方便直接粘贴
const cursorOffset = 5 + lang.length; // 即 \n“`python\n 之后的那个空行位置
insertMarkdown(
`\n\`\`\`${lang}\n\n\`\`\`\n`,
“”,
cursorOffset,
);
}
break;
插完代码块结构就能直接粘贴代码,不用再点一下挪光标,爽多了~
👉 自定义列表Emoji,告别丑丑的默认圆点
其实一直有点看不上列表默认的圆点列表,干脆改成Emoji,好看还层次分明。
这里有个关键点:微信不支持CSS伪元素,所以我们必须把Emoji写到真实DOM里,不然粘贴过去就没了。
核心逻辑就是递归处理所有ul,给每个li前面插一个带Emoji的span:
constBULLETS = [‘✨ ‘, ‘▸ ‘, ‘◦ ‘, ‘▪ ‘]
functiondecorateUl(ulElement, depth = 0) {
const bullet = BULLETS[depth] || BULLETS.at(-1)
ulElement.style.listStyleType = ‘none’
const directLis = Array.from(ulElement.children).filter(c => c.tagName === ‘LI’)
directLis.forEach(li => {
li.style.paddingLeft = ‘1.6em’
li.style.position = ‘relative’
const marker = document.createElement(‘span’)
marker.textContent = bullet
marker.style.cssText = `position:absolute;left:0;top:0;`
li.insertBefore(marker, li.firstChild)
// 递归处理嵌套列表
li.querySelectorAll(‘:scope > ul’).forEach(ul =>decorateUl(ul, depth+1))
})
}
现在不管预览还是粘贴,列表都是美美的Emoji,层级还清晰~
👉 代码块兼容微信:解决长代码挤成一团的坑
这个是最坑的!公众号会把把white-space强制改成pre-wrap,之前我试过各种方法,最后总结出最稳妥的方案:拆解代码块每一行为一个 p 段落,空格转实体以保留缩进
div.querySelectorAll(“pre”).forEach((pre) => {
// 创建替换元素
const section = document.createElement(“section”);
section.style.cssText = WECHAT_TAG_STYLES.pre; // 重点:overflow-x: auto;
// 处理每行为一个 p 标签,以保留换行
const code = pre.innerHTML;
const lines = code.split(“\n”);
lines.forEach((line) => {
const p = document.createElement(“p”);
p.style.cssText = WECHAT_TAG_STYLES.prep; // 重点:white-space: nowrap;
// 空格转实体以保留缩进,注意保留span标签属性定义内的空格:<span style=”xxx”>
line = line.replace(/ (?![^<>]*>)/g, “\u00A0”); // 空格转实体
p.innerHTML = line;
section.appendChild(p);
});
pre.replaceWith(section);
})
constWECHAT_TAG_STYLES = {
pre: “overflow-x:auto;padding:14px 16px;font-size:13px;line-height:1.5;margin: 0.8em 0;border-radius:6px;border:1px solid #e8e8e8;”,
prep: ‘white-space:nowrap;font-family:”JetBrains Mono NL”, Consolas, monospace; font-size: 14px; line-height: 1.3;text-align:left;’,
}
如此就舒服多了,有代码高亮,有缩进,有横向滚动~
📌 最后啰嗦几个容易翻车的点
✨ 同步滚动一定要加锁,不然左右滚动会互相触发,直接卡成PPT
✨ 拖拽分栏的时候,一定要给body加user-select: none,不然拖的时候会选中文本,体验特别差
✨ 所有样式必须转成行内,微信会把<style>标签直接删掉,任何class都留不住
✨ 不要加什么零宽字符凑bug,污染源码不说,用户复制还会带出不可见字符,坑人
🎉 总结一下
其实做这个小工具,搭架构挺快的,核心逻辑加起来也就那些代码,但细节微调和与公众号文章之间的反复粘贴测试太折腾人,不过这些我都给你趟平了
现在,你就拥有了一个:不用看广告,不用冲会员,所有格式都是按我自己写技术文章的习惯来的,想改哪里改哪里。
如果你也天天写公众号技术文章,又烦透了第三方编辑器的各种破事,不妨照着这个思路自己搓一个,真的不难~
如果你觉得这篇思路对你有用,麻烦点个赞❤️ 收藏一下,不然下次想用找不到了~关注我,不定期分享全栈开发踩坑和好玩小工具的制作思路~
夜雨聆风