其他

现代化模板渲染

基于 React 与 TailwindCSS 的模板开发方案

本插件采用 React + TailwindCSS 技术栈进行图片渲染,将现代前端的工程化能力引入模板开发。

相比传统模板引擎在维护上的痛点——逻辑与视图耦合、全局 CSS 冲突、缺乏类型检查——本方案通过组件化构建、原子化样式和类型安全三条路径彻底解决了这些问题。

传统方案的维护陷阱

基于 art-template 的模板在项目迭代中往往会陷入维护泥潭: - 逻辑黑洞:业务逻辑混杂在 HTML 模板中,难以阅读和剥离。 - 样式冲突:全局 CSS 类名随时间推移不断堆积,修改一处可能导致多处崩坏。 - 重构风险:缺乏类型检查,修改字段名就像在"排雷",只能祈祷运行时不出错。

架构概览

整个渲染链路分为四层:

  1. 数据层(@kkk/richtext):定义平台无关的富文本文档协议,作为平台层与模板层之间的数据边界。
  2. 平台解析层:将抖音、B 站等平台的原始 API 响应解析为标准的 RichTextDocument JSON。
  3. 模板渲染层:React 组件消费 JSON 数据,TailwindCSS 处理样式,最终通过 SSR 输出静态 HTML。
  4. 输出层: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 图标库,支持任意缩放不失真。