进阶指南

插件开发

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')
}

执行顺序规则

  1. beforeRenderprenormalpost
  2. SSR 渲染:生成模板片段(组件渲染出的 HTML,不含 <html>/<head>/<body>
  3. afterRenderprenormalpost,串行加工片段
  4. 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
  • 返回 voidundefined:保持原 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 配置:

karin.template.ts
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 配置:

karin.template.ts
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 配置:

karin.template.ts
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))
  }
}

插件注册

配置文件注册

karin.template.ts
import {  } from '@karinjs/template-react'
import type { RenderPlugin } from '@karinjs/template-react'

const : RenderPlugin = {
  : 'my-plugin',
  : () => {
    /* ... */
  }
}

export default ({
  // 通过 vite.plugins 无效,需要通过运行时注册
})

运行时注册

const  = (, {
  ,
  ,
  : []
})

Karin 插件集成

src/utils/render.ts
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): any

6. 类型安全

为数据定义类型:

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 类型已缩窄
    .(..)
  }
}

On this page