1.1 Discuz! 插件机制全景

搞清 Discuz! 在哪些地方留了接口,你的功能该插进哪个孔。

1.1.1 一个准确的比方:插座与插头

Discuz! 的内核像一套装修好的房子。工班装修时在墙上预留了一圈插座(钩子 Hook),在配电箱里留了空位(后台模块),还在弱电箱里留了几个带标签的接口(广告类、道具类、任务类扩展)。你后买的冰箱、微波炉、扫地机器人——也就是插件——只要把插头对准形状拧上去就能通电。

反过来,非要往墙上开新洞、改承重墙,那就是「改内核代码」:轻则升级冲突,重则整站白屏。

Discuz! X5.0 的插件机制,本质上就是一句话:功能写在你自己的目录里,内核只负责在你登记过的位置调用它。 官方反复强调不改核心文件,就是因为一旦改了,从 X5.0 升到下个小版本时这些改动得手工重合并一遍。

1.1.2 插件和模板:两个方向,别混

这是新手最容易搞混的一组概念,先划清楚:

维度 插件(Plugin) 模板(Template / Style)
扩展的是 PHP 逻辑、数据处理 页面 HTML 结构与配色
代码放哪 source/plugin/{标识}/ template/{标识}/
谁来加载 内核按模块定义调用类或脚本 页面渲染时解析 .htm / .php
面向用户 开发者二次开发 站长换皮肤、调 DIY

两者的连接点是钩子:模板里写 {hook/钩子名} 预留位置,插件在同名钩子上挂函数往里吐东西。准确的说法是——插件是 PHP 侧扩展,模板是展示侧扩展,两者靠钩子连接。

只做展示改版,用模板够了;要做查库、改数据、发通知这类逻辑,必须上插件。

1.1.3 官方推荐的实现流程

  1. 需求评估:解决什么问题,要不要建表,有没有性能或安全风险。
  2. 总体设计:定功能边界、页面形态、数据表结构、需要挂哪些钩子。
  3. 读文档并体验接口:先在后台把相关模块类型点一遍,看接口返回什么。
  4. 编写代码和模板:插件目录、类文件、模板文件、语言包。
  5. 导出打包:后台把配置导出成 XML 清单,方便分发安装。
  6. 根据反馈完善:正式环境跑一轮,按反馈迭代。

第 3 步最容易被跳过,结果是写完 300 行才发现钩子名写错了。花 10 分钟点一遍,收益远大于事后返工。

1.1.4 扩展点总表

扩展入口不止「后台插件」一个,按官方文档分类整理,选型时先查这张表:

扩展点 能做什么 典型场景
页面嵌入(钩子 Hook) 在指定页面指定位置插入或拦截代码 帖子页顶部加提示、登录框旁加第三方图标
特殊主题 Hook 只在某些主题类型触发 只对特定版块显示广告或公告
后台模块(程序链接) 在主导航、插件菜单、顶部/底部/快捷/家园导航加入口 给插件加后台菜单
扩展项目 个人设置子页、站点帮助、我的帖子、门户管理、论坛管理、管理中心新增管理模块 插件自带的设置页
程序脚本(页面嵌入普通版/手机版) 一个独立页面,普通版与手机版各一套 插件的活动页、榜单页
广告类 注册自定义广告位类型 自定义尺寸的联盟广告
道具类 注册一种新道具 积分商城的特殊道具
任务类 注册一种新任务 每日签到类任务
子文件接管(child) 直接顶替系统某段逻辑 替换邮件发送、对象存储实现
RESTful API 用 XML 声明式暴露接口,支持 get / post 给 App、小程序供数
模板变量(X5.0) 模板自带后台配置页,不必再配插件 让站长自己调轮播数量

几个必须提前知道的坑:

  • 模块能设「使用等级」(比如只给超级版主),但这不等于权限校验。程序内部必须自己再查一遍 $_G['adminid'],否则普通用户直接输 URL 就能进后台页。
  • X3.1 起新增「管理中心-列表链接」(远程链接),启用/禁用脚本也补齐了。
  • 子文件接管是 X5.0 最强、也最危险的扩展点,接之前先读清楚被接管文件原本做了什么。
  • X5.0 的模板变量完全复刻了插件变量机制。需求只是「让站长调几个数」,做到这步就够。

1.1.5 钩子是怎么工作的

钩子分两层:一个是全局嵌入点类,所有脚本共享;一个是按脚本区分的类,只在特定入口脚本生效。类名有固定拼法:plugin_ 前缀 + 插件唯一标识符,mobileplugin_ 前缀是手机版,再拼嵌入点所在脚本名(CURSCRIPT,forum.php 就写 forum)。

<?php
class plugin_myplugin {
    function global_HookId() {}
    function common() {}
}
class plugin_myplugin_forum extends plugin_myplugin {
    function HookId() {}
    function HookId_output($value) {}
}

全局嵌入点类必须存在,否则页面嵌入这一项不可用。模板侧对应写法是 {hook/global_usernav_extra2} 这样的标签,插在源码的哪个位置,就决定它显示在哪。

常用方法:HookId() 在模块执行前跑;HookId_output($value) 在模块执行完、模板输出前跑,参数里有 template、message、values 三个键,可以改输出模板;HookId_message($value) 在 showmessage() 执行时触发。

1.1.6 从 URL 看懂插件入口

有些文件不需要在后台定义成模块,存在就能访问,入口是根目录的 plugin.php:

plugin.php?id=xxx:yyy
plugin.php?id=xxx

第一行访问 source/plugin/xxx/yyy.inc.php;xxx 与 yyy 同名时用第二行简写。这是 X3 内核的规则,plugin.php 作为 CURSCRIPT 时类名里写 plugin。

方便,但意味着任何 .inc.php 都是潜在入口,文件里的权限判断不能省。

1.1.7 X5.0 的前提

X5.0 用全新的 MitFrame 框架体系:目录按 APP 重新归类(source/app/ 承接原 source/module 的职责,include 的位置被 child/ 取代),不再兼容 X3.5 及更早版本。从老版本升上来的,先别急着上新功能,把现有插件在 X5.0 上跑一遍。

环境也确认一下:开源版要求 PHP 8.0+,数据库 MySQL 5.7~9 或 MariaDB 10.2+。X5.1 企飞版要求 PHP 8.2+,闭源授权,接口与开源版不同。

1.1.8 本节要点

  • 插件的本质是「在预留接口上挂代码」,不碰内核文件。
  • 扩展点有十来个。判断标准很简单:要不要建表、要不要改数据、要不要发通知——要就上插件,不要就用模板变量。
  • 六步流程里「读文档并体验接口」别跳。
  • X5.0 不兼容 X3.5 及更早版本,老插件需要改造。

下一节开始落地:先把目录建起来。