插件开发指南

更新于 2026-11-08 15:33:56 27 次浏览

本指南面向 BlogKit 插件开发者,介绍插件框架的能力、目录规范、清单字段与各开发专题。模板标签语法请同时参考 主题开发指南,开放 API 相关能力参考 API 文档。

一、插件能做什么

BlogKit 插件框架(2.0)提供以下能力,全部通过继承 PluginBase 基类获得:

  • 钩子监听:订阅系统在业务流程中触发的钩子(文章发布、评论提交、文件上传等),在不修改核心代码的前提下介入业务逻辑;
  • 挂载点注入:向后台模板的固定埋点(如后台 <head>、仪表盘快捷入口卡)输出自定义 HTML/CSS/JS;
  • 前台路由接管:在 plugin.json 中声明 routes,自定义 URL 由插件控制器响应,支持 {param} 路径参数;
  • 后台页面:声明 admin_routes 注册自有后台页面,访问地址为 admin.php?action=plugin.{slug}.{page},从「插件管理 → 插件详情 → 配置」进入;
  • 自有模板渲染:插件目录内 templates/ 下的模板由核心模板引擎渲染,可用全部模板标签;
  • 设置项:在 plugin.json 中声明 default_settings,系统自动生成配置表单并持久化;
  • 系统服务访问:数据库、缓存、日志、配置、路由等核心服务可直接获取;
  • 插件间通信:调用其他插件的公开方法、向其他插件派发事件。

插件不占用后台侧边栏菜单:所有插件(含官方插件)的管理页统一从「插件管理 → 插件详情 → 配置」进入。

二、插件目录结构

一个插件是 plugins/ 下的一个独立目录,目录名必须与 slug 一致:

plugins/hello/
├── plugin.json          # 插件清单(必需):名称、版本、路由、设置项声明
├── plugin.php           # 入口文件(必需):负责引入主类文件
├── HelloPlugin.php      # 插件主类(必需):类名为 PascalCase(slug) + Plugin
├── Admin/               # 后台控制器目录(按需)
│   └── HelloAdminController.php
├── Controllers/         # 前台路由控制器目录(按需)
├── templates/           # 插件模板目录(按需),由 Plugin::view 渲染
├── assets/              # 静态资源(CSS/JS,按需)
├── icon.png             # 插件图标
├── thumbnail.png        # 插件详情页缩略图
└── README.md            # 说明文档(建议提供)

主类命名规则:slug 按下划线分段、每段首字母大写后拼接,再加 Plugin 后缀。例如 hello_world 对应类名 HelloWorldPlugin;系统也兼容无 Plugin 后缀的类名。入口文件 plugin.php 只做一件事——引入主类文件。

附:PluginBase 基类方法速查

插件主类继承 PluginBase 后可直接使用以下方法(均为源码核实的方法签名):

方法签名用途
initinit()初始化入口,每次请求加载插件时执行,子类重写用于注册钩子
getInfogetInfo()返回传入构造函数的插件信息数组
getManifestgetManifest()返回 plugin.json 解析结果(带内存缓存)
addHookaddHook($hook, callable $callback, $priority = 10)注册钩子/挂载点订阅,优先级数值越小越先执行
adminUrladminUrl($page)生成插件后台页 URL:admin.php?action=plugin.{slug}.{page}
viewview($tpl, $data = [])渲染插件 templates/ 目录下模板,$data 注入为模板变量
installinstall()生命周期:安装时调用
activateactivate()生命周期:启用时调用
deactivatedeactivate()生命周期:停用时调用
uninstalluninstall($keepData = false)生命周期:卸载时调用,$keepData 指示是否保留数据
callPlugincallPlugin($slug, $method, ...$args)调用其他已启用插件的公开方法
triggerPluginEventtriggerPluginEvent($slug, $event, $data = null)触发目标插件的 on{事件名} 方法
getPlugingetPlugin($slug)获取指定已启用插件实例
getActivePluginsgetActivePlugins()获取全部已启用插件
getServicegetService($serviceName)获取系统服务(database/config/log/cache/router/template/plugin)
getDatabasegetDatabase()快捷获取数据库服务
getConfig / getLog / getCache / getRouter / getTemplate / getPluginService同上形式快捷获取对应系统服务
loglog($level, $message, $context = [])按级别记录日志
debug / info / warning / error / critical($message, $context = [])各级别日志便捷方法

三、plugin.json 字段全表

字段类型必填说明
namestring是插件名称,2-50 个字符
slugstring是插件标识,仅小写字母/数字/下划线/连字符,2-50 字符,必须与目录名一致
versionstring是插件版本,SemVer 格式(如 1.0.0)
descriptionstring是插件描述,建议不超过 500 字符
authorstring是作者名称
author_urlstring否作者主页 URL(需为合法 URL)
compatible_versionstring否声明兼容的主系统版本,SemVer 写法,^2.0.0 表示兼容 2.0.0 及以上、同一主版本内的系统
dependenciesarray/object否依赖的其他插件;依赖项可带 version 最低版本与 optional(可选依赖),安装时系统会校验
settings_pageboolean否是否有配置页;声明为 true 时详情页出现「配置」入口
iconstring否图标文件名(相对插件目录,如 icon.png)
thumbnailstring否缩略图文件名(相对插件目录)
default_settingsobject否设置项声明,详见「设置项开发」一节
admin_routesobject否后台页面路由表,键为 plugin.{slug}.{page},值为 控制器文件@方法名
routesarray否前台路由接管列表,每项含 path/handler/method
plugin_urlstring否插件主页 URL
changelogarray否更新记录,每条含 version/date

四、快速开始:Hello 插件

下面完成一个最小可运行的插件:监听文章内容渲染钩子、向后台 admin.head 挂载点注入内容、提供一个设置项。

1. plugins/hello/plugin.json

{
    "name": "Hello",
    "slug": "hello",
    "version": "1.0.0",
    "settings_page": true,
    "description": "最小示例插件:演示钩子监听、挂载点注入与设置项读写",
    "author": "Your Name",
    "author_url": "https://example.com",
    "compatible_version": "^2.0.0",
    "dependencies": [],
    "icon": "icon.png",
    "thumbnail": "thumbnail.png",
    "default_settings": {
        "signature": {
            "type": "text",
            "label": "文章尾部签名",
            "value": "本文由 Hello 插件提供",
            "help": "追加在每篇文章正文末尾的签名文字,留空则不追加"
        }
    }
}

2. plugins/hello/plugin.php(入口文件)

<?php
// 入口文件:插件系统根据 slug 加载本文件,并实例化主类
// 类名规则:PascalCase(slug) + 'Plugin',即 hello -> HelloPlugin
require_once __DIR__ . '/HelloPlugin.php';

3. plugins/hello/HelloPlugin.php(主类)

<?php
// 插件主类必须继承 PluginBase,才能获得钩子注册、服务访问等能力
class HelloPlugin extends PluginBase {

    // init() 在每次请求加载插件时执行(仅启用状态的插件会被加载),
    // 是注册钩子与挂载点订阅的唯一时机
    public function init() {
        // 注册钩子:文章正文渲染过滤器,回调返回处理后的内容
        $this->addHook('article_content_render', [$this, 'appendSignature']);

        // 订阅后台挂载点:admin.head 在后台所有页面 </head> 前执行,
        // 回调返回的字符串会被直接输出到页面(也可在回调中直接 echo)
        $this->addHook('admin.head', [$this, 'injectHead']);
    }

    // 钩子回调:$content 为文章正文 HTML,必须返回处理后的内容
    public function appendSignature($content) {
        // 读取本插件设置:getPluginConfig 返回「默认值 + 已保存值」合并结果
        $settings = Plugin::getInstance()->getPluginConfig($this->plugin['slug']);
        $signature = isset($settings['signature']['value'])
            ? $settings['signature']['value']
            : (isset($settings['signature']) ? $settings['signature'] : '');

        if ($signature === '' || $signature === null) {
            return $content; // 未填写签名时不修改正文
        }
        // 追加签名段落(生产环境请对富文本内容做必要的转义处理)
        return $content . '<p class="hello-signature">' . htmlspecialchars($signature) . '</p>';
    }

    // 挂载点回调:$context 为埋点处透传的上下文数组(admin.head 无上下文),
    // 返回字符串由系统输出到挂载点位置;单个回调抛出异常不会影响页面渲染
    public function injectHead($context = []) {
        // 示例:注入一条元信息。实际开发中通常在此输出插件的 CSS/JS 引用
        echo '<meta name="plugin" content="hello">';
    }
}

将目录放入 plugins/ 后,到「插件管理」安装并启用即可生效。

五、核心开发专题

5.1 钩子监听

通过 $this->addHook($hook, $callback, $priority = 10) 注册($priority 数值越小越先执行)。回调通过 Hook::trigger / Hook::triggerWithReturn / Hook::filter 三种方式被触发:普通触发拼接字符串返回值;带返回值触发收集全部回调返回值为数组;过滤器将数据依次传入各回调并以最终返回值替换。

以下是已在核心代码中验证触发的钩子清单(推荐监听这些钩子;Hook 类中还定义了更完整的常量清单,可按需监听):

钩子名触发位置参数说明
system_initAPI 入口引导阶段无系统初始化
api_dispatchAPI 入口无返回值数组非空即视为插件接管本次 API 请求
article_content_render前台文章详情渲染正文 HTML(过滤器)修改文章正文
page_content_render前台页面详情渲染内容 HTML(过滤器)修改页面内容
front_profile_page个人中心各页渲染前页面标识字符串回调返回 true 表示接管本页渲染
front_interaction_action前台互动 AJAX 端点['action' => 'like'/'favorite'/'follow']返回 true 表示接管本次请求
comment_posted评论提交后comment_id/article_id/user_id/parent_id/auto_approve评论后续处理
profile_settings_deletion个人中心设置页['user', 'template', 'action']注入注销等功能变量
template_notification_count模板引擎变量注入用户 ID返回通知未读数
template_plugin_flags模板引擎变量注入无返回插件可用性标记数组,注入为模板变量
article_create_before / article_create_after文章模型创建前后文章数据 / 文章记录文章创建
comment_add_before / comment_add_after评论模型添加前后评论数据 / 评论记录评论添加(回复为 comment_reply_* 系列)
send_notification通知发送(多处)通知数据数组自定义通知处理
upload_finished文件落盘后、生成文件信息前file_path/file_type/thumbnail图片压缩等后处理
file_upload_after上传完成文件信息数组(含 name/path/relative_path/type)云存储同步等
article_add_before / article_add_after后台文章新增页渲染前后无后台编辑页注入(编辑页为 article_edit_before/after,参数含 article_id)
article_save_before后台保存文章前['data' => $_POST]保存前处理
page_add_before / page_add_after 等后台页面增改同文章系列(编辑含 page_id)页面编辑页注入
admin_header / admin_footer后台公共头尾模板无后台全局输出
plugin_admin_actions插件详情页操作区插件记录数组向详情页追加操作
plugin_admin_controller插件管理控制器内无插件扩展插件管理页
admin_config_collect_custom后台保存系统配置时$_POST 数组返回 ['字段名' => 值] 注入配置
admin_user_list_field后台用户列表['users' => [...]](过滤器)过滤用户列表字段
admin_user_detail_field后台用户详情['user' => &$user]修改用户详情(引用传递)

主题模板中还可通过 {plugin_hook name="钩子名"} 标签在任意位置触发钩子(无参数)。

5.2 挂载点注入

挂载点(slot)是后台模板中的精确埋点,通过 Plugin::slot(挂载点名, 上下文数组) 执行,订阅方式与钩子完全一致(addHook),回调返回的字符串(或可转字符串的对象/字符串数组)直接输出到页面。单个回调抛出的异常会被捕获并记录日志,不会中断页面渲染。

已验证的挂载点清单(共 11 个,覆盖 19 处模板埋点):

挂载点埋点位置上下文参数
admin.head后台所有页面 </head> 前无
admin.footer后台所有页面 </body> 前无
dashboard.quick.links后台仪表盘快捷入口卡区域无
article.title.after后台文章新增/编辑页标题区之后`['mode' => 'add'
article.editor.before后台文章编辑器区域之前同上
article.panel.bottom后台文章侧栏面板底部同上
article.form.after后台文章表单之后同上
page.title.after后台独立页面标题区之后`['mode' => 'add'
page.editor.before后台页面编辑器区域之前同上
page.panel.bottom后台页面侧栏面板底部同上
page.form.after后台页面表单之后同上

注入示例(输出仪表盘快捷入口卡,使用 $this->adminUrl() 生成插件后台页链接):

public function init() {
    $this->addHook('dashboard.quick.links', [$this, 'quickLink']);
}

public function quickLink($context = []) {
    // adminUrl('index') 生成 admin.php?action=plugin.hello.index
    $url = $this->adminUrl('index');
    return '<a href="' . $url . '" class="plugin-quick-card">我的插件</a>';
}

5.3 前台路由接管

在 plugin.json 中声明 routes,核心路由分发前会按「先注册先得」顺序匹配插件路由,命中即由插件控制器处理,未命中回落核心路由:

"routes": [
    {
        "path": "hello/{name}",
        "handler": "Controllers/HelloController.php@show",
        "method": "GET"
    }
]
  • path:相对站点根路径,支持 {param} 占位符(匹配除 / 外的任意字符),参数按出现顺序传给处理器方法;
  • handler:控制器文件相对路径@方法名,控制器文件必须位于插件目录内;
  • method:GET/POST 等,* 或留空匹配任意方法。
<?php
// plugins/hello/Controllers/HelloController.php
class HelloController {
    // $name 由路由占位符 {name} 按顺序传入
    public function show($name = '') {
        // 渲染插件模板:模板根目录指向 plugins/hello/templates/,
        // 模板内可使用全部模板标签(引擎约束与主题模板一致,如循环不可嵌套)
        Plugin::getInstance()->view('hello', 'greet.html', [
            'name' => htmlspecialchars($name),
        ]);
    }
}

5.4 后台页面开发

在 plugin.json 中声明 admin_routes(键为访问 action,值为 控制器文件@方法名,控制器文件相对插件根目录):

"admin_routes": {
    "plugin.hello.index": "Admin/HelloAdminController.php@index"
}

访问地址为 admin.php?action=plugin.hello.index;建议在插件内用 $this->adminUrl('index') 生成,避免手写。控制器为普通类(无需继承基类),方法内自行渲染页面:

<?php
// plugins/hello/Admin/HelloAdminController.php
class HelloAdminController {
    public function index() {
        // 读取插件设置并展示(getPluginConfig 返回默认结构合并已保存值)
        $config = Plugin::getInstance()->getPluginConfig('hello');

        // 方式一:直接引入插件目录内模板(同插件内传参可直接用局部变量)
        $signature = $config['signature']['value'] ?? '';
        include __DIR__ . '/../templates/admin_index.html';

        // 方式二:走模板引擎渲染,Plugin::getInstance()->view('hello', 'admin_index.html', $data)
    }
}

控制器输出的内容会嵌入后台布局;若需要在 <head> 中加载插件专属 CSS/JS,订阅 admin.head 挂载点输出 <link>/<script> 标签即可(参考官方插件的做法,资源地址附带文件修改时间作为版本参数以刷新缓存)。

模板文件示例 plugins/hello/templates/admin_index.html:

<!-- 插件后台页面模板:可使用与主题模板一致的标签语法输出变量 -->
<div class="plugin-page">
    <h2>Hello </h2>
    <p>{$signature}</p>
</div>

5.5 设置项

default_settings 中每一项的键为设置名,值结构支持:type(类型)、label(显示名)、value(默认值)、help(帮助文本)、attributes(HTML 属性,如 min/max/step/required/accept)、options(select/radio/checkbox 选项)、dependency(依赖其他设置项的条件显示)、tab(分组到 config_tabs 标签页)。

支持的类型:boolean、string/text、number、select、radio、checkbox、password、email、url、date、time、datetime、color、file、rich_text、hidden。

"default_settings": {
    "enabled": {
        "type": "boolean",
        "label": "启用功能",
        "value": true,
        "help": "关闭后插件功能整体停用"
    },
    "api_key": {
        "type": "password",
        "label": "接口密钥",
        "value": "",
        "attributes": { "required": true }
    },
    "mode": {
        "type": "select",
        "label": "工作模式",
        "value": "auto",
        "options": { "auto": "自动", "manual": "手动" }
    }
}

读写 API(均在 Plugin 管理器上,公开方法):

  • Plugin::getInstance()->getPluginConfig($slug):读取设置,返回默认结构合并数据库已保存值的结果;
  • Plugin::getInstance()->savePluginConfig($slug, $config):保存设置(boolean 项的取消勾选由系统自动补齐为 false)。

设置保存在插件表 settings 字段(JSON)。插件内部建议封装一个「默认值 + 已保存值」合并读取方法,参考官方插件的写法。

卸载数据规则:卸载默认保留数据。插件可在 default_settings 中声明布尔项 keep_data_on_uninstall(卸载确认弹窗未显式选择时,系统读取该设置作为是否保留数据的依据),并在 uninstall($keepData) 中按该参数决定是否删除自有数据表与配置。

六、数据库操作

插件通过基类 getDatabase() 获取系统数据库服务(亦可 Database::getInstance()),主要方法:

方法说明
table($table)返回带系统前缀的完整表名(拼接 SQL 时必须使用,避免硬编码前缀)
query($sql, $params, $cache = false, $expire = 3600)执行 SQL,$params 为绑定参数数组(预编译占位符 ?),可缓存结果
fetch($sql, $params, ...)查询单行
fetchAll($sql, $params, ...)查询多行
insert($table, $data)插入一行,$data 为关联数组
update($table, $data, $where)更新,$where 为条件数组
delete($table, $where)删除
beginTransaction() / commit() / rollback()事务控制

系统不为插件提供独立的建表迁移机制:插件如需自有数据表,在 install() 生命周期方法中通过上述 API 自行建表与写入种子数据(保持幂等,重复安装不覆盖已有数据),并在 uninstall($keepData) 中根据 $keepData 决定是否清理。始终使用参数绑定,禁止字符串拼接 SQL。

七、访问控制与权限

  • 前台路由:由插件控制器自行处理会话与权限;
  • 后台插件页面(plugin.{slug}.{page})采用两级控制,均由后台权限中间件在分发前完成:
    1. 启用即放行:插件处于启用状态且目标页面已在 admin_routes 注册即放行;插件停用/卸载或路由未注册时返回 403(停用插件的分发还会被路由分发器以 404 兜底);
    2. 角色授权名单(ACL):站长可在插件详情页「访问授权」中配置可访问角色;未配置时对所有可登录后台的角色缺省放行,配置后仅名单内角色可访问,超级管理员(角色 1)始终放行;
  • 后台其余功能页沿用系统基于角色的权限模型(权限代码 + 角色授权),插件不写入权限表。

八、生命周期

插件状态存于数据库(未安装/停用/启用/已卸载),各阶段系统行为与插件可响应的方法:

阶段系统行为插件可响应
发现扫描 plugins/ 目录下含 plugin.json 的目录,校验 name/slug/version/description/author 完整性—
安装依赖与版本兼容校验;注册到数据库(默认停用);实例化插件install():建表、写入种子配置等(建议幂等)
启用实例化插件后更新状态为启用activate():启用初始化
每次请求加载所有启用插件,实例化并调用__construct() 与 init():注册钩子/挂载点订阅(此时才可依赖启用状态)
停用实例化插件后更新状态为停用deactivate():清理临时状态
卸载二次确认弹窗;实例化插件并传入是否保留数据;状态置为已卸载;清除该插件的后台页面授权名单uninstall($keepData):按 $keepData 清理或保留数据

注意:init() 仅在插件启用时执行,停用即完全退出,主系统行为不受影响;不要把必须在安装期完成的工作放进 init()。

九、调试技巧

  • 开发者工具箱插件(官方插件):提供钩子管理与测试、钩子执行日志查看/导出/清除、页面调试面板与调试参数设置。安装启用后,从「插件管理 → 插件详情 → 配置」进入,可查看全部钩子定义、向任意钩子注入测试回调、按时间线排查钩子执行耗时与返回值;
  • 调试配置:系统设置中的调试分组可开启调试模式、错误显示与慢查询日志,对应 debug_* 系列配置键;
  • 日志:插件内可用基类 debug() / info() / warning() / error() 方法记录日志,便于线上排查;
  • 卸载残留排查:插件卸载默认保留数据,重装同 slug 插件不会静默继承旧的后台授权名单(系统会清除),但插件自有数据需在 uninstall() 中自行处理。

十、打包与发布

  1. 目录即包:插件目录本身就是发布单元,zip 包根级应直接包含以 slug 命名的文件夹,文件夹内首层即为 plugin.json 与入口文件,不要嵌套多余目录;
  2. 发布前自检:核对 plugin.json 必填字段、版本号 SemVer 格式、slug 与目录名一致、主类继承 PluginBase、无 eval() 等高风险调用;系统内置的打包校验工具(PluginValidator)可对插件目录执行结构、清单与安全检查,并输出错误与警告摘要;
  3. 图标与缩略图:icon.png 用于列表展示,thumbnail.png 用于详情页封面,均放置在插件根目录并在清单中声明;
  4. README:随包附 README.md,说明功能、配置项含义与卸载数据策略;
  5. 应用市场上架:打包 zip 后提交到 BlogKit 应用市场,通过官方审核后即可被站长搜索安装;上架信息(名称、版本、描述、图标)取自 plugin.json,更新插件时递增 version 并保持 changelog 同步。

相关文档:主题开发指南、API 文档、目录与文件说明