公众号排版太痛苦?我写了个工具,从飞书到草稿箱一条龙

发布于 2026年06月06日 22:30 #开源

公众号排版太痛苦?我写了个工具,从飞书到草稿箱一条龙 封面图

大家好,我是若风。

写公众号这半年,排版一直是我最烦的环节。

飞书写完文章,复制到微信编辑器,代码块没了样式,表格挤成一团,引用块变成普通文字,图片要一张张手动上传。一篇技术文章,排版比写还久。

试过几个排版工具,要么只能处理纯 Markdown,要么不支持代码高亮,要么生成的东西一到微信编辑器就变形。

后来我自己写了一个:feishu2wx

项目地址:github.com/wangruofeng/feishu2wx

今天聊聊这个工具做了什么、怎么做的、以及它最近从一个“网页小工具”变成了什么。

微信编辑器是前端工程的噩梦

微信公众号编辑器对 HTML 的限制,远比大多数人想象的严格。我列举一下实际踩过的坑。

CSS 类名全部失效,所有样式必须内联

这是最基本的约束。你不能写一个 .code-block 类然后复用,你得给每一行代码、每一个关键字、每一个背景色都写完整的 style="..." 属性。

feishu2wx 的核心渲染函数 formatForWeChat() 有 1700 多行,其中大量逻辑就是遍历 DOM 树,为每个元素注入完整的内联样式。

只认 px,其他单位全部吞掉

emremvh 在微信编辑器里直接消失。所以所有尺寸都要显式转换为像素。

列表结构会被重新排列

微信编辑器会重排 <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。它的剪贴板数据里带有 feishularksuitelark 等特征标记。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 实现了三级回退策略:

  1. Clipboard API + ClipboardItem:现代浏览器支持的方案,直接把 HTML 和纯文本同时写入剪贴板
  2. execCommand + contenteditable div:兼容方案,创建一个临时编辑区域,选中后执行复制命令
  3. textarea 回退:最终兜底,至少保证纯文本内容能复制出来

还支持智能选区:如果你在预览区选中了部分内容,只复制选中部分;没选中就复制全文。

封面图自动生成

微信公众号要求每篇文章有封面图。如果你没有准备,feishu2wx 会自动生成:用文章第一张图片裁剪成微信要求的 2.35:1 比例。如果文章里没有图片,就用渐变背景加上标题文字生成一张。

推送草稿箱:后端只做代理,不存你的密钥

配置好公众号 AppID 和 AppSecret 后,可以直接把文章推送到微信草稿箱。

推送流程:

浏览器 localStorage(存凭证)
  → POST 请求发送到 Cloudflare Functions
  → Functions 调用微信 API(获取 token → 上传图片 → 创建草稿)
  → 返回结果

这里有一个很重要的设计决策:凭证只存在用户浏览器的 localStorage 里。后端不存储任何凭证,每次推送时随请求发送过来,用完即走。

多用户场景也没问题:每个用户在前端配置自己的凭证,凭证存在各自的浏览器里,互不干扰。

推送过程中,服务端还会自动处理 WebP 图片。静态 WebP 转为 PNG,动态 WebP 转为 GIF(通过 sharp 库逐级缩小尺寸直到低于 1MB)。

排版不是换几个颜色那么简单

feishu2wx 提供了 4 套主题:经典、橙色、蓝色、青绿。但主题系统做的事情远不止换个配色。

每个主题要同步维护三个地方

主题配置分散在三处,修改时必须同步更新:

  1. ThemeSwitcher.tsx:UI 层的主题按钮定义
  2. wechatCopy.tsgetThemeStyles() 函数,返回各元素的内联样式值
  3. 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 在项目里明确了一份保守基线:

  • 稳定依赖pspanstrongemul/ol/liaimgsectionblockquoteh1-h6table 系列、hrsup/sub
  • 不依赖figurefigcaptiondiv、复杂布局标签
  • 不支持scriptstyleiframevideo

对应实现上做了收敛:预览层可以保留更语义化的 figure + figcaption,微信导出层自动降级为 section + img + p.img-caption。不再把“标签语义正确”误认为“公众号里就一定稳定”。

从网页工具到命令行工作流

feishu2wx 最早的形态就是一个网页:打开 → 粘贴 → 预览 → 复制。

但真实使用场景开始往两个方向延伸:

  1. 有些用户继续在网页里可视化排版
  2. 有些用户已经在本地编辑器、脚本、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_IDFEISHU2WX_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 转 MarkdownTurndown + turndown-plugin-gfm
代码高亮highlight.js(Atom One Dark)
后端(生产)Cloudflare Functions
后端(本地)Express + ts-node
图片处理sharp(WebP 归一化)
部署GitHub Pages + Cloudflare Pages
CLINode.js(npm link 全局安装)

没有用 Vite、Next.js 或 CSS-in-JS 方案。对这个体量的工具应用,CRA + 纯 CSS 已经足够。保持技术栈简单,是刻意的选择。

状态管理也没引入 Redux,所有状态集中在 App.tsxuseState 中,20 多个状态全部自动持久化到 localStorage(键名前缀 feishu2wx_)。整个数据流是纯函数式的变换管道,没有副作用链。

写在最后

很多工具一开始都只解决一个局部问题:能不能用,能不能快一点,能不能少复制几次。

feishu2wx 最早也是这样——我只是想把飞书写的东西排版好看一点发到公众号。

但当你开始认真对待一个问题时,它会把你带得更远。排版规则需要可配置、可复用。发布流程需要可自动化、可脚本化。配置需要跟着项目走而不是锁在浏览器里。

最后这个工具就从一个网页小东西,变成了一套可以从浏览器、命令行、CI 脚本三种入口使用的排版工作流。

如果你也在为公众号排版头疼,欢迎试用。

项目地址:github.com/wangruofeng/feishu2wx

觉得有用的话,给个 Star 就更好了。

评论互动

© 2026 王若风的技术博客 · Powered by Astro