公众号排版太痛苦?我写了个工具,从飞书到草稿箱一条龙
大家好,我是若风。
写公众号这半年,排版一直是我最烦的环节。
飞书写完文章,复制到微信编辑器,代码块没了样式,表格挤成一团,引用块变成普通文字,图片要一张张手动上传。一篇技术文章,排版比写还久。
试过几个排版工具,要么只能处理纯 Markdown,要么不支持代码高亮,要么生成的东西一到微信编辑器就变形。
后来我自己写了一个:feishu2wx。
项目地址:github.com/wangruofeng/feishu2wx
今天聊聊这个工具做了什么、怎么做的、以及它最近从一个“网页小工具”变成了什么。
微信编辑器是前端工程的噩梦
微信公众号编辑器对 HTML 的限制,远比大多数人想象的严格。我列举一下实际踩过的坑。
CSS 类名全部失效,所有样式必须内联
这是最基本的约束。你不能写一个 .code-block 类然后复用,你得给每一行代码、每一个关键字、每一个背景色都写完整的 style="..." 属性。
feishu2wx 的核心渲染函数 formatForWeChat() 有 1700 多行,其中大量逻辑就是遍历 DOM 树,为每个元素注入完整的内联样式。
只认 px,其他单位全部吞掉
em、rem、vh 在微信编辑器里直接消失。所以所有尺寸都要显式转换为像素。
列表结构会被重新排列
微信编辑器会重排 <li> 里面的内容。你精心设计的嵌套列表,粘贴进去大概率被打乱。
feishu2wx 的解法是:把 <li> 里的所有内容包裹成一个 <span>,把 <li> 内的 <p> 标签扁平化处理,列表内的 <strong> 标签替换为带 font-weight:bold 的 <span>。这些处理都是为了让微信编辑器不乱动。
外部图片链接直接被过滤
不是微信 CDN 上的图片,粘贴进去会被直接移除。推送草稿箱时,后端需要先把所有图片上传到微信素材库,再把 HTML 中的 URL 替换为微信 CDN 地址。
SVG 完全不支持
连基本的多色图标都显示不了。feishu2wx 在导出时会自动把 SVG 通过 Canvas 光栅化为 PNG。
链接颜色也会被吞掉
微信编辑器会移除 <a> 标签上的颜色样式。解法是用 <span> 给链接文字显式加颜色,并且给 <a> 的所有子元素都设置 color: inherit。
代码块语法高亮全靠 computed style 读取
highlight.js 生成的代码高亮是通过 CSS 类名实现的(比如 .hljs-keyword)。但微信不认类名。feishu2wx 的做法是:从预览区的 DOM 里递归读取每个元素的 getComputedStyle(),然后把计算后的颜色值直接写成内联样式。这样做最准确,但也是最耗性能的一步。
这些问题单个看都不算致命,但加在一起,就足以让任何想在公众号上发技术文章的人崩溃。
feishu2wx 要解决的,就是这一整条链路的问题。
从飞书复制粘贴到草稿箱推送,中间发生了什么
整个工具的数据流可以简化成一条管道:
飞书 HTML / Markdown 文本
→ HTML 转 Markdown(如果是飞书粘贴)
→ Markdown 渲染为预览 HTML
→ 内联样式注入 + 微信兼容处理
→ 复制到剪贴板 / 推送到草稿箱
这条管道里的每一步都有值得说的细节。
飞书粘贴不是简单的文本转换
飞书复制出来的内容是 HTML,但不是普通 HTML。它的剪贴板数据里带有 feishu、larksuite、lark 等特征标记。feishu2wx 会自动检测这些标记,命中时走专门的飞书转换规则。
粘贴检测的逻辑也挺讲究。如果你粘贴的内容来自飞书,或者包含 <table>,或者看起来像“已渲染的 Markdown”(有 h1-h6、blockquote、pre 等标签,但纯文本本身不是 Markdown 源码),就会走 HTML 转 Markdown 的路径。否则就当纯文本处理。
当然,也不是所有场景都希望自动转换。设置面板里有个“智能 HTML 转 Markdown”开关,关掉后粘贴就只保留纯文本。毕竟工具应该听你的,不是替你做决定。
转换基于 Turndown 引擎加 GFM 插件,但飞书有自己的“方言”:
- 高亮标记用
==text==,需要自定义规则转为 Markdown - 代码块的 HTML 结构和标准 Markdown 不完全一样,需要匹配
<div class="code-block">、<div class="highlight">等多种写法 - 表格的 HTML 经常带
colspan,需要正确计算列数并补齐空列
这些都需要特殊适配,不是装个插件就能搞定的。
编辑器带 50 步撤销历史
编辑区不是简单的 <textarea>。它有自定义的撤销/重做系统,保存了 50 步历史记录,每一步都记录了光标位置。Cmd+Z 撤销时,光标会精确回到编辑那个位置,而不是跳到文末。
还支持 Cmd+B 加粗、Cmd+I 斜体、Cmd+U 下划线、Cmd+K 插入链接等快捷键。如果光标处有选中文本,会自动包裹语法;如果文本已经被包裹,会智能解包裹。所有快捷键在底部抽屉面板中一览无余,按 Option+E 可以切换编辑/预览。
编辑器还支持导入本地 .md 文件。如果你习惯在 Cursor、VS Code 里写 Markdown,直接拖进来就行。
图片查看器和设备预览
预览区的图片支持点击放大查看,自动收集文章内所有图片,键盘左右箭头切换浏览,底部显示图片序号(如 2 / 5)。
还支持电脑/手机两种预览宽度切换。手机预览模式下内容以 420px 居中,模拟真实阅读效果。
三级剪贴板回退,只为复制不丢样式
复制到微信公众号这一步,是整个工具最复杂的部分。核心函数 wechatCopy.ts 有 1700 多行。
因为目标环境是微信编辑器,你不能用常规的富文本复制方案。feishu2wx 实现了三级回退策略:
- Clipboard API + ClipboardItem:现代浏览器支持的方案,直接把 HTML 和纯文本同时写入剪贴板
- execCommand + contenteditable div:兼容方案,创建一个临时编辑区域,选中后执行复制命令
- textarea 回退:最终兜底,至少保证纯文本内容能复制出来
还支持智能选区:如果你在预览区选中了部分内容,只复制选中部分;没选中就复制全文。
封面图自动生成
微信公众号要求每篇文章有封面图。如果你没有准备,feishu2wx 会自动生成:用文章第一张图片裁剪成微信要求的 2.35:1 比例。如果文章里没有图片,就用渐变背景加上标题文字生成一张。
推送草稿箱:后端只做代理,不存你的密钥
配置好公众号 AppID 和 AppSecret 后,可以直接把文章推送到微信草稿箱。
推送流程:
浏览器 localStorage(存凭证)
→ POST 请求发送到 Cloudflare Functions
→ Functions 调用微信 API(获取 token → 上传图片 → 创建草稿)
→ 返回结果
这里有一个很重要的设计决策:凭证只存在用户浏览器的 localStorage 里。后端不存储任何凭证,每次推送时随请求发送过来,用完即走。
多用户场景也没问题:每个用户在前端配置自己的凭证,凭证存在各自的浏览器里,互不干扰。
推送过程中,服务端还会自动处理 WebP 图片。静态 WebP 转为 PNG,动态 WebP 转为 GIF(通过 sharp 库逐级缩小尺寸直到低于 1MB)。
排版不是换几个颜色那么简单
feishu2wx 提供了 4 套主题:经典、橙色、蓝色、青绿。但主题系统做的事情远不止换个配色。
每个主题要同步维护三个地方
主题配置分散在三处,修改时必须同步更新:
ThemeSwitcher.tsx:UI 层的主题按钮定义wechatCopy.ts:getThemeStyles()函数,返回各元素的内联样式值styles/themes.css:预览区的 CSS 样式
预览区用的是正常 CSS 类名(因为预览在我们的页面里),复制到微信时全部转换为内联样式(因为微信不认 CSS)。两套渲染必须视觉一致,但实现方式完全不同。
每套主题定义了 9 个颜色值:主色、标题色(H1/H2/H3-H6 各一个)、链接色、引用块边框色和背景色、表头背景色和文字色。这些颜色会贯穿整个排版链路。
16 种字体,都是免费无版权
支持 16 种字体选择,包括系统默认、微软雅黑、宋体、黑体、Arial、Helvetica、Times New Roman、Georgia、Verdana、Courier New,以及 Google Fonts 的 Roboto、Open Sans、Lato、Montserrat、Raleway、Poppins。
所有字体都是免费无版权的,可以放心使用。
细粒度排版控制
除了主题切换,还提供了很多排版开关:
- H1/H2 是否显示底部横线、是否反色显示(主题色背景 + 白字)、居中还是左对齐
- H1 反色使用
box-decoration-break: clone,确保多行标题的背景色连续不断 - 图片默认/边框/阴影三种模式,支持圆角开关
- 代码块经典(浅色)和现代(深色窗口 + 三色圆点头部)两种风格
- 表格阴影、水平分割线
- Task List 渲染:
- [x]显示 ☑,- [ ]显示 ☐,在预览和微信输出中都能正确显示 - 脚注渲染:
[^1]+[^1]: 定义会生成上标引用、分隔线和返回链接,在微信中也有对应的内联样式处理 - 深色模式独立于主题——可以选“青绿主题 + 深色界面”
这些选项看起来琐碎,但每一个都是实际使用中踩过坑的需求。
微信标签兼容基线
微信公众号编辑器没有公开的 HTML 标签白名单。很多标签“有时能用”,但不代表“值得依赖”。
feishu2wx 在项目里明确了一份保守基线:
- 稳定依赖:
p、span、strong、em、ul/ol/li、a、img、section、blockquote、h1-h6、table系列、hr、sup/sub - 不依赖:
figure、figcaption、div、复杂布局标签 - 不支持:
script、style、iframe、video等
对应实现上做了收敛:预览层可以保留更语义化的 figure + figcaption,微信导出层自动降级为 section + img + p.img-caption。不再把“标签语义正确”误认为“公众号里就一定稳定”。
从网页工具到命令行工作流
feishu2wx 最早的形态就是一个网页:打开 → 粘贴 → 预览 → 复制。
但真实使用场景开始往两个方向延伸:
- 有些用户继续在网页里可视化排版
- 有些用户已经在本地编辑器、脚本、CI、批量发布流程中工作,希望同一套排版规则也能在命令行跑
如果能力被锁在浏览器里,这个工具的天花板就很低。
CLI:把排版能力从浏览器里搬出来
v1.19 新增了完整的命令行工具。现在可以直接在终端完成所有操作:
# 初始化配置
feishu2wx init --project
# 配置公众号凭证
feishu2wx auth set --app-id <appid> --app-secret <secret>
# 设置主题和排版参数
feishu2wx theme set blue --show-h1-underline --align-h1-left
# 渲染并预览(本地图片自动内联为 base64)
feishu2wx render article.md --preview
# 直接推送到草稿箱
feishu2wx publish article.md --title "文章标题" --cover cover.jpg
同一套排版能力现在可以跑在三种环境里:浏览器交互、本地终端、自动化脚本。
CLI 还支持从标准输入读取内容,方便管道操作:
cat article.md | feishu2wx render --preview
--copy 选项会把渲染后的 HTML 直接写入系统剪贴板(macOS 用 pbcopy,Windows 用 clip,Linux 用 xclip),不用手动复制文件内容。
CLI 渲染时会在 Node.js 里用 JSDOM 模拟浏览器环境,然后调用 Web 端同一套 renderMarkdown() 和 formatForWeChat() 函数。零逻辑重复。
项目级配置:排版规则也是代码
过去配置是“用户级”的,默认放在 ~/.feishu2wx/config.json。单人用没问题,但多人协作或多项目并行时,不同项目的排版偏好会互相污染。
现在支持项目级配置,放在项目根目录的 .feishu2wx/config.json,自动优先读取项目级配置。
配置读取的优先级很清晰:CLI 参数 > 环境变量 > 项目级配置 > 用户级配置 > 默认值。每一层都能覆盖上一层,但不会互相污染。
配置文件权限默认设为 0o600(仅用户可读写),目录权限 0o700,不会意外泄露凭证。
还支持环境变量覆盖:FEISHU2WX_WECHAT_APP_ID 和 FEISHU2WX_WECHAT_APP_SECRET 可以覆盖配置文件中的值,方便 CI 环境。
这个变化看起来小,但它把工具从“个人偏好面板”推进到了“项目工作流配置”。排版规则可以跟着仓库走,也可以写进 CI 脚本。
不是重写一套 CLI,是复用现有链路
CLI 不是额外写了一堆命令行功能。新的处理路径是:
Markdown / 飞书内容
→ renderMarkdown()
→ formatForWeChat()
→ Web 预览 / 剪贴板 / HTML 文件 / 草稿箱推送
CLI 只是这条管道的另一层入口。因为链路被收得足够清晰,加一层 CLI 入口几乎不需要重复逻辑。
值得注意的是,Web 端和 CLI 端的配置是独立的。你在浏览器里设的主题、字体存在 localStorage 里,CLI 不会读;CLI 的配置存在文件系统里,浏览器也不会碰。两端互不干扰,各自维护自己的偏好。
部署方案:双通道,各取所需
项目采用双通道部署策略:
GitHub Pages(纯前端):每次推送到 main 分支自动部署。只包含排版和复制功能,不需要后端。适合只用排版功能的用户。
Cloudflare Pages(全栈):包含完整的前后端功能。Cloudflare Functions 处理微信 API 的代理请求。两个部署通道共享同一个前端代码。
本地开发支持三种模式:
npm start # 纯前端(端口 3100)
npm run dev # 前端 + Express 后端
npm run cf:dev # 前端 + Cloudflare Functions 本地模拟
技术选型:够用就好
技术栈很简单:
| 类别 | 选型 |
|---|---|
| 前端 | React 18 + TypeScript |
| 构建 | Create React App 5 |
| Markdown 渲染 | markdown-it + markdown-it-footnote |
| HTML 转 Markdown | Turndown + turndown-plugin-gfm |
| 代码高亮 | highlight.js(Atom One Dark) |
| 后端(生产) | Cloudflare Functions |
| 后端(本地) | Express + ts-node |
| 图片处理 | sharp(WebP 归一化) |
| 部署 | GitHub Pages + Cloudflare Pages |
| CLI | Node.js(npm link 全局安装) |
没有用 Vite、Next.js 或 CSS-in-JS 方案。对这个体量的工具应用,CRA + 纯 CSS 已经足够。保持技术栈简单,是刻意的选择。
状态管理也没引入 Redux,所有状态集中在 App.tsx 的 useState 中,20 多个状态全部自动持久化到 localStorage(键名前缀 feishu2wx_)。整个数据流是纯函数式的变换管道,没有副作用链。
写在最后
很多工具一开始都只解决一个局部问题:能不能用,能不能快一点,能不能少复制几次。
feishu2wx 最早也是这样——我只是想把飞书写的东西排版好看一点发到公众号。
但当你开始认真对待一个问题时,它会把你带得更远。排版规则需要可配置、可复用。发布流程需要可自动化、可脚本化。配置需要跟着项目走而不是锁在浏览器里。
最后这个工具就从一个网页小东西,变成了一套可以从浏览器、命令行、CI 脚本三种入口使用的排版工作流。
如果你也在为公众号排版头疼,欢迎试用。
项目地址:github.com/wangruofeng/feishu2wx
觉得有用的话,给个 Star 就更好了。
评论互动