乐于分享
好东西不私藏

不想用第三方在线编辑器?我用Vue3手搓了一个公众号专属Markdown

不想用第三方在线编辑器?我用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 { start0end0selected“” };

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

✨ 拖拽分栏的时候,一定要给bodyuser-select: none,不然拖的时候会选中文本,体验特别差

✨ 所有样式必须转成行内,微信会把<style>标签直接删掉,任何class都留不住

✨ 不要加什么零宽字符凑bug,污染源码不说,用户复制还会带出不可见字符,坑人

🎉 总结一下

其实做这个小工具,搭架构挺快的,核心逻辑加起来也就那些代码,但细节微调和与公众号文章之间的反复粘贴测试太折腾人,不过这些我都给你趟平了

现在,你就拥有了一个:不用看广告,不用冲会员,所有格式都是按我自己写技术文章的习惯来的,想改哪里改哪里。


如果你也天天写公众号技术文章,又烦透了第三方编辑器的各种破事,不妨照着这个思路自己搓一个,真的不难~

如果你觉得这篇思路对你有用,麻烦点个赞❤️ 收藏一下,不然下次想用找不到了~关注我,不定期分享全栈开发踩坑和好玩小工具的制作思路~