插件 API
RenderPlugin 接口、beforeRender / afterRender 钩子、PluginContext 与自定义插件示例
渲染插件是 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 字段
控制插件执行顺序:
'pre':最先执行,通常用于前置检查、环境准备。'normal':默认顺序,大多数插件用这个。'post':最后执行,通常用于清理、日志输出。
同级插件按注册顺序执行。
apply 字段
过滤器函数,接收模板路由(如 'hello/card'),返回 boolean:
true或不提供:执行钩子。false:跳过该模板,不执行beforeRender和afterRender。
const : RenderPlugin = {
: 'user-only',
: () => .('user/'), // 只对 user/ 开头的路由生效
: () => {
.(`[user-only] 渲染 ${.}`)
}
}beforeRender 钩子
在 React SSR 之前执行,可以:
- 检查数据有效性(除了
validate之外的业务逻辑校验) - 修改输出目录或上下文
- 记录日志、发送监控
const : RenderPlugin = {
: 'log-plugin',
: async () => {
.(`[渲染前] 路由: ${.}`)
.(`[渲染前] 数据:`, .)
.(`[渲染前] 上下文:`, .)
}
}不要修改 ctx.data:引用类型修改会影响后续插件和渲染逻辑,容易产生副作用。若需加工数据,应在调用渲染函数之前处理。
afterRender 钩子
在 React SSR 完成之后执行,ctx.html 是 HtmlWrapper 包装前的模板 HTML 片段(片段随后才被包进完整 HTML 文档),可以:
- 加工片段(包裹/追加节点、替换占位符、压缩等)
- 记录渲染结果
- 上传 HTML 到远程存储
const : RenderPlugin = {
: 'watermark',
: async () => {
// 给模板片段追加一个角标节点
return `${.}<div class="watermark">generated by ktr</div>`
}
}片段里没有 <head> / <body>,不要对 ctx.html 做 replace('</head>', ...) 这类整文档操作。需要往 <head> 注入 <meta>、<link> 等内容时,使用 html.headExtra 配置(见 配置与 CLI)。
返回值规则:
- 返回
string:替换原 HTML。 - 返回
void或undefined:保持原 HTML 不变。 - 返回 Promise:支持异步操作。
PluginContext
interface PluginContext {
/** 模板路由(如 'hello/card')。 */
: string
/** 当前渲染的数据。 */
: unknown
/** 运行时上下文(scale、theme)。 */
: RenderContext
/** HTML 输出目录。 */
: string
}beforeRender 和 afterRender 钩子接收的上下文对象,afterRender 额外有 html: string 字段。
字段说明
Prop
Type
afterRender 的上下文额外带有 html: string 字段(生成的 HTML 字符串,可修改后返回)。
使用示例
import type { RenderPlugin } from '@karinjs/template-react'
import from 'node:fs'
import from 'node:path'
const : RenderPlugin = {
: 'save-metadata',
: async () => {
const = .(., `${..('/', '_')}_meta.json`)
await ..(, .({ : ., : .., : .() }, null, 2))
}
}注册插件
通过 createRenderer 的 plugins 选项注册:
const = (, {
,
,
: [, ]
})createTemplateRenderer 也可以通过 renderer.plugins 传入:
const = (import.meta., {
: {
: []
}
})自定义插件示例
以下是几个常见的自定义插件写法,可以直接复制到插件里使用。
捕获数据插件
框架渲染器已内置数据捕获(captureDir 选项),这里演示如何用插件实现等效能力:
import type { RenderPlugin } from '@karinjs/template-react'
import from 'node:fs'
import from 'node:path'
function (: string): RenderPlugin {
return {
: 'capture-data',
: 'post', // 最后执行,确保渲染成功
: async () => {
const [, ] = ..('/')
const = .(, , , 'data')
await ..(, { : true })
const = .(, 'captured.json')
await ..(, .(., null, 2))
.(`[capture] 已捕获数据到 ${}`)
}
}
}HTML 压缩插件
import type { RenderPlugin } from '@karinjs/template-react'
function (): RenderPlugin {
return {
: 'minify-html',
: 'post',
: () => {
// 简单压缩:移除多余空白符和换行
return .
.(/\s+/g, ' ') // 多个空白符合并为一个
.(/>\s+</g, '><') // 标签间空白移除
.()
}
}
}生产环境推荐使用专业的 HTML 压缩库(如 html-minifier-terser),上面的简单正则可能破坏 <pre> / <code> 等标签内容。
性能监控插件
import type { RenderPlugin } from '@karinjs/template-react'
function (): RenderPlugin {
const = new <string, number>()
return {
: 'perf-monitor',
: () => {
.(., .())
},
: () => {
const = .(.)
if () {
const = .() -
.(`[性能] ${.} 渲染耗时: ${}ms`)
.(.)
}
}
}
}条件渲染插件
import type { RenderPlugin } from '@karinjs/template-react'
function (): RenderPlugin {
return {
: 'conditional-render',
: () => {
// 仅在开发环境渲染 dev/ 开头的模板
if (.('dev/')) {
return .. === 'development'
}
return true
},
: () => {
.(`[条件渲染] 允许渲染 ${.}`)
}
}
}异常处理
插件内抛出的异常会中断渲染流程,返回 { success: false, error: '...' }。渲染器不对错误信息做包装,error 就是异常的原始 message:
import type { RenderPlugin } from '@karinjs/template-react'
const : RenderPlugin = {
: 'unsafe-plugin',
: () => {
if (!.) {
throw new ('数据为空') // 渲染失败,error 为 '数据为空'
}
}
}若插件异常不应中断渲染,需在插件内 try-catch:
import type { RenderPlugin } from '@karinjs/template-react'
const : RenderPlugin = {
: 'safe-plugin',
: async () => {
try {
// 可能失败的操作
await ('https://api.example.com/log', {
: 'POST',
: .({ : . })
})
} catch () {
.('[safe-plugin] 上报失败,但不影响渲染:', )
}
}
}插件最佳实践
1. 命名清晰
插件 name 用于日志和错误提示,应清晰表达功能:
// ✅ 好的命名
{
name: 'inject-analytics'
}
{
name: 'compress-html'
}
{
name: 'upload-to-oss'
}
// ❌ 不好的命名
{
name: 'plugin1'
}
{
name: 'my-plugin'
}2. 避免副作用
不要修改 ctx.data 或 ctx.ctx:
import type { RenderPlugin } from '@karinjs/template-react'
// ❌ 错误:修改上下文
const : RenderPlugin = {
: 'bad-plugin',
: () => {
;(. as any).extra = 'injected' // 影响后续插件和渲染
}
}
// ✅ 正确:只读取,不修改
const : RenderPlugin = {
: 'good-plugin',
: () => {
const = 'title' in (. as any)
.(`数据包含 title: ${}`)
}
}3. 合理使用 enforce
- 前置检查、环境准备 →
enforce: 'pre' - 核心逻辑 →
enforce: 'normal'(默认) - 清理、日志、上传 →
enforce: 'post'
import type { RenderPlugin } from '@karinjs/template-react'
const : RenderPlugin = {
: 'check-env',
: 'pre', // 最先执行
: () => {
if (!..) {
throw new ('缺少 API_KEY 环境变量')
}
}
}
const : RenderPlugin = {
: 'upload-html',
: 'post', // 最后执行
: async () => {
// 上传 HTML 到 OSS
}
}4. 过滤不需要的路由
用 apply 减少不必要的执行:
import type { RenderPlugin } from '@karinjs/template-react'
const : RenderPlugin = {
: 'expensive-operation',
: () => .('report/'), // 只对 report/ 生效
: async () => {
// 耗时操作
}
}5. 异步操作用 async
钩子支持 Promise,耗时操作应用 async:
import type { RenderPlugin } from '@karinjs/template-react'
const : RenderPlugin = {
: 'async-plugin',
: async () => {
// 等待异步操作完成
await new (() => (, 100))
return `${.}<div data-loaded="true"></div>`
}
}6. 保持插件轻量
插件在每次渲染时都会执行,避免:
- 重复读取文件(在插件初始化时缓存)
- 同步阻塞操作(用
async+ 异步 API) - 复杂正则替换(HTML 加工尽量简单)
import type { RenderPlugin } from '@karinjs/template-react'
import from 'node:fs'
// ❌ 错误:每次渲染都读文件
const : RenderPlugin = {
: 'slow-plugin',
: () => {
const = .('/path/to/template.html', 'utf-8') // 慢
return ..('{{content}}', )
}
}
// ✅ 正确:插件初始化时缓存
function (): RenderPlugin {
const = .('/path/to/template.html', 'utf-8') // 只读一次
return {
: 'fast-plugin',
: () => ..('{{content}}', )
}
}调试插件
开发插件时可以打印 PluginContext 查看字段内容:
import type { RenderPlugin } from '@karinjs/template-react'
const : RenderPlugin = {
: 'debug-plugin',
: () => {
.('[DEBUG] PluginContext:', .(, null, 2))
},
: () => {
.('[DEBUG] HTML length:', ..)
.('[DEBUG] HTML preview:', ..(0, 200))
}
}渲染时添加该插件,查看日志输出即可了解插件接收到的完整上下文。