轻墨插件开发指南

轻墨插件开发指南:用钩子给博客开“外挂”

插件是轻墨 v2.0 起给第三方留的扩展位:不用改任何核心文件,就能往前台加功能、改文章输出、发通知邮件;v2.1 又补上了插件自己的后台设置页。整件事由一套小小的“钩子”系统支撑。读完本文 + 手写一个示例插件,你就能玩转它。

一个插件 = 一个文件夹(可带设置页)

把插件放进站点根目录的 plugins/ 下,每个插件一个文件夹:

plugins/我的插件/
├── plugin.json   # 名片:在后台“插件市场”里显示的信息(必需)
├── plugin.php    # 灵魂:被系统加载时注册钩子的代码(必需)
└── admin.php     # 可选:后台「设置」页(v2.1 起,启用后会多出「设置」入口)

plugin.json 长这样(纯 JSON):

{
    "name": "页脚问候语",
    "description": "在每页底部追加一句问候(演示 qm_footer 钩子)。",
    "version": "1.0.0",
    "author": "你"
}

plugin.php 的最简骨架:

<?php
// 第一行是安全护栏:插件被直接放网址访问时(绕过系统加载),直接拒绝
if (!defined('QM_BOOT')) { exit('Access denied'); }

add_action('qm_footer', function () {
    echo '<p class="qm-greet" style="font-size:12px;color:#999;">感谢阅读,欢迎常来。</p>';
});

启用方式:后台 →「插件市场」→ 找到它点「启用」。刷新前台页面底部就多了一行字。

工作机制一句话:系统在运行时把 active_plugins(存在配置里)里每个启用插件的主文件 include_once 进来;插件在“加载那一刻”用 add_action / add_filter 把自己注册到钩子上,之后系统在关键位置触发这些钩子,你的代码就跑起来了。停用 = 不再加载,文件不执行,什么副作用都没有。

让插件有后台设置页(v2.1+)

早期插件没有“填写入口”,文案只能写死在代码里(比如备案插件一度改一行字要动文件)。v2.1 起:只要插件启用且自带了 admin.php,插件市场就会在操作列显示「设置」链接,点开是一个已登录鉴权、已套好后台布局的独立页面——表单、保存逻辑全部由你的插件自己负责。

admin.php 模板:

<?php
if (!defined('QM_BOOT')) { exit('Access denied'); }
// 本文件由 admin/plugin-settings.php 在登录后 include,CSRF 字段自己加
$ok = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!verify_csrf($_POST['csrf_token'] ?? '')) {
        echo '<div class="msg error">安全验证失败</div>';
    } else {
        $file = DATA_DIR . '/plugin_hello.php';
        $data = load_data($file, []);
        $data['text'] = trim($_POST['text'] ?? '');
        save_data($file, $data);
        $ok = '已保存。';
    }
}
$text = load_data(DATA_DIR . '/plugin_hello.php', [])['text'] ?? '';
if ($ok) echo '<div class="msg success">' . e($ok) . '</div>';
?>
<form method="post" class="admin-form" style="max-width:520px;">
    <?php csrf_field(); ?>
    <label>问候文字</label>
    <textarea name="text" style="height:80px;"><?php echo e($text); ?></textarea>
    <p><input type="submit" value="保存"></p>
</form>

配套地,plugin.php 里不再写死文案,而是从同一个数据文件读:

$text = trim((string)load_data(DATA_DIR . '/plugin_hello.php', [])['text'] ?? '');
if ($text !== '') {
    add_action('qm_footer', function () use ($text) {
        echo '<p class="qm-hello">' . e($text) . '</p>';
    });
}

看活例子:plugins/footer-beian/admin.php(页脚备案)、plugins/sidebar-notice/admin.php(侧栏公告)就是这么写的,后台改一行字不用再碰代码。

钩子地图:哪里能“插手”

  • qm_head(动作):前台 <head> 内 —— 适合注入 CSS、统计脚本、字体;
  • qm_sidebar(动作):侧边栏各小部件之后 —— 往里塞自定义小部件;
  • qm_sidebar_links(动作):侧边栏“链接”列表内 —— 追加链接;
  • qm_footer(动作):页脚区域 —— 追加版权/备案/问候;
  • qm_post_content(过滤器):单篇文章正文输出前,参数 $content, $post —— 改正文;
  • qm_post_footer(过滤器):文章页脚(标题/链接/作者/协议)输出前,参数 $html, $post
  • qm_admin_footer(动作):后台页面底部。

v2.1 起,后台与列表也开放了扩展点(置顶插件 plugins/qingmo-sticky 正是用它们实现的):

  • qm_posts_list(过滤器)load_posts() 排好序后的文章数组 —— 可重新排序(置顶)、增删文章;
  • qm_post_row_actions(过滤器):后台文章列表每行的操作 HTML,参数 $actions, $post, $token —— 往里追加按钮(置顶/自定义操作);
  • qm_admin_posts_head(动作):后台「文章管理」页顶部(鉴权后、输出前)—— 处理 GET 类切换请求并跳转。
  • 动作(do_action):到点喊一嗓子,你在回调里 echo 或做事即可;
  • 过滤器(apply_filters):把系统的一个值交给你“加工”后再还回去,必须 return 加工后的值。

add_action($钩子, $回调, $优先级=10)add_filter($钩子, $回调, $优先级=10, $接受参数个数)。回调建议用带自己前缀的具名函数或匿名函数,避免和别的插件撞名。

示例一:给文章正文“加戏”(过滤器)

写一个在每篇文章末尾追加小尾巴的插件。因为正文早已渲染成 HTML(Markdown 模式也在保存时转成 HTML),你处理的是 HTML 字符串:

<?php
if (!defined('QM_BOOT')) { exit('Access denied'); }

add_filter('qm_post_content', function ($html, $post) {
    $tail = '<p class="qm-post-tail" style="margin-top:18px;padding:10px 14px;'
        . 'border:1px dashed #ccc;font-size:13px;color:#666;">'
        . '—— 本文来自「' . e($post['title'] ?? '') . '」,欢迎留言讨论。</p>';
    return $html . $tail;
}, 10, 2);

要点:

  • 签名固定两个参数 $content(正文 HTML)和 $post(文章数组,可读 titleid 等);
  • 一定要 return,否则前台文章会“消失”;
  • 输出任何文本前用系统提供的 e() 转义,尤其是来自用户的内容。

示例二:往侧栏塞一个小部件(动作)

<?php
if (!defined('QM_BOOT')) { exit('Access denied'); }

add_action('qm_sidebar', function () {
    echo '<div class="box qm-sidebar-about">'
        . '<h3>关于作者</h3>'
        . '<p style="font-size:13px;">你好,我是「'
        . e(get_setting('site_title')) . '」的维护者。</p>'
        . '</div>';
});

顺带一提 qm_head 的常见用法是注入统计脚本或外部字体:

add_action('qm_head', function () {
    echo '<link rel="stylesheet" href="https://example.com/extra.css">';
});

插件能用哪些系统能力

插件加载时,轻墨的核心函数都可用,常用的有:

  • 读配置get_setting('键', 默认值) —— 读取站点设置项;
  • 输出转义e($str) —— HTML 转义,输出用户内容必用;
  • 数据读取load_data($文件路径, $默认) —— 读取 PHP return 数组文件;
  • 数据保存save_data($文件路径, $数组) —— 自动生成数据文件(带请求内缓存);
  • 文章/评论/友链(只读)load_posts()load_post($id)load_categories()load_comments($post_id)load_links()
  • 站点 URLsite_base_url() —— 得到站点根地址;
  • Markdownmd_to_html($text) —— 把 md 渲染成 HTML;
  • 发邮件qm_send_mail($收件人, $标题, $纯文本, $html=null) —— 走后台 SMTP / mail 配置。

插件想存自己的数据怎么办? 数据目录受 .htaccess / Nginx 规则保护,外部访问不到,是理想的插件存储位置:

// 示例:记录一个简单的“公告文案”
$file = DATA_DIR . '/plugin_announce.php';
$data = load_data($file, []);
$data['text'] = '欢迎来到本站';
save_data($file, $data);

DATA_DIR 常量指向 data/ 目录。注意所有数据文件都以 <?php return ...; 格式存储,由 save_data 生成,别手写奇怪格式。

发提醒邮件同样简单(前提:后台已配置 SMTP 或服务器支持 PHP mail):

qm_send_mail('someone@example.com', '你好', "这是一封来自轻墨插件的邮件。\n第二行。");

三个会踩的坑

  1. 忘记 return(过滤器专用)。动作钩子你随便 echo;过滤器你 add_filter 后若回调没 return,文章正文就没了。写完先开一篇草稿文试。
  2. 函数/全局撞名。插件可能和别的插件或未来核心函数重名。具名函数加自己前缀(myplugin_xxx),变量尽量写在函数/闭包内。
  3. 把用户数据直接 echo。昵称、评论内容都来自访客,输出前一律过 e(),不然等于给站点开 XSS 后门。

调试与分发

  • 改完 plugin.php / admin.php 直接刷新对应页面即可(每次请求都重新加载),觉得没生效先 Ctrl+F5 清浏览器缓存;
  • 出错了不知道哪来的,先在 plugin.php 顶部临时 error_log('here');,再打开后台看一眼 php_errors.log 之类日志;
  • 分发 = 把 plugin.json + plugin.php(如有 admin.php 一并)打包成 zip,别人解压进 plugins/ 后到「插件市场」启用,需要填写的就能点「设置」;
  • 卸载 = 后台停用,然后删掉整个文件夹(设置存在 data/ 下的独立数据文件里,会一并删)。

内置插件是最好的活教材:footer-beian(页脚备案,动作钩子 + 设置页示例)、post-copyright(文章版权尾巴,过滤器)、sidebar-notice(侧栏公告,动作钩子 + 设置页示例)、qingmo-sticky(文章置顶,列表排序/行操作钩子示例)。直接对着读一遍胜过十篇教程。


一句话版本:plugin.json 声明自己,plugin.phpadd_action / add_filter 挂到钩子上;想给站长可填的配置,再加一个带表单的 admin.php 设置页。动作钩子做事、过滤器钩子改值;输出必转义、过滤器必 return。

标签: Qingmo, 插件, 教程