现代化模板渲染
基于 React 与 TailwindCSS 的模板开发方案
本插件采用 React + TailwindCSS 技术栈进行图片渲染,将现代前端的工程化能力引入模板开发。
相比传统模板引擎在维护上的痛点——逻辑与视图耦合、全局 CSS 冲突、缺乏类型检查——本方案通过组件化构建、原子化样式和类型安全三条路径彻底解决了这些问题。
传统方案的维护陷阱
基于 art-template 的模板在项目迭代中往往会陷入维护泥潭: - 逻辑黑洞:业务逻辑混杂在 HTML 模板中,难以阅读和剥离。 -
样式冲突:全局 CSS 类名随时间推移不断堆积,修改一处可能导致多处崩坏。 -
重构风险:缺乏类型检查,修改字段名就像在"排雷",只能祈祷运行时不出错。
架构概览
整个渲染链路分为四层:
- 数据层(
@kkk/richtext):定义平台无关的富文本文档协议,作为平台层与模板层之间的数据边界。 - 平台解析层:将抖音、B 站等平台的原始 API 响应解析为标准的
RichTextDocumentJSON。 - 模板渲染层:React 组件消费 JSON 数据,TailwindCSS 处理样式,最终通过 SSR 输出静态 HTML。
- 输出层:Koishi 宿主侧的 puppeteer 插件打开 HTML 截图,生成图片消息发送到群聊。
富文本文档(RichTextDocument)
@kkk/richtext 是平台层与模板层共用的富文本模块(源码在 src/richtext/),负责定义平台无关的富文本中间表示(IR)。
设计动机
不同平台的文本描述格式差异很大:抖音的富文本是一段带特殊标记的字符串,B 站则提供了结构化的内容节点。如果在模板侧分别解析,会导致平台逻辑泄漏到渲染层,维护成本极高。
富文本文档将这一问题收拢到平台层:平台层负责把各平台的异构数据转换成统一的 JSON 节点树,模板只负责渲染。
节点类型
富文本文档支持两类节点:行内节点与块级节点。
行内节点用于描述段落内的文本片段:
| 节点类型 | 用途 | 示例 |
|---|---|---|
text | 普通文本,支持粗体/斜体/颜色/超链接 | 评论正文 |
emoji | 平台表情包图片 | [doge] |
mention / at | @用户 | @某某 |
searchKeyword | 搜索词高亮 | 带搜索图标的蓝色高亮文本 |
topic | 话题标签 | #某某话题# |
lottery | 抽奖信息 | 带抽奖图标的文本 |
webLink | 网页链接 | 带链接图标的标题 |
vote | 投票 | 带投票图标的标题 |
viewPicture | 查看图片提示 | 带相册图标的提示文本 |
lineBreak | 换行 | <br /> |
块级节点用于描述文档结构:
| 节点类型 | 用途 |
|---|---|
heading | 标题(1-6 级) |
paragraph | 段落 |
image | 图片 |
blockquote | 引用块 |
list / listItem | 有序/无序列表 |
codeBlock | 代码块(带语法高亮和行号) |
linkCard | 链接卡片 |
在平台层中使用
@kkk/richtext 的 parse 模块提供了一系列工厂函数,用于从平台原始数据中构建节点:
import { , , , } from '@kkk/richtext'
const = (
[
([
('这条视频太棒了', { : true }),
('doge', 'https://example.com/doge.png'),
('!推荐大家看看。')
])
],
{ : 'douyin' }
)平台层只需要导入类型和节点创建方法,不依赖 React 运行时,输出的是可序列化的纯 JSON。
在模板层中使用
@kkk/richtext 的 react 模块提供了渲染器,将 RichTextDocument 转成 React 节点:
import { renderRichTextToReact } from '@kkk/richtext/react'
// 在平台模板组件中
const content = renderRichTextToReact(document, {
mention: { className: 'text-blue-500' },
topic: { className: 'text-pink-500' },
searchKeyword: { className: 'text-blue-600 bg-blue-100', iconClassName: 'text-blue-400' }
})
return <div className="text-base leading-relaxed">{content}</div>渲染器会自动处理以下细节:
- 文本转义:React 自动转义文本内容,防止 XSS。
- 图片安全:对
emoji和image节点的src做协议白名单校验(仅允许http://、https://和data:image/*;base64)。 - URL 自动识别:
text节点中的链接会被自动包裹为可点击的超链接。 - 行内样式:支持
bold、italic、strike、color、fontSize、link等行内样式。
为什么不用 dangerouslySetInnerHTML?
平台返回的富文本描述往往包含未经校验的 HTML。使用 renderRichTextToReact 将结构化 JSON 映射为 React 节点,可以避免直接注入 HTML
带来的安全风险,同时让样式完全由 Tailwind 接管。
节点归一化
createRichTextDocument 内部会自动调用 normalizeRichTextNodes,合并相邻的文本节点并丢弃空文本节点。这样平台层在解析时可以按匹配过程简单 push 节点,无需担心碎片过多的问题。
开发工作流
模板源码在 src/ktr/template/<板块>/<模板>/ 下,路由由 src/ktr/registry.ts 维护,用惰性 import() 按需加载;新增或移动模板后,在路由表里补一条即可。
- 渲染自检:
node scripts/smoke-render.cjs用固定数据直接跑模板 SSR,不依赖 puppeteer 与外网,检查路由能否加载、关键内容是否出现在 HTML 里、样式有没有内联进去。 - 数据 Mock:自检脚本里的数据就是可编辑的 mock,改成文本超长、头像缺失之类的边缘情况很方便。
- 实机预览:
node scripts/build.mjs构建后在本地 Koishi 里发一条链接,就能看到真实链路的渲染效果(需要宿主侧启用 puppeteer 插件)。
SSR 渲染引擎
渲染入口是 src/karin/module/utils/Render:按路由从模板注册表取出组件,用 react-dom/server 做 SSR 出 HTML,再交给宿主侧的 puppeteer 插件截图,插件逻辑与模板渲染由此解耦。
入口与初始化:renderTemplateHtml 拿到路由后先确保二维码生成器可用,再从 src/ktr/registry.ts 的路由表里惰性加载模板组件;取不到组件、或模板自带的 validate 校验不通过时,回退到内置通用卡片,保证解析结果始终有图。
SSR 渲染:用 buildPosterContext 组装模板上下文(缩放比例、明暗主题、框架与插件版本、页脚 logo),拼成 React.createElement(template.component, { data, ctx }),再用 renderToPipeableStream 做流式 SSR —— 部分模板会 suspend,流式渲染会等异步内容就绪后再产出完整 HTML,超时则中止并回退。
HTML 包装:把上游构建好的 resources/template/style.css 内联进 <style>,再追加截图专用样式(清掉页面边距与最外层卡片圆角,避免截图出现白边),最后补上 DOCTYPE、meta 与 body 的主题类名,写入 HTML 文件。
puppeteer 截图:通过 ctx.puppeteer 打开该 HTML,视口宽度按模板的设计宽度 1440、高度量测后再设,deviceScaleFactor 由 app.renderScale 决定(下限 2x、上限 3x),超大页面自动降一档缩放,最后输出图片消息到群聊。
性能优化
SSR 直接输出包含完整样式与内容的 HTML,puppeteer 插件打开页面后无需等待 JavaScript 加载与执行即可立即截图,显著降低了渲染耗时与内存占用。
生态复用
得益于 React 标准,可以直接引入成熟的库来丰富静态画面的表现力:
- 排版布局:引入现代化的 UI 组件库,快速构建精美的卡片、列表。
- 数据可视化:使用专业图表库将复杂数据渲染为统计图表。
- 矢量图标:引入海量 SVG 图标库,支持任意缩放不失真。