进阶指南
插件开发
RenderPlugin 接口详解、beforeRender / afterRender 钩子使用、插件执行顺序与实用插件示例
渲染插件是 SSR 阶段的可插拔扩展,允许在渲染前后插入自定义逻辑——数据捕获、HTML 加工、性能监控、资源注入等。
RenderPlugin 接口
interface RenderPlugin {
/** 插件名称,用于日志和错误提示 */
: string
/** 执行顺序:pre(前置)→ normal(普通)→ post(后置),默认 normal */
?: 'pre' | 'normal' | 'post'
/** 过滤器:返回 false 时跳过当前模板,不执行钩子 */
?: (: string) => boolean
/** 渲染前钩子:在 React SSR 之前执行 */
?: (: PluginContext) => void | <void>
/** 渲染后钩子:在模板片段生成后、HtmlWrapper 包装前执行,拿到的是组件片段而非完整文档,可以加工片段 HTML */
?: (: PluginContext & { : string }) => string | void | <string | void>
}插件执行顺序
enforce 字段
控制插件执行顺序,同级插件按注册顺序执行:
import type { RenderPlugin } from '@karinjs/template-react'
const : RenderPlugin = {
: 'pre-plugin',
: 'pre', // 最先执行
: () => .('1. pre beforeRender')
}
const : RenderPlugin = {
: 'normal-plugin',
// enforce: 'normal' 是默认值,可省略
: () => .('2. normal beforeRender')
}
const : RenderPlugin = {
: 'post-plugin',
: 'post', // 最后执行
: () => .('3. post beforeRender')
}执行顺序规则:
- beforeRender:
pre→normal→post - SSR 渲染:生成模板片段(组件渲染出的 HTML,不含
<html>/<head>/<body>) - afterRender:
pre→normal→post,串行加工片段 - HtmlWrapper 包装:片段包装成完整文档(注入 CSS、headExtra、改写
/assets引用)后写盘
内部排序机制
// packages/core/src/runtime/plugins.ts
const order = {
pre: -1,
normal: 0,
post: 1
}
// 插件按 enforce 排序
plugins.sort((a, b) => order[a.enforce ?? 'normal'] - order[b.enforce ?? 'normal'])apply 过滤器
按路由过滤插件执行:
const : RenderPlugin = {
: 'user-only',
: () => .('user/'), // 只对 user/* 生效
: () => {
.(`[user-only] 渲染 ${.}`)
}
}
const : RenderPlugin = {
: 'exclude-draft',
: () => !.('draft'), // 排除 draft 模板
: () => {
// afterRender 拿到的是组件片段(没有 </body>),直接追加节点即可
return . + '<div class="watermark">正式版</div>'
}
}高级过滤:
const : RenderPlugin = {
: 'conditional',
: () => {
// 正则匹配
if (/^(user|admin)\//.()) return true
// 环境变量控制
if (.. === 'development' && .('test')) return true
// 白名单
const = ['hello/card', 'user/profile']
return .()
},
: () => {
/* ... */
}
}beforeRender 钩子
在 React SSR 之前执行,可以检查数据、记录日志、修改上下文。
PluginContext
interface PluginContext {
/** 模板路由(如 'hello/card') */
: string
/** 当前渲染的数据 */
: unknown
/** 运行时上下文(scale、theme) */
: RenderContext
/** HTML 输出目录 */
: string
}基础示例
const : RenderPlugin = {
: 'log-plugin',
: async () => {
.(`[渲染前] 路由: ${.}`)
.(`[渲染前] 数据:`, .)
.(`[渲染前] 主题:`, ..)
}
}数据校验插件
const : RenderPlugin = {
: 'validate-data',
: () => {
if (!. || typeof . !== 'object') {
throw new (`[${.}] 数据无效:必须是对象`)
}
// 检查必填字段
const = . as <string, unknown>
if (!.) {
.(`[${.}] 缺少 title 字段`)
}
}
}性能监控插件
import type { RenderPlugin } from '@karinjs/template-react'
const = new <string, number>()
const : RenderPlugin = {
: 'perf-monitor',
: () => {
.(., .())
},
: () => {
const = .(.)
if () {
const = .() -
.(`[性能] ${.} 渲染耗时: ${}ms`)
.(.)
}
}
}afterRender 钩子
在模板片段生成后、HtmlWrapper 包装成完整文档前执行。ctx.html 是组件渲染出的片段——没有 <html>、<head>、<body> 标签,对它们做 replace 会空转。可以加工片段、包裹或追加节点、上传内容;要往 <head> 注入内容请改用 html.headExtra 配置。
返回值规则
- 返回
string:替换原 HTML - 返回
void或undefined:保持原 HTML 不变 - 返回
Promise:支持异步操作
const : RenderPlugin = {
: 'transform',
: async () => {
// 读取原 HTML
const { } =
// 加工
let = .('{{VERSION}}', '1.0.0')
// 返回新 HTML
return
}
}HTML 压缩插件
import type { RenderPlugin } from '@karinjs/template-react'
import { } from 'html-minifier-terser'
const : RenderPlugin = {
: 'html-minify',
: 'post', // 最后执行
: async () => {
return await (., {
: true,
: true,
: true,
: true
})
}
}注入 meta 标签
meta 标签属于 <head>,而 afterRender 拿到的片段里没有 <head>,这类注入走 html.headExtra 配置:
export default ({
: {
: `
<meta name="generator" content="ktr">
<meta name="template" content="user/card">
`
}
})水印插件
const : RenderPlugin = {
: 'watermark',
: () => !.('official'), // 非正式版才加水印
: () => {
const = `
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) rotate(-45deg); font-size: 72px; color: rgba(0,0,0,0.05); pointer-events: none; user-select: none; z-index: 9999;">
DRAFT
</div>
`
// 片段没有 </body>,直接追加节点(外壳 #container 是 position: relative 定位包含块)
return . +
}
}资源内联插件
import type { RenderPlugin } from '@karinjs/template-react'
import from 'node:fs'
import from 'node:path'
const : RenderPlugin = {
: 'inline-assets',
: async () => {
// 注意时机:afterRender 在 HtmlWrapper 包装前执行,此时 /assets 引用还是模板原始写法
// (框架的 base64/file:// 改写发生在之后的包装阶段),所以这里能匹配到原始路径
let = .
// 内联小图片(< 10KB)
const = /<img[^>]+src="\/assets\/([^"]+)"[^>]*>/g
= .(, (, ) => {
const = .(., '../assets', )
if (!.()) return
const = .()
if (. > 10240) return // 超过 10KB 不内联
const = .().(1)
const = .(, 'base64')
return .(`src="/assets/${}"`, `src="data:image/${};base64,${}"`)
})
return
}
}实用插件示例
1. 数据捕获插件
捕获真实渲染数据到 captured.json:
import type { RenderPlugin } from '@karinjs/template-react'
import from 'node:fs'
import from 'node:path'
const : RenderPlugin = {
: 'capture-data',
: 'post',
: async () => {
const = .(., '../template', .)
const = .(, 'data')
const = .(, 'captured.json')
// 确保目录存在
await ..(, { : true })
// 写入捕获数据
await ..(, .({ : ., : . }, null, 2))
}
}2. 错误上报插件
import type { RenderPlugin } from '@karinjs/template-react'
const : RenderPlugin = {
: 'error-report',
: async () => {
try {
// 数据校验
if (!.) throw new ('数据为空')
} catch () {
// 上报到监控平台
await ('https://monitor.example.com/report', {
: 'POST',
: .({
: .,
: ( as Error).,
: .()
})
})
throw // 重新抛出
}
}
}3. 缓存插件
import type { RenderPlugin } from '@karinjs/template-react'
import from 'node:fs'
import from 'node:path'
import from 'node:crypto'
const : RenderPlugin = {
: 'cache',
: 'pre',
: async () => {
const = .(., '.cache')
await ..(, { : true })
// 计算数据哈希
const = .('md5').(.(.)).('hex')
const = .(, `${..('/', '_')}_${}.html`)
// 检查缓存
if (.()) {
.(`[缓存] 命中: ${.}`)
// 跳过渲染(需要框架支持,此处仅示意)
}
}
}4. OG 图片标签
og 标签属于 <head>,afterRender 的片段里没有 <head>,这类注入走 html.headExtra 配置:
export default ({
: {
: `
<meta property="og:title" content="模板预览">
<meta property="og:type" content="website">
<meta property="og:image" content="/preview/user_card.png">
`
}
})5. 多语言插件
import type { RenderPlugin } from '@karinjs/template-react'
const : RenderPlugin = {
: 'i18n',
: () => {
const = (. as any).lang || 'zh-CN'
// 片段里没有 <html> 标签,用一层带 lang 的包裹 div 代替
return `<div lang="${}">${.}</div>`
}
}6. 响应式图片插件
const : RenderPlugin = {
: 'responsive-image',
: () => {
// 将 <img> 转换为 <picture>
return ..(
/<img src="([^"]+)" alt="([^"]*)"/g,
(, , ) => `
<picture>
<source srcset="${}?w=640" media="(max-width: 640px)">
<source srcset="${}?w=1280" media="(max-width: 1280px)">
<img src="${}" alt="${}"
</picture>
`
)
}
}7. CSP 安全策略
CSP 是 <head> 里的 meta 标签,afterRender 的片段够不到 <head>,走 html.headExtra 配置:
export default ({
: {
: `
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'unsafe-inline'; img-src 'self' data:">
`
}
})8. 统计插件
import type { RenderPlugin } from '@karinjs/template-react'
import from 'node:fs'
import from 'node:path'
const : RenderPlugin = {
: 'stats',
: 'post',
: async () => {
const = .(., 'stats.json')
let : <string, any> = {}
if (.()) {
= .(.(, 'utf-8'))
}
[.] = {
: ..,
: .(),
: ..?. || 'default'
}
await ..(, .(, null, 2))
}
}插件注册
配置文件注册
import { } from '@karinjs/template-react'
import type { RenderPlugin } from '@karinjs/template-react'
const : RenderPlugin = {
: 'my-plugin',
: () => {
/* ... */
}
}
export default ({
// 通过 vite.plugins 无效,需要通过运行时注册
})运行时注册
const = (, {
,
,
: []
})Karin 插件集成
import { } from '@karinjs/template-react'
import type { RenderPlugin } from '@karinjs/template-react'
const : RenderPlugin = {
: 'watermark',
// afterRender 拿到的是组件片段(没有 </body>),直接追加节点
: () => . + '<div class="watermark">My Bot</div>'
}
const = (import.meta., {
: { : [] }
})
export default 插件最佳实践
1. 单一职责
每个插件只做一件事:
// ✅ 好:职责清晰
const logPlugin = { name: 'log', beforeRender: log }
const minifyPlugin = { name: 'minify', afterRender: minify }
// ❌ 差:职责混杂
const megaPlugin = {
beforeRender: (ctx) => {
log()
validate()
track()
}
}2. 错误处理
插件失败不应影响其他插件:
const : RenderPlugin = {
: 'safe',
: async () => {
try {
// 可能失败的操作
await ()
} catch () {
.(`[${.}] 插件失败:`, )
// 不重新抛出,避免中断渲染
}
}
}3. 性能优先
避免阻塞操作:
const : RenderPlugin = {
: 'async',
: async () => {
// ✅ 好:异步上传,不阻塞返回
(.).(.)
return . // 立即返回
}
}4. 可配置性
插件应接受配置:
import type { RenderPlugin } from '@karinjs/template-react'
function (: string, = 0.05): RenderPlugin {
return {
: 'watermark',
: () => {
// 片段没有 </body>,直接追加节点
const = `<div style="opacity: ${}">${}</div>`
return . +
}
}
}
// 使用
const = ('DRAFT', 0.1)5. 条件执行
使用 apply 而非内部判断:
// ✅ 好:声明式过滤
const : RenderPlugin = {
: 'prod-only',
: () => .. === 'production',
:
}
// ❌ 差:命令式判断
const : RenderPlugin = {
: 'prod-only',
: () => {
if (.. !== 'production') return
return ()
}
}
declare function (: any): any6. 类型安全
为数据定义类型:
import type { RenderPlugin, PluginContext } from '@karinjs/template-react'
interface UserCardData {
: string
: string
}
function (: unknown): is UserCardData {
return typeof === 'object' && !== null && 'name' in
}
const : RenderPlugin = {
: 'typed',
: () => === 'user/card',
: () => {
if (!(.)) {
throw new ('数据类型错误')
}
// 此处 ctx.data 类型已缩窄
.(..)
}
}