ThinkWo 开发文档DOCS v1.0.45
快速开始
文档首页 / 扩展开发 / 插件开发

插件开发

插件用来给系统"加功能而不改主程序"。一个插件就是一个文件夹,放进去、后台点启用就生效,删掉文件夹就彻底消失。

一、插件能做什么

插件通过钩子(hook)介入系统。系统在固定位置触发钩子,插件在钩子上挂自己的代码:

钩子名触发位置典型用途
head前台每页 <head> 末尾({tw:seo /} 输出时)站长验证 meta、统计代码、自定义 CSS、补充 JSON-LD、hreflang 扩展
admin_menu后台菜单同步时给后台加一级菜单或子菜单项
自定义由插件自己 Plugin::run('xxx') 触发多个插件之间协作(A 触发,B 响应)
插件不是万能的

目前没有"body 注入"钩子。要往页面主体插内容(悬浮客服、弹窗),正规做法是在模板里直接写,或用站点设置里的 footer_code 字段。插件适合做"head 里加东西"和"后台加菜单"这类事。

二、目录结构

plugins/ └─ my-plugin/ 文件夹名 = 插件名(只能用字母、数字、短横线) ├─ plugin.json 必填:插件清单 ├─ Plugin.php 必填:入口类,必须叫 Plugin ├─ config.php 可选:配置项定义(有它后台才有"设置"按钮) └─ lib/ view/ ... 可选:自己的类与模板

命名空间固定为 plugins\{插件名},由系统自动加载,不需要写 require、不需要 composer。

三、最小可跑插件(3 个文件)

目标:在前台 head 里输出一段搜索引擎站长验证 meta。

1. plugin.json

{
  "title": "站点验证",
  "version": "1.0.0",
  "author": "你的名字",
  "description": "在 head 输出 Google / Bing 站长验证 meta 标签",
  "homepage": "https://example.com"
}
字段说明
title后台插件列表显示的名称
version版本号,自己维护
author / description / homepage可选,展示用

2. Plugin.php

<?php

declare(strict_types=1);

namespace plugins\seo_verify;

use app\common\library\Plugin as PluginManager;

class Plugin
{
    /**
     * 插件被启用时由系统调用,在这里挂钩子
     */
    public static function register(): void
    {
        PluginManager::listen('head', [self::class, 'head']);
    }

    /**
     * 输出到前台 head
     * 返回字符串会被拼进 <head> 里
     */
    public static function head(): string
    {
        $google = (string)PluginManager::config('seo_verify', 'google', '');
        $bing   = (string)PluginManager::config('seo_verify', 'bing', '');

        $out = '';
        if ($google !== '') {
            $out .= '<meta name="google-site-verification" content="' . htmlspecialchars($google, ENT_QUOTES) . '">' . "\n";
        }
        if ($bing !== '') {
            $out .= '<meta name="msvalidate.01" content="' . htmlspecialchars($bing, ENT_QUOTES) . '">' . "\n";
        }

        return $out;
    }
}
三条硬规则

1)类名必须是 Plugin,命名空间必须是 plugins\{文件夹名};
2)必须有 public static function register(),系统只调这一个方法;
3)register() 里只挂钩子,别做耗时操作(每页都会执行)。

3. config.php(配置项定义)

<?php

// 后台「插件管理 → 设置」弹窗里的表单定义
return [
    [
        'key'     => 'google',
        'title'   => 'Google 验证码',
        'type'    => 'text',
        'default' => '',
    ],
    [
        'key'     => 'bing',
        'title'   => 'Bing 验证码',
        'type'    => 'text',
        'default' => '',
    ],
];

支持的字段类型

type控件额外参数
text单行输入框—
textarea多行文本域(4 行)—
code等宽多行(写 JS/CSS 用)—
select下拉框'options' => ['left' => '左侧', 'right' => '右侧']
switch开关(值 1 / 0)—

四、读写配置

use app\common\library\Plugin as PluginManager;

// 读单个
$phone = PluginManager::config('my-plugin', 'phone', '默认值');

// 读全部(返回数组)
$all = PluginManager::config('my-plugin');

// 写(会覆盖整个配置数组,先读再合并)
$cfg = PluginManager::config('my-plugin');
$cfg['phone'] = '+86 13800000000';
PluginManager::saveConfig('my-plugin', $cfg);

配置存放在 tw_plugin 表的 config 字段(JSON),跟着数据库走,换服务器不会丢。

五、给后台加菜单

public static function register(): void
{
    PluginManager::listen('admin_menu', [self::class, 'menu']);
}

public static function menu(): array
{
    return [
        [
            'title'    => '站点验证',
            'name'     => 'seo_verify',
            'path'     => '',
            'icon'     => 'layui-icon-vercode',
            'children' => [
                ['title' => '验证设置', 'name' => 'seo_verify/index', 'path' => 'seo_verify/index'],
            ],
        ],
    ];
}

返回的结构和 app/admin/menu.php 完全一致。菜单同步会在下次进入后台时自动执行(菜单版本号变了才同步)。

菜单项指向的控制器必须在 app/admin 里存在

path 写的是「控制器/方法」,路由器去 app\admin\controller 找。插件目录里的类不会被后台路由识别,所以要么指向已有的方法,要么在 app/admin/controller/ 放一个"薄控制器"(几行代码,逻辑仍写在插件目录里)。

注意:app/admin/ 属于主程序目录,升级会覆盖,自建的控制器要记在升级清单里。

六、完整示例:JSON-LD 增强

给所有产品页补一条 Product 结构化数据(系统已内置,这里演示如何用插件追加 aggregateRating 之类扩展字段)。

<?php

declare(strict_types=1);

namespace plugins\rich_snippet;

use app\common\library\Plugin as PluginManager;
use app\common\library\Tags;

class Plugin
{
    public static function register(): void
    {
        PluginManager::listen('head', [self::class, 'head']);
    }

    public static function head(): string
    {
        if (!PluginManager::config('rich_snippet', 'enable', '1')) {
            return '';
        }

        // 只有详情页才输出
        $content = Tags::ctx('content');
        if (!$content) {
            return '';
        }

        $data = [
            '@context' => 'https://schema.org',
            '@type'    => 'Product',
            'name'     => (string)$content['title'],
            'sku'      => (string)($content['model_no'] ?? ''),
            'offers'   => [
                '@type'         => 'Offer',
                'price'         => (string)($content['price'] ?? '0'),
                'priceCurrency' => 'USD',
                'availability'  => 'https://schema.org/InStock',
            ],
        ];

        $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        return '<script type="application/ld+json">' . $json . '</script>';
    }
}

要点:Tags::ctx('content') 能拿到当前页面的内容数组,没有就是 null(说明不是详情页)。所有输出都要 json_encode + 不转义斜杠,否则 URL 会带 \/。

七、启用 / 停用流程

  1. 把插件文件夹放进 plugins/
  2. 后台 → 插件 → 插件管理,列表里会自动出现(系统扫描磁盘)
  3. 点「启用」→ 写入 tw_plugin 表 status=1
  4. 前台刷新即可看到效果(head 钩子每页都会跑)
  5. 「停用」只是把 status 改 0,文件还在;删文件夹则彻底移除
停用插件后记得清缓存

静态化页面里已经烤进了插件输出,不清缓存会继续显示旧内容。

八、安全与性能约定

要求原因
所有输出必须转义插件输出的 HTML 直接进页面,用户可编辑的字段一律 htmlspecialchars()
数据库查询用构造器用 Db::name('xxx')->where(...),不要拼字符串,防注入
register() 里不做 IO每页请求都会执行,做重活会拖慢全站
异常自己 try/catch系统已兜底(插件异常不会白屏),但你自己 catch 能拿到日志更准
不要改主程序文件升级会覆盖;要改行为就给官方提需求或写插件
插件名加前缀如 acme_seo,避免和别人重名

九、调试排查

现象原因
后台插件列表里没有我的插件缺 plugin.json,或 JSON 格式错(多余逗号最常见)
启用了但前台没输出① 钩子名写错;② register() 没被调用(类名/命名空间不对);③ 静态化缓存没清
报 Class "plugins\xxx\Plugin" not found文件夹名和命名空间不一致(大小写也算)
后台没有「设置」按钮缺 config.php,或它 return 的不是数组
保存配置后读到的还是旧值Plugin::config() 有进程内缓存,同一请求内改完要重新读库;跨请求无影响
插件导致白屏看 runtime/log/,通常是语法错或用了不存在的类

十、打包分发

给自己的插件做个 zip,让别人也能装:

# 目录结构要带插件名这一层
zip -r my-plugin.zip my-plugin/

# Windows 上用系统自带 tar(注意用正斜杠,别用 PowerShell 的 Compress-Archive)
C:/Windows/System32/tar.exe -a -c -f my-plugin.zip --format zip my-plugin
Windows 打包必须用 tar.exe

Compress-Archive 打出来的 zip 内部是反斜杠路径,Linux 服务器解压会生成带 \ 的怪文件名,插件直接失效。

ThinkWo CMS v1.0.45 开发文档 · 基于 ThinkPHP 8.1.4 + LayUI 2.12 · 文档随代码更新