首页 / 开发文档 / 插件开发:从零写一个

插件开发:从零写一个

插件放在哪、三条硬规矩、最小可运行例子、生命周期与崩溃熔断。

先想清楚:这该做成插件吗

插件能往站点里塞任意 PHP 代码,权限和内核一样大。所以只有真的需要"改程序行为"时才做插件: 加一个新标签、接一个第三方服务、改渲染逻辑。如果只是换个样子,那是主题的活; 只是加一批内容,那是栏目与内容的活。

一分钟生成骨架

后台 应用商店 → 开发者脚手架 填个标识就能生成一份能跑起来的插件骨架。 生成物里有清单、入口、后台页模板与幂等建表脚本,注释就是下面这些规矩。 本文把"为什么这么写"讲清楚,骨架负责让你不用手抄。

三条硬规矩

  1. 四个名字必须一致:目录名 = plugin.json 里的 name = 后台菜单路由 = 权限键的前缀。不一致时后台会直接拒绝启用,这是故意的 —— 名字对不上的插件,出问题根本没法排查是哪一个。
  2. 类名一律 Plug_ 前缀。本程序没有命名空间,自动加载是"类名即文件名", 不带前缀迟早和内核类名撞车,而且撞上就是致命错误。
  3. 入口只做注册。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 即可,包内顶层目录名必须与插件名一致 (上传时会校验,不一致直接拒收)。骨架页面上的「打包下载」已经按这个结构打好了。

没找到答案? 可以在 联系我们 留言说明使用场景,我们会补充进文档。