插件开发:从零写一个
插件放在哪、三条硬规矩、最小可运行例子、生命周期与崩溃熔断。
先想清楚:这该做成插件吗
插件能往站点里塞任意 PHP 代码,权限和内核一样大。所以只有真的需要"改程序行为"时才做插件: 加一个新标签、接一个第三方服务、改渲染逻辑。如果只是换个样子,那是主题的活; 只是加一批内容,那是栏目与内容的活。
一分钟生成骨架
后台 应用商店 → 开发者脚手架 填个标识就能生成一份能跑起来的插件骨架。 生成物里有清单、入口、后台页模板与幂等建表脚本,注释就是下面这些规矩。 本文把"为什么这么写"讲清楚,骨架负责让你不用手抄。
三条硬规矩
- 四个名字必须一致:目录名 =
plugin.json里的name= 后台菜单路由 = 权限键的前缀。不一致时后台会直接拒绝启用,这是故意的 —— 名字对不上的插件,出问题根本没法排查是哪一个。 - 类名一律
Plug_前缀。本程序没有命名空间,自动加载是"类名即文件名", 不带前缀迟早和内核类名撞车,而且撞上就是致命错误。 - 入口只做注册。
bootstrap.php每个请求都会跑: 在这里查库、发网络请求、甚至echo任何东西,都会拖慢或污染整站。
目录结构
| 文件 | 作用 |
|---|---|
plugin.json | 清单:名字、版本、依赖、后台菜单、能力声明 |
bootstrap.php | 入口,每个请求加载,只做注册 |
install.php | 可选。点「启用」时跑一次,必须幂等 |
uninstall.php | 可选。只有用户勾选「删除数据」时才会跑 |
| 其它 | 你自己的类文件、模板、静态资源,随便放 |
最小可运行插件
<?php
// plugins/hello/plugin.json
{
"name": "hello",
"title": "打招呼",
"version": "1.0.0",
"entry": "bootstrap.php",
"requires": { "cms": ">=1.4.0" },
"admin": {
"menu": [ { "route": "hello", "label": "打招呼", "perm": "plugin.hello.manage" } ]
}
}
// plugins/hello/bootstrap.php
<?php
if (!defined('NCR_ROOT')) { exit; }
class Plug_hello
{
const PERM = 'plugin.hello.manage';
public static function boot(): void
{
// 单标签:{naicha:hello_hi name="世界"}
Template::registerTag('hello_hi', [self::class, 'tagHi']);
// 后台页:/admin/hello(路由来自上面 admin.menu 的 route)
Hook::on('admin.route.hello', [self::class, 'page']);
// 权限点:注册之后才会出现在角色权限矩阵里
Perm::register(self::PERM, '打招呼插件', '插件', false);
}
public static function tagHi(Template $t, array $a): string
{
// 返回值会被自动 HTML 转义(插件标签的安全默认值)
return '你好,' . (string)($a['name'] ?? '世界');
}
public static function page(array $p): void
{
Perm::require(self::PERM);
echo '<h1>这里是插件自己的后台页</h1>';
}
}
Plug_hello::boot();
把这两个文件放进 plugins/hello/,到后台「插件管理」点启用,
然后随便一个模板里写 {naicha:hello_hi name="奶茶"} 就能看到输出。
后台页要把整页输出完。路由命中后由插件自己负责页面:
想复用后台的顶栏侧栏,就渲染一个模板文件,里面用
{naicha:include file="header.html"}。
外壳要用到 adminBase / perms / nav 这几个变量,
插件页得自己补上 —— 少一个,顶栏里的链接就是空的。
生命周期
| 动作 | 发生什么 |
|---|---|
| 安装 | 上传 zip → 解压到 plugins/<name>/ → 写入注册表(默认未启用) |
| 启用 | 校验依赖 → 跑 install.php → 标记为启用 → 清模板缓存 |
| 停用 | 只标记停用,不删任何数据,也不跑卸载脚本 |
| 卸载 | 先跑 uninstall.php(仅当勾选删数据)→ 再删目录与注册表记录 |
出错了会怎样(读一遍,能省很多时间)
- 普通异常:每个插件入口与每个钩子回调都被
try/catch包住,一个插件抛错不会影响别人。 - 致命错误(语法错、内存耗尽):PHP 里 没法 catch。 所以内核给每个插件挂了关机钩子,一旦发现是这个插件把进程搞死的,就记一次崩溃。
- 连续崩两次自动停用:目录还在、后台里能看到它,但不再加载 —— 让站点先活下来。
- 安全模式活门:万一连后台都进不去,访问
站点地址/?nc_safe=安全词可以一次性跳过所有插件进后台停用它。 安全词在后台「插件管理」里生成。
诚实说一句:插件与内核同进程同权限,能读数据库、读任意文件、发任意网络请求。
上面的熔断与安全模式解决的是"插件把站点搞挂了",解决不了"插件故意作恶" ——
那需要把插件跑在独立 PHP 进程池里(服务器侧的改造),不在本程序的能力范围内。
所以:只装你信得过的插件;自己写插件也别把
config/ 里的私钥往外发。
打包发布
把整个 plugins/<名字>/ 目录打包成 zip 即可,包内顶层目录名必须与插件名一致
(上传时会校验,不一致直接拒收)。骨架页面上的「打包下载」已经按这个结构打好了。