ZhiCms 插件开发文档
本指南介绍如何为 ZhiCms 开发标准(Native)格式插件。插件通过钩子(Hook)挂载到系统运行流程中,实现「不改动框架代码」地扩展功能。
兼容性:ZhiCms 同时兼容 Z-Blog / WordPress / Emlog 三种历史格式插件(通过
ZhiCms\base\compat\Compat兼容层),本文仅针对官方推荐的 Native 格式。
0. 先选类型:你要写哪种插件?
ZhiCms 的插件按「是否需要接管前台页面 / 是否依赖数据库」分为三大类,开发方式差异很大,请先对号入座:
| 类型 | 典型场景 | 依赖数据库 | 是否接管前台页 | 核心入口 |
|---|---|---|---|---|
| A. 普通插件(不依赖数据库) | 页脚加公告、注入统计代码、改写已有模板内容、SEO 追加 | 否 | 否 | register() 挂钩子 / 过滤器 |
| B. 依赖数据库的插件 | 留言板、访问日志、自定义表单、积分、独立数据表业务 | 是 | 否(或仅后台页) | install.sql 建表 + register() 钩子 + Setting.php |
| C. 模板化插件(接管前台) | 导购站、论坛、商城风格整站、把插件设为站点首页 | 通常也依赖数据库 | 是 | plugin.json 的 rewrite + Plugin::displayPage() + 自有控制器/视图 |
本文第 1–4 章讲通用结构(三类插件都要遵守),第 5–7 章分别用完整示例讲三类插件怎么写,第 8 章讲后台菜单 / 设置页 / 首页接管,第 9 章讲打包与安装,第 10 章讲注意事项。
1. 插件目录结构(通用约定)
插件统一存放于 网站根目录 plugins/{alias}/,别名(alias)即目录名,只能包含 字母、数字、下划线、连字符。
1.1 最小结构(类型 A/B 够用)
plugins/
└── my_plugin/ # 插件目录(alias = my_plugin)
├── plugin.json # 元信息(必需)
├── Plugin.php # 插件入口类(必需,继承 BasePlugin)
├── Setting.php # 设置组件(可选,hasSetting=true 时需要)
├── install.sql # 安装时执行的建表 SQL(可选,类型 B 需要)
├── uninstall.sql # 卸载时执行的 SQL(可选)
└── view/ # 插件私有视图(可选,后台设置页 / 片段)
└── setting.php
1.2 模板化插件结构(类型 C 推荐)
plugins/
└── demo/ # alias = demo
├── plugin.json # 必须声明 type=template + rewrite 规则
├── Plugin.php # 入口:displayPage() 调度到控制器
├── controller/
│ ├── DemoController.php # 插件控制器基类(assign/display/isMobile)
│ └── SiteController.php # 业务:index/detail/list... 真实写在哪
├── view/ # 模板目录(与控制器里的 view_path 对应)
│ ├── header.html # 插件页头(复用或自建)
│ ├── footer.html
│ ├── sidebar.html
│ ├── index.html # 首页
│ ├── detail.html # 详情页
│ ├── list.html
│ └── ...
├── static/ # 插件自有静态资源
│ ├── css/style.css
│ └── js/main.js
├── install.sql # 若插件自身有表(可选)
└── Setting.php # 可选后台设置
目录命名建议:
controller/(业务控制器)、view/(模板)、static/(资源)是社区约定,框架本身不强制,只要Plugin.php/plugin.json在根目录即可。关键点:view/必须是Plugin::displayPage()或控制器display()实际指向的模板根目录。
2. plugin.json 元信息(三类插件通用)
{
"alias": "my_plugin",
"name": "我的插件",
"version": "1.0.0",
"author": "你的名字",
"description": "插件功能描述",
"hasSetting": true,
"type": "common",
"menu": [
{ "title": "插件设置", "url": "index.php?r=manage/plugin/setting&alias=my_plugin" }
],
"requires": "5.0.0",
"rewrite": {
"plug-<alias>.html": "index/plug/view/alias=<alias>",
"plug-<alias>-<id>.html": "index/plug/view/alias=<alias>/id=<id>"
}
}
字段说明:
| 字段 | 必填 | 说明 |
|---|---|---|
alias |
是 | 插件别名,必须与目录名一致 |
name |
是 | 插件显示名称 |
version |
是 | 版本号 |
type |
否 | common(默认,普通插件)/ template(模板化插件,可被设为站点首页)。template 同时需要 rewrite 里含 plug-<alias>.html 规则 |
hasSetting |
否 | true 时后台出现「设置」入口,需提供 Setting.php |
menu |
否 | 后台侧边栏「扩展功能」分组下注入的菜单项 |
rewrite |
否 | 模板化插件必填。伪静态规则(见第 7 章) |
requires |
否 | 依赖的最低系统版本 |
模板化插件判定逻辑(源码
PluginManager::getTemplatePlugins()):rewrite中存在plug-<alias>.html且Plugin::displayPage()方法存在,即被视为可被设为主页的模板化插件。后台「系统设置 → 站点设置 → 主页展示插件」下拉里会列出这些插件。
3. 插件入口类 Plugin.php(三类插件通用)
入口类必须继承 ZhiCms\base\plugin\BasePlugin,命名空间为 plugins\{alias},类名固定为 Plugin:
<?php
namespace plugins\my_plugin;
use ZhiCms\base\plugin\BasePlugin;
use ZhiCms\base\Hook;
class Plugin extends BasePlugin
{
/**
* 钩子注册:插件「已启用」时,每次请求都会调用。
* 在此通过 Hook::add() 把回调挂到系统钩子上。
*/
public function register()
{
Hook::add('appBegin', array($this, 'myHook'));
}
/** 前台展示页入口(仅模板化插件 C 需要覆写) */
public function displayPage($params = array()) {}
/** 首次安装:建目录 / 初始化数据(install.sql 已自动执行) */
public function install() {}
/** 卸载:清理数据 / 删除文件 */
public function uninstall() {}
/** 启用 / 停用 */
public function enable() {}
public function disable() {}
}
3.1 生命周期方法(BasePlugin 可覆写)
| 方法 | 触发时机 | 典型用途 |
|---|---|---|
register() |
每次请求,插件处于启用状态时 | 注册钩子(最重要) |
install() |
首次安装(install.sql 已执行后) |
创建目录、初始化数据 |
uninstall() |
卸载插件时 | 清理建的表 / 生成的文件 |
enable() |
从停用变为启用时 | 恢复数据 / 建目录 |
disable() |
从启用变为停用时 | 停用运行 / 暂停任务 |
displayPage($params) |
访问 plug-<alias>*.html 或 index.php?r=index/plug/view 时 |
模板化插件输出前台页 |
注意:
BasePlugin没有boot()方法(历史文档里提到的boot()实际是register()的别名用途,请以register()为准)。系统启动时PluginManager::boot()负责合并插件rewrite规则并register()钩子,插件无需自己写boot()。
3.2 BasePlugin 内置可用方法
| 方法 | 可见性 | 说明 |
|---|---|---|
getConfig() |
protected | 读插件配置(来自 yun_plug.config 的 JSON),返回数组 |
setConfig($data) |
protected | 保存插件配置(自动 JSON 化写入 yun_plug.config) |
displayPage($params) |
public | 模板化插件前台页入口(见第 7 章) |
pageUrl($params) |
public | 生成插件展示页链接(伪静态优先,动态兜底) |
render($tpl, $vars) |
protected | 渲染插件私有模板 plugins/{alias}/view/{tpl}.html(复用 think-template 引擎,返回字符串) |
3.3 使用框架能力(三类插件通用)
use ZhiCms\base\Config;
// 读数据库({pre} 自动替换为表前缀 yun_)
$rows = obj('api/ApiData')->thisQuery("SELECT * FROM {pre}my_table WHERE id = ?", array($id));
// 读站点配置
$siteName = obj('base/Base')->SiteConfig('sitename');
$cfg = \app\common\ConfigStore::load('site'); // 站点配置数组
// 写操作日志
\ZhiCms\ext\AdminLog::write('my_plugin', '做了某事');
// 读/写插件自身配置
$conf = $this->getConfig();
$this->setConfig(array_merge($conf, array('foo' => 'bar')));
4. 钩子(Hook)系统(类型 A/B 的核心)
4.1 注册钩子
Hook::add($tag, $callable, $priority = 10);
$tag:钩子名(系统内置或业务自定义)$callable:回调,支持数组[$this,'method']、字符串'Class::method'、'Class@method'、普通函数名、闭包$priority:数值越小越先执行
4.2 系统内置钩子
| 钩子名 | 触发时机 | 参数 |
|---|---|---|
appBegin |
框架启动 | (无) |
appEnd |
框架结束 | (无) |
appError |
框架错误 | $e |
routeParseUrl |
路由解析 | $rewriteRule, $rewriteOn |
actionBefore / actionAfter |
控制器方法执行前后 | $obj, $action |
templateParse |
模板编译前 | $template(模板内容,可改写) |
dbQueryBegin / dbQueryEnd / dbExecuteBegin / dbExecuteEnd / dbException |
SQL 执行前后 | $sql, $params / $data / $affectedRows / $err |
业务代码也可通过 Hook::listen($tag, $args) 自定义触发任意钩子名,插件可自由挂载。
4.3 过滤器(改写数据)
// 插件注册过滤器
Hook::add('article_content', function ($content) {
return $content . "\n<!-- by plugin -->";
});
// 业务代码触发(返回值被依次改写)
$content = Hook::filter('article_content', $content, array($article));
全局助手
do_action()/apply_filters()(ZhiCms/core.php)分别对应Hook::listen()/Hook::filter()。
5. 类型 A:不依赖数据库的普通插件(示例:页脚公告 + 统计代码)
特征:只挂载钩子、注入 HTML、改写模板/内容,完全不碰数据库。
5.1 目录
plugins/hello/
├── plugin.json
└── Plugin.php
5.2 plugin.json
{
"alias": "hello",
"name": "Hello 公告插件",
"version": "1.0.0",
"author": "你",
"description": "在页脚注入公告与统计代码(无数据库)",
"hasSetting": true
}
5.3 Plugin.php
<?php
namespace plugins\hello;
use ZhiCms\base\plugin\BasePlugin;
use ZhiCms\base\Hook;
class Plugin extends BasePlugin
{
public function register()
{
// 在 appEnd 钩子(框架输出结束前)注入页脚公告 + 统计代码
Hook::add('appEnd', function () {
$cfg = $this->getConfig();
if (empty($cfg['enabled'])) return;
$notice = $cfg['notice'] ?? '欢迎光临本站';
echo '<div class="hello-footer-notice" style="text-align:center;padding:10px;background:#f5f5f5">'
. htmlspecialchars($notice, ENT_QUOTES) . '</div>';
if (!empty($cfg['stat_code'])) {
echo $cfg['stat_code']; // 统计代码一般是 <script>,不转义
}
});
// 改写文章正文(追加署名)
Hook::add('article_content', function ($content) {
return $content . '<p style="color:#999">—— 本文由 Hello 插件标记 ——</p>';
});
}
public function install() {}
public function uninstall() {}
}
5.4 Setting.php(可选,用于后台配置公告内容/统计代码)
<?php
namespace plugins\hello;
class Setting
{
protected $meta = array();
public function __construct($meta = array()) { $this->meta = $meta; }
public function view()
{
$config = \ZhiCms\base\PluginManager::getConfig('hello');
$config = array_merge(array('enabled' => 0, 'notice' => '', 'stat_code' => ''), $config);
ob_start();
include __DIR__ . '/view/setting.php';
return ob_get_clean();
}
public function save($data)
{
return array(
'enabled' => intval($data['enabled'] ?? 0),
'notice' => trim($data['notice'] ?? ''),
'stat_code' => $data['stat_code'] ?? '', // 原始值,不转义
);
}
}
后台访问
index.php?r=manage/plugin/setting&alias=hello。save()返回的数组由系统自动写入yun_plug.config,无需手动setConfig()。
6. 类型 B:依赖数据库的插件(示例:访问日志)
特征:需要自己的数据表(install.sql 建表),在钩子里读写数据,可配合后台设置页。
6.1 目录
plugins/visit_log/
├── plugin.json
├── Plugin.php
├── Setting.php
├── install.sql # 建表
├── uninstall.sql # 删表(可选,也可在 uninstall() 里 DROP)
└── view/setting.php
6.2 install.sql({pre} 会自动替换为 yun_)
CREATE TABLE IF NOT EXISTS `{pre}visit_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`url` varchar(255) NOT NULL DEFAULT '',
`ip` varchar(45) NOT NULL DEFAULT '',
`ua` varchar(255) NOT NULL DEFAULT '',
`addtime` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `url` (`url`(191))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
6.3 Plugin.php
<?php
namespace plugins\visit_log;
use ZhiCms\base\plugin\BasePlugin;
use ZhiCms\base\Hook;
class Plugin extends BasePlugin
{
public function register()
{
Hook::add('appEnd', function () {
$cfg = $this->getConfig();
if (empty($cfg['enabled'])) return;
$url = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
// 写入插件自有表(参数化防注入)
obj('api/ApiData')->insertData('yun_visit_log', array(
'url' => $url,
'ip' => $ip,
'ua' => $ua,
'addtime' => time(),
));
}, 999); // 优先级大,最后执行,避免影响主流程
}
/** 后台查看日志(可选):配合菜单跳转或用 Setting 页输出列表 */
public function uninstall()
{
// 卸载时删表(二选一:这里 DROP 或提供 uninstall.sql)
try {
obj('api/ApiData')->executeQuery("DROP TABLE IF EXISTS `{pre}visit_log`");
} catch (\Throwable $e) {}
}
}
6.4 数据库 CRUD 速查(类型 B 必读)
表名统一 yun_ 前缀,{pre} 在 SQL 中自动替换。推荐用封装方法:
// 增(返回自增 id)
$id = obj('api/ApiData')->insertData('yun_visit_log', array('url'=>$url,'ip'=>$ip,'addtime'=>time()));
// 批量增
obj('api/ApiData')->insertAllData('yun_visit_log', $dataList);
// 查(单条,无 $order 走 find() 返回一维数组)
$row = obj('api/ApiData')->dataSelect('yun_visit_log', array("`id` = $id"));
// 查(多条,传 $order 走 select() 返回二维数组)
$list = obj('api/ApiData')->dataSelect('yun_visit_log', array("1"), '`id` DESC LIMIT 0,20');
// 原生查询(参数化)
$rows = obj('api/ApiData')->thisQuery(
"SELECT * FROM {pre}visit_log WHERE `ip` = ? ORDER BY id DESC LIMIT 10", array($ip)
);
// 改(注意 SET 与 WHERE 占位符不要同名冲突,建议用 :__id)
obj('api/ApiData')->dataUpdate('yun_visit_log', array('url'=>$newUrl), array("`id` = $id"));
// 删(参数化)
obj('api/ApiData')->deleteThis('yun_visit_log', "`id` = ?", array($id));
auto_increment 注意:插入时不要手填自增
id字段,否则 MySQL 严格模式下id重复/类型错误会报错。让数据库自增即可。
7. 类型 C:模板化插件(接管前台页,可被设为站点首页)
特征:有自己的控制器 + 视图 + 静态资源,通过 plugin.json 的 rewrite 规则暴露前台页,可整站接管首页(home_plug)。
本仓库
plugins/guangdiu、plugins/kiees就是这种插件,下面以「极简导购插件 demo」为例,结构照搬它们。
7.1 plugin.json(必须 type=template + rewrite)
{
"alias": "demo",
"type": "template",
"name": "Demo 导购",
"version": "1.0.0",
"author": "你",
"description": "模板化插件示例",
"hasSetting": false,
"rewrite": {
"plug-<alias>-cheaps.html": "index/plug/view/alias=<alias>/mod=cheaps",
"plug-<alias>-brand.html": "index/plug/view/alias=<alias>/mod=brand",
"plug-<alias>-<id>.html": "index/plug/view/alias=<alias>/id=<id>",
"plug-<alias>.html": "index/plug/view/alias=<alias>"
}
}
7.2 Plugin.php(displayPage 调度入口)
<?php
namespace plugins\demo;
use ZhiCms\base\plugin\BasePlugin;
class Plugin extends BasePlugin
{
public function displayPage($params = array())
{
$controller = new \plugins\demo\controller\SiteController();
$controller->run($params); // run() 内部按 $params['mod'] 分发到 index/detail/...
exit; // 必须 exit,避免框架再输出默认布局
}
}
调度机制:框架
app\index\controller\PlugController::view()收到请求后,把除alias/r外的所有参数(如mod、id、kw、page)收集进$params,调用Plugin::displayPage($params)。插件自己决定怎么分发。
7.3 控制器基类(assign / display / isMobile)
因为模板化插件不继承框架前台控制器,需要自建一个轻量基类,提供 assign() / display() / isMobile() 等能力。可照搬 guangdiu 的 GuangdiuController:
<?php
namespace plugins\demo\controller;
use ZhiCms\base\ThinkTemplate; // 注意:插件默认用 think-template 引擎渲染自有模板
class DemoController
{
protected $vars = array();
protected $alias = 'demo';
protected $isMobile = null;
protected function isMobile()
{
if ($this->isMobile !== null) return $this->isMobile;
// 简化:直接用 UA 判断(可参考 guangdiu 支持 ?pc=1 / ?m=1 强制切换)
$ua = strtolower($_SERVER['HTTP_USER_AGENT'] ?? '');
$this->isMobile = (bool) preg_match('/iphone|android|mobile|micromessenger/i', $ua);
return $this->isMobile;
}
protected function assign($name, $value = null)
{
if (is_array($name)) { foreach ($name as $k=>$v) $this->vars[$k]=$v; }
else { $this->vars[$name] = $value; }
return $this;
}
/**
* 渲染插件私有模板 plugins/demo/view/{tpl}.html
* 注意:display() 内必须 exit,防止框架再输出。
*/
protected function display($tpl = 'index', $extra = array())
{
$config = \ZhiCms\base\Config::get('TPL');
$viewPath = \BASE_PATH . 'plugins/' . $this->alias . '/view/';
$tplConfig = array_merge((array)$config, array(
'TPL_PATH' => $viewPath,
'view_path' => $viewPath,
'TPL_SUFFIX' => '.html',
'view_suffix' => 'html',
));
$engine = new ThinkTemplate($tplConfig);
// 注入控制器 public 属性 + $this->vars + 公共站点变量
foreach (get_object_vars($this) as $k => $v) {
if ($k === 'vars') continue;
$engine->assign($k, $v);
}
foreach ($this->vars as $k => $v) { $engine->assign($k, $v); }
foreach ((array)$extra as $k => $v) { $engine->assign($k, $v); }
// 注入常用站点变量,供模板使用
$base = obj('base/Base');
$engine->assign('site_name', $base->SiteConfig('sitename'));
$engine->assign('copyright', $base->SiteConfig('banquan'));
$engine->assign('plug_static', '/plugins/' . $this->alias . '/static');
$engine->assign('is_mobile', $this->isMobile() ? 1 : 0);
$engine->display($tpl); // 渲染 plugins/demo/view/{tpl}.html
exit;
}
protected function arg($name = null, $default = null)
{
static $args;
if (!$args) { $args = array_merge((array)$_GET, (array)$_POST); }
if ($name === null) return $args;
if (!isset($args[$name])) return $default;
$v = $args[$name];
if (is_array($v)) { array_walk($v, function(&$x){ $x=trim(htmlspecialchars($x, ENT_QUOTES)); }); return $v; }
return trim(htmlspecialchars($v, ENT_QUOTES));
}
protected function db() { return obj('api/ApiData'); }
}
7.4 业务控制器 SiteController(真实逻辑写这)
<?php
namespace plugins\demo\controller;
class SiteController extends DemoController
{
/** 统一入口:按 $params['mod'] 分发 */
public function run($params)
{
$mod = isset($params['mod']) ? $params['mod'] : '';
switch ($mod) {
case 'detail': $this->detail($params); break;
case 'cheaps': $this->cheaps(); break;
case 'brand': $this->brand(); break;
default: $this->index();
}
}
public function index()
{
$list = $this->db()->dataSelect('yun_article', array("`status` = 1"), '`id` DESC LIMIT 0,20');
$this->assign('list', $list ?: array());
$this->assign('page_title', '首页 - Demo');
$this->display('index');
}
public function detail($params)
{
$id = isset($params['id']) ? intval($params['id']) : 0;
$row = $this->db()->dataSelect('yun_article', array("`id` = $id", "`status` = 1"));
if (empty($row)) { header('HTTP/1.1 404 Not Found'); echo '内容不存在'; exit; }
// 浏览量自增(访客去重)
$vk = 'demo_view_' . $id;
if (empty($_COOKIE[$vk])) {
$this->db()->executeQuery("UPDATE `{pre}article` SET `view`=`view`+1 WHERE `id`=?", array($id));
if (isset($row['view'])) $row['view'] = (int)$row['view'] + 1;
setcookie($vk, '1', time()+3600, '/');
}
$this->assign('article', $row);
$this->assign('page_title', $row['title']);
$this->display('detail');
}
public function cheaps() { $this->assign('list', array()); $this->display('cheaps'); }
public function brand() { $this->assign('list', array()); $this->display('brand'); }
}
7.5 模板(plugins/demo/view/)
index.html:
{include file="plugins/demo/view/header"}
<div class="container">
{foreach $list as $item}
<div class="gd-post">
<a href="{plug_url}-{$item.id}.html">{$item.title}</a>
<span>人气:{$item.view}</span>
</div>
{/foreach}
</div>
{include file="plugins/demo/view/footer"}
插件模板用 相对于插件根目录的 include 路径(
plugins/demo/view/header),与框架前台模板app/index/view/public/header区分开。模板语法用 think-template 引擎({foreach}/{$x.y}/{include}),与 TEMPLATE_DEV.md 第 3 章通用语法一致。
7.6 设为站点首页(home_plug)
后台「系统设置 → 站点设置 → 主页展示插件」下拉选择 demo 后,系统把 site.home_plug 存为 demo。框架 ZhiCms\base\App::applyHomePlug() 在访问 /、/index.php、/index.html 时,自动把路由改写为 index/plug/view/alias=demo(URL 不变,用户看到的还是首页地址)。
前提:
plugin.json的type=template且rewrite含plug-<alias>.html。- 插件已启用。
- 站点伪静态
REWRITE_ON=1(仅伪静态访问需要;动态index.php始终可走)。
7.7 伪静态规则注意
- 占位符:规则里用
<key>,映射里对应key=<key>。<id>仅允许数字;其余[\w%-]+。 - 顺序:框架按「占位符越少越靠前」合并(具体见
PluginManager::boot()),字面量越具体的规则(占位符少的)排在前面。例如plug-<alias>-cheaps.html(0 个动态占位符,cheaps 是字面量)必须排在plug-<alias>-<id>.html(1 个占位符)之前,否则plug-demo-123.html会被 cheaps 规则误匹配。 - 修改
rewrite后:停用再启用插件(触发PluginManager::clearBoot()重新合并规则),并清runtime/cache+runtime/static_cache。
7.8 链接生成
// 插件内
echo $this->pageUrl(array('id' => 123)); // plug-demo-123.html(伪静态开)/ index.php?r=index/plug/view&alias=demo&id=123
echo $this->pageUrl(array('mod' => 'cheaps')); // plug-demo-cheaps.html
// 模板里(已由 display() 注入 plug_url 变量)
<a href="{$plug_url}-{$item.id}.html">详情</a>
8. 后台菜单 / 设置页 / 首页接管 汇总
8.1 后台菜单注入
plugin.json 的 menu 配置后,插件启用时菜单自动注入后台侧边栏「扩展功能」:
"menu": [ { "title": "插件设置", "url": "index.php?r=manage/plugin/setting&alias=my_plugin" } ]
8.2 设置页(Setting.php)
hasSetting: true 时提供 Setting.php(plugins\{alias}\Setting),含 view()(返回 HTML 字符串)与 save($data)(返回要落库的数组)。系统自动把 save() 结果写入 yun_plug.config。
8.3 首页接管(仅模板化)
见第 7.6 节。
9. 打包与安装
- 将插件目录打成 ZIP,压缩包根目录应直接包含
{alias}/文件夹(包内路径demo/plugin.json)。 - 后台「系统 → 插件管理 → 上传插件」,选 ZIP。
- 上传后点「安装」:执行
install.sql→ 调install()→ 写yun_plug注册表 → 启用。 - 可「启用 / 停用 / 卸载」(卸载可选删除文件)。
支持
.zba(Z-Blog 格式)与__MACOSX元目录自动排除。ZIP 多层嵌套会被规范化。
9.1 插件注册表
插件信息存于 {pre}plug 表:
| 字段 | 说明 |
|---|---|
alias |
插件别名 |
name / version / author |
元信息 |
status |
1=启用 0=停用 |
installed |
1=已安装 |
config |
插件配置(JSON 文本,Setting::save 写入) |
addtime |
安装时间 |
10. 开发建议与注意事项
10.1 通用
- 不要改框架核心文件,只通过钩子/插件扩展,避免升级被覆盖。
- 命名空间:入口
plugins\{alias}\Plugin、设置plugins\{alias}\Setting,类名固定。 register()每次请求执行,钩子注册要轻量,重活在回调里按需做。{pre}表前缀:SQL 里统一用{pre},不要写死yun_。- 配置读写:用
getConfig()/setConfig(),不要直接改yun_plug表。 - 出错防护:钩子回调里 try/catch,单插件错误不应拖垮整站。
10.2 模板化插件(类型 C)特有
display()必须exit:否则框架会继续输出默认布局,导致页面重复/错乱。- 模板根目录:
display()里的TPL_PATH必须指向plugins/{alias}/view/,否则找不到模板。 - include 路径:插件模板 include 插件自己的片段用
plugins/demo/view/header,与前台app/index/view/public/header区分。 - 引擎选择:插件自有模板建议用 think-template 引擎(与 guangdiu/kiees 一致),语法见 TEMPLATE_DEV.md 第 3/5 章。
- 移动端:
isMobile()自行实现(参考 guangdiu 支持?pc=1/?m=1强制切换 + cookie 记忆)。 - 静态资源:用
{plug_static}(由 display 注入为/plugins/{alias}/static)引用,避免硬编码。 - SEO / 版权:模板里用
obj('base/Base')->SiteConfig('sitename'/'banquan'/'beian')取站点配置,保持与主站一致。
10.3 数据库插件(类型 B)特有
- 建表用
install.sql({pre}占位),卸载用uninstall()里DROP TABLE或uninstall.sql。 - 表名
yun_前缀,与框架一致。 - 用
insertData/dataSelect/dataUpdate/deleteThis封装;复杂 SQL 用thisQuery/executeQuery并参数化。 - 大数据量用
insertAllData()批量插;分页用dataCount()+page()。
10.4 最小可运行插件清单
- [ ]
plugin.json(alias 与目录一致) - [ ]
Plugin.php(继承 BasePlugin,必要逻辑在register()或displayPage()) - [ ] 依赖 DB:补充
install.sql+ CRUD - [ ] 模板化:补充
rewrite+ 控制器 +view/模板 +display()内exit - [ ] 有设置:
Setting.php - [ ] 安装后清
runtime/cache+runtime/static_cache验证
本文档基于 ZhiCms 插件系统实现(参考 plugins/guangdiu、plugins/kiees 真实代码)编写,覆盖三类插件(无数据库 / 依赖数据库 / 模板化接管前台)的目录结构、写法、伪静态、首页接管与打包安装。