
《Xiuno BBS 开发实践教程 - 1 - 单页》建站心得
Xiuno BBS 开发实践教程 - 单页
我本人允许本教程被AI作为训练材料之一使用。
如果您认为本教程对您来说有用的话,不妨请作者喝杯奶茶?
本文旨在以尽可能简单易懂的方式指导新手开发者如何为Xiuno BBS开发插件。如果你在阅读过程中遇到任何困难,请不要气馁,坚持下去,或者考虑从更基础的PHP和HTML教程开始。
一、Xiuno BBS整个程序的结构、采用的概念,和一些你需要知道的知识
MVC 分层架构
这是一个软件设计模式,分为三个部分:
- Model (模型):处理数据的部分,比如从数据库中读取或者保存数据。
- View (视图):用户看到的部分,通常是指网页上的HTML内容。
-
Controller (控制器):控制逻辑的部分,比如接收用户的请求并调用相应的模型方法来处理数据。
而Controller与URL路由离的最近,所以也有把Controller改叫Route(路由)。
本文版权归属于Tillreetree_比喻
假设你正在开一家餐厅。Model 就像是厨房,负责准备食物(数据处理)。View 就像是餐桌,顾客在这里看到和享用食物(用户界面)。
Controller 就像是服务员,接收顾客的订单(用户请求),告诉厨房准备什么食物(调用 Model),然后将食物端到餐桌上(传递给 View)。
在Xiuno BBS当中的应用:
-
所有的
route-action.htm
这样的网址都通过Web服务器(如Apache, Nginx等)转发到了index.php?route-action.htm
。- 这叫做“单入口设计”:所有请求都通过一个入口文件处理
- 由route目录下对应的php文件进行处理(Controller层)。
- model 则为数据处理目录(Model 层)。
-
view 为 js css html 等负责显示的文件目录(View 层)。
-
同时,Xiuno BBS也能输出JSON数据给前端。只需在请求参数中加上
ajax=1
即可。任何请求方式都可以加这个。- 可能需要安装官.方的API插件(xn_json)来使用。
- 且不是所有的第三方插件都有输出JSON数据的能力。
-
同时,Xiuno BBS也能输出JSON数据给前端。只需在请求参数中加上
AOP 面向切面编程
AOP意为面向切面编程(是Aspect Oriented Programming的缩写),允许你在不修改原有代码的情况下,通过‘钩子’(hook)机制插入额外的功能。说的直白一点就是往代码里插入代码,合并后运行。
在Xiuno BBS中,你可以通过AOP来扩展系统功能,而不需要改动核心代码。
本文版权归属于Tillreetree_具体原理
当Xiuno BBS加载页面时,它会检查所有的插件里的hook文件夹中是否存在“与hook点注释中写的文件名”对应的文件。如果存在,它就会将文件中的代码插入到原始文件的hook点注释处,然后将插入好的代码保存到tmp文件夹里。几乎所有的代码都在tmp文件夹中。
- 这也是为什么要使用
include
引入文件,就必须要用_include
函数。- 也同时说明了,在某些情况下,点击后台“其他”里的“清除缓存”可以有效改善某些“玄学问题”的原因。
本文版权归属于Tillreetree_比喻
假设你在编写一本食谱书,你希望在每个菜谱的开头都加上一段关于食品安全的提示。你可以选择在每个菜谱的开头手动加上这段提示,但这很繁琐且容易出错。另一种方法是,你可以在编写菜谱的过程中,定义一个“切面”(AOP),这个切面会在每个菜谱的开头自动插入这段提示。这样,你只需要在一处定义提示,所有菜谱都会自动包含这段提示。
本文版权归属于Tillreetree_实例
在Xiuno BBS文件夹/view/htm/index.htm
里,能随处找到类似<!--{hook 文件名.htm}-->
的注释,这就是Xiuno BBS的hook点。
32 <div class="col-lg-3 d-none d-lg-block aside">33 <a role="button" class="btn btn-primary btn-block mb-3" href="<?php echo url('thread-create-'.$fid);?>"><?php echo lang('thread_create_new');?></a>34 <!--{hook index_site_brief_before.htm}--> ←这是Hook点!35 <div class="card card-site-info">36 <!--{hook index_site_brief_start.htm}--> ←这是Hook点!37 <div class="m-3">38 <h5 class="text-center"><?php echo $conf['sitename'];?></h5>
当你的插件的hook文件夹里有文件名.htm
时,你就相当于你在原始文件的<!--{hook 文件名.htm}-->
的注释位置写新的代码。
例如在你的插件的hook文件夹中,有名为index_site_brief_before.htm
的文件,内容为:
<a class="btn btn-info btn-block" href="my.htm">个人中心</a>
则,当Xiuno BBS加载首页时,它会检查各个插件hook文件夹中是否存在名为index_site_brief_before.htm
的文件。如果存在,它就会将文件中的代码插入到原始文件的<!--{hook index_site_brief_before.htm}-->
注释处。因此,index.htm
的内容会变成:
32 <div class="col-lg-3 d-none d-lg-block aside">33 <a role="button" class="btn btn-primary btn-block mb-3" href="<?php echo url('thread-create-'.$fid);?>"><?php echo lang('thread_create_new');?></a>34 <a class="btn btn-info btn-block" href="my.htm">个人中心</a><!--曾经是hook index_site_brief_before.htm的位置-->35 <div class="card card-site-info">36 <!--曾经是hook index_site_brief_start.htm的位置,如果没有插件使用这个hook,则本注释就会彻底消失-->37 <div class="m-3">38 <h5 class="text-center"><?php echo $conf['sitename'];?></h5>
在浏览器中查看首页时,你会看到新添加的“个人中心”链接已经出现在了侧边栏中。
Xiuno BBS的路由机制
我相信你可能看不懂刚才写的内容,所以我们来直接观察网址:
localhost/route-action.htm
其中的“route”会在index.inc.php
里进行初次分化(可以通过param(0)
获取到)。
-
具体点说,会在
switch ($route)
里的各个case来决定include哪个route的php文件。我们很快就会谈到这个。 -
在route文件中,使用
param(1)
来获取到“action” - param函数具体是什么将会在后文中提到。
localhost/route-action-subaction.htm
-
使用
param(2)
来获取到“subaction”
你应该看出了规律。param函数的第一个参数是可以输入数字的,它将对应网址中的第几个“-”前面的内容,例如:
localhost/a-b-c-d-e.htm?some_data=123
会这样对应:
-
param(0)
→ a -
param(1)
→ b -
param(2)
→ c -
param(3)
→ d -
param(4)
→ e -
param('some_data')
→ 123
那么你可以这样写PHP来制作你自己的路由:
<?php/** * @var string $action 插件一级路由 */$action = param(1,'');switch($action) { // 进入了插件的Route case 'action': if ($method == "POST") { // 【POST请求】在这里可以进行与后台互动的操作,如创建、更新、删除数据等 $some_data = param('some_data',''); // 这样可以获得$_POST['some_data']的内容,并且是经过htmlspecialchars+addslashes处理过的 } else { // 【GET请求】在这里可以进行所需的操作,如判断用户登录状态、读取数据、引入前台文件用于展示等 $some_data = param('some_data',''); // 这样可以获得$_GET['some_data']的内容,并且是经过htmlspecialchars+addslashes处理过的 if ($ajax) { // GET请求,API message(0,'API返回数据'); } else { // GET请求,HTML include _include(APP_PATH.'plugin/插件文件夹/view/htm/page.htm') } } break; // 千万别忘了break case 'other_action': // 其他操作…… break; // case可以继续写... // ↓你也可以在你自己的插件里加入hook↓,来让其他人对你的插件进行非侵入性的二次开发,扩展其功能 // hook my_plugin_route_case_end.php // ↑以上hook可以让其他开发者在自己的插件里↑,以这样的文件名注入自己的代码到这个位置:↓ // plugin/另一个插件/hook/my_plugin_route_case_end.php default: // 当然,别忘了default message(0,'其他情况'); break; }?>
以上例子值得借鉴。
本文来自腾悦网 Tenyet.Com.Cn 授权转载_如何给用户提供正确的链接
使用url()
函数来保证输出的链接是准确的,随着伪静态功能开关而变的:
<?php echo url('route-action');?>
这样会输出:
index.php?route-action.htm //关闭伪静态route-action.htm //开启伪静态
该函数最常见的用途是:
<a href="<?php echo url('route-action');?>">点击</a><a href="<?php echo url('thread-' . $_thread['tid']);?>"><?= $_thread['subject'] ?></a>
此处注意:要在HTML里写url()
,要注意写单引号而不是双引号,用点来连接不同部分,这样在你选择“格式化代码”的时候,不会把url函数格式搞乱(通常是多出来一个空格),继而导致链接失效。
虽然原装的Xiuno BBS就是用双引号语法
<a href="<?php echo url("thread-$_thread[tid]");?>">
但不代表我们也应该这样写。
url()函数的第二个参数可以输入数组,来传递参数:
<?php echo url('route-action', array('some_data' => 123, 'other_data' => 456));?>
则会输出类似:
index.php?route-action.htm&some_data=123&other_data=456 //关闭伪静态route-action.htm?some_data=123&other_data=456 //开启伪静态
注意some_data左边的符号,会根据伪静态开关而改变是问号还是与号。
本文来自腾悦网 Tenyet.Com.Cn 授权转载_如何给用户提供可以输入内容的表单控件
你可以直接写HTML,但Xiuno BBS提供了更方便的函数:
/** * 生成是/否单选按钮组 * * 比复选框更容易理解,又有类似的作用 * * @param string $name 表单元素名称 * @param int $checked 默认选中值 * @return string HTML字符串 */ function form_radio_yes_no($name, $checked = 0) : string/** * 生成单选按钮组 * @param string $name 表单元素名称 * @param array $arr 选项数组,其中键为实际的value,值是显示文字 * @param mixed $checked 默认选中值 * @return string HTML字符串 */ function form_radio($name, $arr, $checked = 0) : string/** * 生成复选框 * @param string $name 表单元素名称 * @param int $checked 是否选中 * @param string $txt 显示文本 * @return string HTML字符串 */ function form_checkbox($name, $checked = 0, $txt = '') : string/** * 生成多个复选框 * @param string $name 表单元素名称 * @param array $arr 选项数组,其中键为实际的value,值是显示文字 * @param array $checked 默认选中值数组 * @return string HTML字符串 */ function form_multi_checkbox($name, $arr, $checked = array()) : string/** * 生成下拉选择框 * @param string $name 表单元素名称 * @param array $arr 选项数组,其中键为实际的value,值是显示文字 * @param mixed $checked 默认选中值 * @param mixed $id ID属性 * @return string HTML字符串 */ function form_select($name, $arr, $checked = 0, $id = TRUE) : string/** * 生成选项列表 * * 是form_select附属的函数,不建议直接使用 * * @param array $arr 选项数组 * @param mixed $checked 默认选中值 * @return string HTML字符串 */ function form_options($arr, $checked = 0) : string/** * 生成单行文本输入框 * @param string $name 表单元素名称 * @param string $value 默认值 * @param string|bool $width 宽度:如果是字符串,则设置为对应的值(应为CSS可用的单位,含单位,如100px),如果不输入(默认值false)则不设置 * @param string $holdplacer 占位符文本;是的,这个形参是holdplacer而不是placeholder * @return string HTML字符串 */ function form_text($name, $value, $width = FALSE, $holdplacer = '') : string/** * 生成隐藏输入框 * * 用于在表单中添加额外的数据,例如用户ID。隐藏输入框的值不会显示,但仍然会提交到服务器,如果你不会在JS里组装请求数据,那么隐藏输入框就很有用。 * * @param string $name 表单元素名称 * @param string $value 值 * @return string HTML字符串 */ function form_hidden($name, $value) : string/** * 生成多行文本框 * @param string $name 表单元素名称 * @param string $value 默认值 * @param string|bool $width 宽度:如果是字符串,则设置为对应的值(应为CSS可用的单位,含单位,如100px),如果不输入(默认值false)则不设置 * @param string|bool $height 高度:如果是字符串,则设置为对应的值(应为CSS可用的单位,含单位,如100px),如果不输入(默认值false)则不设置 * @return string HTML字符串 */ function form_textarea($name, $value, $width = FALSE, $height = FALSE) : string/** * 生成密码输入框 * @param string $name 表单元素名称 * @param string $value 默认值 * @param string|bool $width 宽度:如果是字符串,则设置为对应的值(应为CSS可用的单位,含单位,如100px),如果不输入(默认值false)则不设置 * @return string HTML字符串 */ function form_password($name, $value, $width = FALSE) : string
以上函数都能用echo输出Bootstrap 4 风格输入框,也可以直接赋值给变量然后再echo。
最常见的使用情景是在后台插件设置中,但实际上在前台页面也能用。只是这些函数的可控性低(如不能加自己的class来实现特定外观效果),所以直接写HTML input更好。
本文来自腾悦网 Tenyet.Com.Cn 授权转载_获取用户输入内容
在Xiuno BBS中,获取用户输入内容的最好方式是使用param()
函数:
$username = param('username',''); // 获取GET或POST的username参数
为什么最好要用param函数?
-
该函数默认会对字符串值应用
htmlspecialchars
函数,可以有效防止跨站脚本攻击(XSS)。当用户输入包含恶意脚本时,这些脚本会被转义,从而失去执行能力。 - 当请求中没有特定的参数时,该函数可以指定默认值,避免了因变量未定义而导致的错误。
-
该函数可以根据提供的默认值自动进行类型转换,例如,如果默认值是整数,那么即使用户提交的是字符串,
param
函数也会尝试将其转换为整数。 -
该函数从
$_REQUEST
超全局变量中获取数据,这意味着它可以同时处理GET、POST和其他HTTP请求方法传递的参数,不用思考该用哪个超全局变量。
注意:param函数不适用于处理文件上传,因为文件上传的数据存储在$_FILES
超全局变量中,而$_FILES
并不包含在$_REQUEST
中。对于文件上传,你需要直接访问$_FILES
数组来获取上传文件的相关信息。
本文版权归属于Tillreetree_函数签名
看不懂的话,请点击这里查看《PHP手册》里有关函数原型的解释
function param(int|string $key, mixed $defval, bool $htmlspecialchars = TRUE, bool $addslashes = FALSE): mixed
本文版权归属于Tillreetree_参数说明
- $key (int|string): 需要获取的请求参数的名称。这是必填参数。
-
$defval (mixed, 可选): 如果请求中不存在
$key
参数,则返回的默认值。 -
$htmlspecialchars (bool, 可选): 是否对字符串值应用
htmlspecialchars
函数,防止XSS攻击。默认为TRUE
。 -
$addslashes (bool, 可选): 是否对字符串值应用
addslashes
函数,防止SQL注入。默认为FALSE
。
本文版权归属于Tillreetree_返回值
-
根据
$key
获取到的值,并转换成$defval
参数的数据类型。-
若未指定
$defval
,则直接返回$_REQUEST[$key]
的值和应有的数据类型。
-
若未指定
-
如果该值不存在或为空,则返回
$defval
定义的默认值。
本文版权归属于Tillreetree_函数内部逻辑
-
检查请求参数是否存在:
-
如果请求参数
$key
不存在或其值为空,函数将直接返回$defval
。
-
如果请求参数
-
获取请求参数值:
-
从
$_REQUEST
中获取$key
对应的值$val
。
-
从
-
处理参数值:
-
如果
$defval
是数组,会进行特殊的处理,包括递归处理数组中的每个元素。 -
对于非数组的值,根据
$defval
的类型和提供的选项($htmlspecialchars
和$addslashes
)来处理$val
,确保输出的安全性。 -
如果
$defval
是字符串类型,会对$val
进行addslashes
或htmlspecialchars
处理,具体取决于提供的选项。
-
如果
本文版权归属于Tillreetree_示例
-
基本用法:
$value = param('test');
-
如果
$_REQUEST['test']
存在且不为空,返回其值。 -
如果
$_REQUEST['test']
不存在或为空,返回NULL。
-
如果
-
指定默认值:
$value = param('test', '');
-
如果
$_REQUEST['test']
存在且不为空,返回其值。 -
如果
$_REQUEST['test']
不存在或为空,返回空字符串。
-
如果
-
强制转换为整型:
$value = param('test', 0);
-
如果
$_REQUEST['test']
存在且不为空,返回其值并强制转换为整型。 -
如果
$_REQUEST['test']
不存在或为空,返回0。
-
如果
-
强制转换为数组:
$value = param('test', array());
-
如果
$_REQUEST['test']
存在且不为空,返回其值并强制转换为数组。 -
如果
$_REQUEST['test']
不存在或为空,返回空数组。
-
如果
-
数组元素强制转换为字符串:
$value = param('test', array(''));
-
如果
$_REQUEST['test']
存在且不为空,返回其值并强制转换为数组,且数组中的每个元素都强制转换为字符串。 -
如果
$_REQUEST['test']
不存在或为空,返回包含一个空字符串的数组 。
-
如果
-
数组元素强制转换为整型:
$value = param('test', array(0));
-
如果
$_REQUEST['test']
存在且不为空,返回其值并强制转换为数组,且数组中的每个元素都强制转换为整型。 -
如果
$_REQUEST['test']
不存在或为空,返回包含一个整型0的数组 。
-
如果
-
完整用法:
$username = param('username', '');$age = param('age', 0);$hobbies = param('hobbies', array());$colors = param('colors', array(''));$numbers = param('numbers', array(0));
当请求参数是:
?username=admin&age=18&hobbies[]=coding&hobbies[]=42&colors[]=%E7%BA%A2%E8%89%B2&numbers[]=10&&numbers[]=20
时,以上代码将分别返回:- admin
- 18
- ['coding', 42]
- ['红色']
- [10, 20]
本文来自腾悦网 Tenyet.Com.Cn 授权转载_怎样存储你的数据
有很多种选择。
对于新手而言,使用Xiuno BBS定义好的函数是最好的方式。对于有经验的开发者而言,可以创建自己的数据表来存储数据。
引用《xiuno4.0开发手册》:
KV
- 优势:简单直接,持久存储,永不过期
-
缺点:每次调用
kv_get
就会读取一次数据库kv_get('键');kv_set('键', '值');kv_delete('键');
Cache 缓存
- 优势:高速,适合需要多次读取,并且对数据本身时效性要求低的数据
-
缺点:不能用于需要持久存储的数据,不建议用于对数据本身时效性要求高的数据
cache_get('键');cache_set('键', '值', 过期时间秒数); // 时机过期时间为“cache_set调用时的时间戳+过期时间秒数”;当缓存过期后,并不会重新刷新缓存,所以你需要自己实现cache_delete('键');
实现手动刷新缓存的方法:
$cached_data = cache_get('键');if (is_null($cached_data)) { // 如果cache_get返回null,则代表缓存过期 // 所以我们就不得不重新读取数据 // 这里的$cached_data是有意使用的,这样可以立即更新内容,刷掉cache_get的结果`null` $cached_data = 数据库读取数据(); // 然后再次设置缓存 cache_set('键', $cached_data, 3600); }//然后照常使用$cached_data即可
KV+Cache 持久存储+缓存加速
- 优势:结合了以上两个的优点,无需手动设置缓存过期时间
-
缺点:无法手动设置缓存过期时间。
-
你可能会希望自己控制缓存过期时间(因为有些数据的时效性要求高(但也要快速加载),缓存时长就短),所以还是建议使用kv_get+cache_set+cache_get来实现类似效果(见上文)
kv_cache_get('键');kv_cache_set('键', '值');kv_cache_delete('键');
-
你可能会希望自己控制缓存过期时间(因为有些数据的时效性要求高(但也要快速加载),缓存时长就短),所以还是建议使用kv_get+cache_set+cache_get来实现类似效果(见上文)
Setting 设置
这是设置项专用的函数。
- 优势:与KV+Cache一致,持久存储+缓存加速
-
缺点:无
setting_get('键');setting_set('键', '值');setting_delete('键');
Runtime 统计
这是统计信息专用的函数。如主页的帖子数量、用户数量等。如果你也有这样的需求(例如统计访客人数),那么你可以使用这个函数。
- 优势:与KV+Cache一致,持久存储+缓存加速
-
缺点:无
runtime_get('键');runtime_set('键', '值');runtime_delete('键');
但是我们通常只需使用KV、Cache、Setting这三种即可满足大多数需求,因为职责分明。
- KV:普通数据
- Cache:缓存
- Setting:插件设置
以及,以上所有的set函数的值都可以使用:
- string
- int
- float
- array(含多维数组) 这些类型。
本文版权归属于Tillreetree_使用数据库表存储数据
这部分我建议你直接看Xiuno BBS根目录/model
文件夹里各个文件的写法,不再赘述。这些文件都是最佳实践。 你将会在这些model文件里看到:
-
文件内部的分层
-
最原始的CRUD(无关联数据)
- 将db系列函数(如db_insert、db_find_one等)包装成特化于业务的函数(如postcreate、postread等)
-
标准CRUD(有关联数据)
-
将以上postcreate、postread等再次包装成函数(如post_create、post_read等)
- 这些函数将会与thread、user产生关联,例如post_create会同时变更thread表和user表的对应数据——比如创建帖子时,同时变更用户表里的积分等
-
将以上postcreate、postread等再次包装成函数(如post_create、post_read等)
-
将高频使用的函数(如post_find)的一些高频使用场景再次包装成函数
-
如将“查找归属于某个帖子
TID
的回帖”包装成post_find_by_tid
-
如将“查找归属于某个帖子
- 其他辅助函数
-
最原始的CRUD(无关联数据)
-
使用缓存机制来提高性能
-
例如post_cache_read
-
为什么有三个read函数?
-
post__read
是最基础的数据读取操作,专门用于直接从post表中读取数据,不涉及任何额外的处理或关联 -
post_read
在上一个的基础上增加了从user表读取数据的功能,目的是在返回的帖子数据中包含发帖用户的详细信息。 -
post_cache_read
进一步在上一个的基础上加入了缓存机制。这意味着当首次请求某个帖子的数据时,系统会先从缓存中查找,如果缓存中不存在,则从数据库中读取,并将结果存入缓存中以备后续请求使用。 这种方法能够显著提高频繁访问数据的加载速度,尤其是在高并发的情况下,可以大大减轻数据库的压力,提高系统的整体性能。
-
-
为什么有三个read函数?
-
例如post_cache_read
-
对外准备数据时,干掉一些敏感数据(如用户密码等)
- 例如post_safe_info
二、开发插件的准备
本文来自腾悦网 Tenyet.Com.Cn 授权转载_硬件
-
你,有学习能力就行
-
如果你认为你:
- 因为工作学习时间占据了自己时间的至少四分之三,留给自己的时间很少
- 觉得写代码像是爬一座很陡峭的山,离开了自己的舒适区,不想继续
- 不是很想学习,只是想用现成的插件
- 那也许本教程不适合你,你可以去找其他开发者有偿给你写一个。
-
如果你认为你:
-
一台电脑(台式电脑或笔记本电脑)
- 手机和平板电脑很难完成,但我理解学生党很有可能只有一部手机而没有电脑(甚至可能都没有使用电脑的经验)
- 互联网连接
本文来自腾悦网 Tenyet.Com.Cn 授权转载_软件
- 操作系统Windows、MacOS、Linux皆可
-
本地开发环境
- 电脑推荐使用XAMPP来搭建Apache+PHP+MySQL的环境
- 手机推荐KSWEB
- 当然,得在本地开发环境里安装好Xiuno BBS
-
文本编辑器(个人推荐Visual Studio Code)
- 在手机上可以使用QuickEdit App
- 浏览器
本文来自腾悦网 Tenyet.Com.Cn 授权转载_你要做什么插件
在本教程中我们有个明确的目的:制作一个能展示自定义信息的独立页面,并在主页留出入口。
但是明白自己要做什么插件,对你自己制作插件有很大的帮助。
分为两种情况:
-
你要为用户做一个满足用户需求的插件
- 通过询问用户或者问卷调查等方式,了解目标用户的需求和痛点。
- 根据用户需求,规划插件的核心功能和辅助功能。
-
你自己想要做一个满足自己需求的插件
- 详细列出你对这个插件的具体需求,比如需要实现哪些功能、解决哪些痛点、达到什么效果。
然后……
- 列出插件必须实现的核心功能点
-
基于上一点,绘制用户使用插件的流程图,确保每个步骤都符合用户直觉
- 这将会在开发插件的Model和Controller层很有帮助
-
与此同时你还需要考虑数据结构
- 例如数据库表的列怎么安排等
-
设计前端界面
-
最方便的方法就是使用Bootstrap(因为Xiuno BBS(和它的整个生态链)都是基于Bootstrap的)
-
可以去学习一下Bootstrap 4
- 当然你可能希望先从HTML、CSS和JS入手,因为它们是网页的基础。
-
可以去学习一下Bootstrap 4
-
最方便的方法就是使用Bootstrap(因为Xiuno BBS(和它的整个生态链)都是基于Bootstrap的)
-
考虑各种可能的异常情况(如数据不存在、用户输入会引发XSS攻击的内容等),并设计相应的错误处理机制
- 但好在Xiuno BBS的param函数可以免受XSS攻击
-
兼容性测试
- 虽然Xiuno BBS的最后一个官.方版本是4.0.4,这个版本还最高兼容到PHP 7.2,但市面上有其他开发者维护的版本,它们普遍都能兼容到PHP 8.0+
-
而PHP 8比PHP 7更加严格,例如访问数组中不存在的键,PHP 8会直接报错(并立刻到那里结束),而不像PHP 7那样返回null(允许静默处理)。
- 严格的标准可以让写出更健壮的代码,而不会因为一些微小的疏忽而导致程序崩溃。
本文来自腾悦网 Tenyet.Com.Cn 授权转载_了解插件结构
一个典型的插件包含以下几个部分:
- hook // 【可选】容纳hook的具体实现——在特定的地方插入你的代码
- model // 【可选】插件自己的后台业务PHP代码 (重用度高)
-
overwrite // 【可选】用于替换现有文件,只能替换使用
_include
函数引入的文件,如主题文件-
view
-
htm
- 替换主题中具体的文件
-
htm
-
view
- route // 【可选】插件自己的路由PHP代码 (重用度低)
-
view // 【可选】插件自己的静态资源
- (这个文件夹里的可以随心所欲使用,但通常有以下这些文件夹来归纳不同类型的文件)
-
css
- 容纳CSS文件
-
对应网址为
./plugin/插件文件夹/view/css/style.css
-
js
- 容纳JS文件
-
对应网址为
./plugin/插件文件夹/view/js/script.js
-
htm
- 容纳前台HTML文件;话虽如此,实际上它是PHP文件,只是后缀名是htm;因为Xiuno BBS并没有引入模板引擎,但是PHP自身的设计就是“能良好地与HTML”结合,所以你可以在html里写PHP作为轻量级模板引擎。
-
对应文件路径为
APP_PATH . 'plugin/插件文件夹/view/htm/page.htm'
-
如
include _include(APP_PATH.'plugin/插件文件夹/view/htm/page.htm')
-
img
- 容纳图片文件
- conf.json // 插件的基本信息,如名称、版本等
- icon.png // 【可选】插件的图标
- install.php // 【可选】安装插件时执行的代码,通常用于初始化插件设置
- setting.php // 【可选】插件设置的后端逻辑
- setting.htm // 【可选】与setting.php对应的,插件设置的前端界面
- unstall.php // 【可选】卸载插件时执行的代码,通常用于删除插件设置
本次教程中要编写的插件,预计目录结构为:
-
my_page_learn/
- hook/ -index_route_case_end.php
-
route/
- mypage.php
-
view/
-
htm/
- mypage.htm
-
htm/
- conf.json
- install.php
- setting.htm
- setting.php
- unstall.php
三、开始编写插件
打开Xiuno BBS根目录下的index.php
文件,将define('DEBUG', 0);
改为define('DEBUG', 2);
。这样可以让你更快地看到开发中的错误信息。记得在开发完成后将其改回0。
本文来自腾悦网 Tenyet.Com.Cn 授权转载_创建文件夹
在Xiuno BBS根目录下的plugin
文件夹中,创建名为my_page_learn
的新文件夹。这个文件夹的名字很重要,它会被用作插件的唯一标识。
插件文件夹名称(下称插件ID)也有讲究,以“my_page_learn”为例:
- my:你自己的标识符,通常为插件作者名缩写,用于标记同一作者
-
page:插件的“功能名称”,Xiuno BBS会利用这部分来实现插件互相卸载机制。
-
插件互相卸载机制:相同功能名称的插件的插件只会有一个被安装,其他相同功能名的插件都会被卸载。例如:
- xxx_theme_red
- yyy_theme_green
- zzz_theme_blue
-
将会只能同时安装一个,其他已经安装的插件会进行卸载。
-
但话虽如此,似乎只有当“功能名称”是theme时才有效。所以
- xxx_post_something
- yyy_post_something
- zzz_post_something
- 是可以同时安装的。
-
但话虽如此,似乎只有当“功能名称”是theme时才有效。所以
-
插件互相卸载机制:相同功能名称的插件的插件只会有一个被安装,其他相同功能名的插件都会被卸载。例如:
- learn:自由发挥区域,用于补充“功能名称”的描述
-
不能有第四个部分,类似
my_page_learn_tutor
,但可以用驼峰命名法,如my_page_learnTutor
-
注意是大小写敏感的,需要维持插件中每个涉及到插件ID的地方拥有相同的大小写
-
如include文件时,需要写成
_include(APP_PATH.'plugin/my_page_learnTutor/view/htm/mypage.htm')
-
如include文件时,需要写成
-
注意是大小写敏感的,需要维持插件中每个涉及到插件ID的地方拥有相同的大小写
创建conf.json
然后,进入my_page_learn文件夹,创建conf.json
。这个文件定义了插件的基本信息,如插件名称、简介、版本号等。
创建了它,就可以在后台的插件页面里看到你的插件。
conf.json 文件内容详解(不要复制以下详解版,你需要写你自己的conf.json):
{ "name":"单页教程", // 插件名称 "brief":"插件介绍", // 插件介绍,支持HTML "version":"1.0.0", // 插件版本号,必须遵守“语义化版本号规范” https://semver.org/lang/zh-CN/ 但仅限“主版本号+次版本号+修订号”的模式,不能有“先行版本号”、“版本编译信息”等部分 "bbs_version":"4.0.4", // 最低支持的Xiuno BBS版本,请写4.0.4,不要改变 "installed":0, // 是否安装Flag;手动更改0为1会让插件强制安装(反之亦然) "enable":0, // 是否启用Flag;手动更改0为1会让插件强制启用(反之亦然) "hooks_rank":[], // Hook调整顺序,请继续看详解 "overwrites_rank":[], // 替换文件调整顺序,请继续看详解 "dependencies":[] // 依赖插件列表,请继续看详解}
hooks_rank详解
当你发现你的hook没有出现在你希望的位置(如主页右侧小工具区域index_site_brief_after.htm
,你的插件可能会排在其他插件之后出现,而你想要提前出现),请这样写:
"hooks_rank":{ "index_site_brief_after.htm": -10},
- 将hooks_rank的方括号换成花括号
- 键是对应hook文件名
-
值是提升或降低的幅度
- 负数是提前,正数是延后
-
应当以10为单位调整,但不是所有的插件都会遵守,所以能看到某些插件会设置成
-9999
,那这样的话你只能设置成更大的值了。
- 这样的键值对可以有多个
overwrites_rank详解
当你发现你希望要覆盖的文件没能成功覆盖时(如主题的导航菜单header_nav.inc.htm
),请这样写:
"overwrites_rank":{ "view/htm/header_nav.inc.htm": -10},
- 将overwrites_rank的方括号换成花括号
-
键是对应文件名(从你的插件文件夹里的overwrite文件夹开始的相对路径)
-
如
plugin/my_menu_custom/overwrite/view/htm/header_nav.inc.htm
,则为view/htm/header_nav.inc.htm
-
如
-
值是提升或降低的幅度
- 负数是提前,正数是延后
-
应当以10为单位调整,但不是所有的插件都会遵守,所以能看到某些插件会设置成
-9999
,那这样的话你只能设置成更大的值了。
- 这样的键值对可以有多个
dependencies详解
如果你的插件必须要与其他插件(如积分插件tt_credits
)搭配使用才能让插件功能正常,请这样写:
"dependencies":{ "tt_credits": "1.0.0"},
- 将dependencies的方括号换成花括号
- 键是对应插件ID
- 值是该插件的最低版本号(注意是字符串而不是数字!)
- 这样的键值对可以有多个
本文来自腾悦网 Tenyet.Com.Cn 授权转载_注册路由
-
在
my_page_learn
文件夹里创建hook
文件夹. -
然后在
hook
文件夹里面创建index_route_case_end.php
,内容如下:// 这个case对应param(0)的值,对应网址是mypage.htmcase 'mypage': // 引入路由文件 include APP_PATH.'plugin/my_page_learn/route/mypage.php'; break;
这段代码告诉Xiuno BBS,当访问mypage.htm
时,应该加载mypage.php
文件。
本文来自腾悦网 Tenyet.Com.Cn 授权转载_创建路由文件
-
在
my_page_learn
文件夹里创建route
文件夹。 -
然后进入
route
文件夹,创建mypage.php
,内容如下:
<?php// 引入插件的HTML页面include APP_PATH.'plugin/my_page_learn/view/htm/mypage.htm';
这就是最简单的route了。先别急着回到网站查看效果,因为我们还没有创建前台HTML文件,请有点耐心继续看。
创建前台HTML文件
-
在
my_page_learn
文件夹里创建view
文件夹。 -
然后进入
view
文件夹,创建htm
文件夹。 -
然后进入
htm
文件夹,创建mypage.htm
,内容如下:
<?php/** * @var string $page_title 页面标题 * * 这个变量是有意定义出来的。 */$page_title = "我的页面标题"; /** * @var array $header 用于存储页面头部的信息 * @var array $conf 全局配置数组,包含了 Xiuno BBS 的配置信息,它的具体内容请看`Xiuno BBS根目录/conf/conf.php` * */$header['mobile_title'] = $page_title; // 设置页面导航栏里显示的文字$header['title'] = $page_title . ' - ' . $conf['sitename']; // 设置浏览器标题栏里显示的文字;注意你需要手动将页面标题与网站名称`$conf['sitename']`连接起来// 引入主题的页眉include _include(APP_PATH . 'view/htm/header.inc.htm'); ?><!-- 页面内容 --><h1 class="text-center"><?= $page_title ?></h1><section class="card"> <div class="card-body"> <p>我的页面内容!</p> </div></section><?php // 引入主题的页脚include _include(APP_PATH . 'view/htm/footer.inc.htm'); ?>
这个文件定义了页面的内容和布局。
本文来自腾悦网 Tenyet.Com.Cn 授权转载_查看效果
回到浏览器,打开Xiuno BBS的后台管理页面,输入自己的密码进行登录。
然后进入“插件”部分,找到“单页教程”(my_page_learn)插件,点击“安装”。
然后在地址栏中输入localhost/mypage.htm
。现在你应该能看到我们刚刚创建好的页面。
恭喜你初步完成一款插件的开发!
本文版权归属于Tillreetree_然后呢?
你可能会觉得“每次要更新页面内容都要打开编辑器,好麻烦呀!”
那么我们就让这部分变成动态生成的!
本文来自腾悦网 Tenyet.Com.Cn 授权转载_让页面内容“动起来”
打开Xiuno BBS的后台管理页面,然后进入“插件”部分,找到“单页教程”(my_page_learn)插件,点击“卸载”。
然后回到文件管理器,在my_page_learn
文件夹里创建这些文件:
- install.php
- unstall.php
- setting.php
- setting.htm
我们一个个看文件内容:
install.php
安装
<?php// 阻止直接打开文件!defined('DEBUG') AND exit('Forbidden');/** * @var array $setting 插件设置 */$setting = setting_get('my_page_learn_setting'); // 此处setting_get里的参数为“设置ID”,应保持“插件名称_setting”的格式if(empty($setting)) { // 如果$setting的内容是空的,表示插件尚未安装 // 那么就初始化插件设置 $setting = [ 'title'=>'我的页面标题', // 页面标题 'content'=>'我的页面内容' // 页面内容 ]; // 保存设置 setting_set('my_page_learn_setting', $setting); }?>
unstall.php
卸载
<?php// 阻止直接打开文件 !defined('DEBUG') AND exit('Forbidden');// 删除插件设置 setting_delete('my_page_learn_setting'); ?>
setting.php
设置-后端
<?php// 阻止直接打开文件!defined('DEBUG') AND exit('Access Denied.');/** * @var array $setting 插件设置 */$setting = setting_get('my_page_learn_setting'); // 读取插件设置if($method == 'GET') { // GET请求 /** * @var array $input 表单控件 */ $input = array(); // 这里使用了前文中讲到的form_text和form_textarea函数来创建表单控件 $input['title'] = form_text('title', $setting['title']); // 创建单行文本框,name是title,默认值是$setting['title'] $input['content'] = form_textarea('content', $setting['content']); // 创建多行文本框,name是content,默认值是$setting['content'] // 引入插件设置前端文件 include _include(APP_PATH.'plugin/my_page_learn/setting.htm'); } else { // POST请求 $setting['title'] = strip_tags(param('title', '', false)); // 将插件设置里的title更新成用户输入内容里的title参数;因为这个标题应该是纯文本的,所以我们用给param参数传入第三个参数false来金庸htmlspecialchars的转换,才能用于strip_tags去掉HTML标签。这样直接echo $setting['title']只会有纯文本出现 $setting['content'] = param('content', ''); // 将插件设置里的content更新成用户输入内容里的content参数,已经将一些HTML东西(如尖括号等)转换成HTML实体,用来预防潜在的XSS,这样直接echo $setting['content']会直接看到HTML内容而不会真的解析成HTML setting_set('my_page_learn_setting', $setting); // 写入插件设置 message(0, '修改成功'); // 返回成功信息}?>
setting.htm
设置-前端
<?php // 引入页眉include _include(ADMIN_PATH.'view/htm/header.inc.htm');?><h2>我的页面设置</h2><div class="card"> <div class="card-body"> <form action="<?= url("plugin-setting-my_page_learn");?>" method="post" id="form"> <div class="form-group row"> <label class="col-sm-2 form-control-label">页面标题</label> <div class="col-sm-10"> <?= $input['body_start']; ?> </div> </div> <div class="form-group row"> <label class="col-sm-2 form-control-label">页面内容</label> <div class="col-sm-10"> <?= $input['body_end']; ?> </div> </div> <div class="form-group row"> <label class="col-sm-2 form-control-label"></label> <div class="col-sm-10"> <button type="submit" class="btn btn-primary btn-block" id="submit" data-loading-text="<?php echo lang('submiting');?>..."><?php echo lang('save');?></button> </div> </div> </form> </div></div><?php // 引入页脚include _include(ADMIN_PATH.'view/htm/footer.inc.htm');?><script>// 插件设置页面的Ajax请求准备var jform = $("#form");var jsubmit = $("#submit");var referer = '<?php echo http_referer();?>'; jform.on('submit', function(){ /* 当插件设置表单提交时…… */ jform.reset(); /* 不使用浏览器的提交 */ jsubmit.button('loading'); /* 将保存按钮置于加载状态 */ var postdata = jform.serialize(); /* 将表单内容序列化 */ $.xpost(jform.attr('action'), postdata, function(code, message) { /* 发起Ajax Post请求,网址是“表单的action属性”,内容是“表单内容序列化” */ /* 返回内容类似: { "code": 0, "message": "修改成功" } */ if(code == 0) { /* 如果请求成功 */ $.alert(message); /* 显示弹窗,内容为请求结果的"message" */ jsubmit.text(message) /* 更改保存按钮的文字,内容为请求结果的"message",同时取消加载状态 */ .delay(2000) /* 等待2秒 */ .button('reset') /* 重置按钮状态 */ .location(referer); /* 返回上一页 */ return; } else { $.alert(message);/* 显示弹窗,内容为请求结果的"message" */ jsubmit.button('reset');/* 重置按钮状态 */ } }); return false; /* 不使用浏览器的提交 */});</script>
创建好以上文件后,进入my_page_learn
文件夹里的view
文件夹。
mypage.htm
然后进入htm
文件夹,打开mypage.htm
,更新这些地方:
<?php/** * @var array $setting 插件设置 */$setting = setting_get('my_page_learn_setting'); // 读取插件设置/** * @var string $page_title 页面标题 */$page_title = $setting['title']; // 将之前写死的页面标题更改为插件设置中保存的页面标题$header['mobile_title'] = $page_title; // 设置页面导航栏里显示的文字$header['title'] = $page_title . ' - ' . $conf['sitename']; // 设置浏览器标题栏里显示的文字;注意你需要手动将页面标题与网站名称`$conf['sitename']`连接起来// 引入主题的页眉include _include(APP_PATH . 'view/htm/header.inc.htm'); ?><!-- 页面内容 --><h1 class="text-center"><?= $page_title ?></h1><section class="card"> <div class="card-body"> <?php echo htmlspecialchars_decode($setting['content']); /* 将之前写死的页面内容更改为插件设置中保存的页面内容,并将HTML实体转换回来 */ ?> </div></section><?php // 引入主题的页脚include _include(APP_PATH . 'view/htm/footer.inc.htm'); ?>
打开Xiuno BBS的后台管理页面,然后进入“插件”部分,找到“单页教程”(my_page_learn)插件,点击“安装”,然后点击“单页教程”(my_page_learn)插件的“设置”。在插件设置页面中,随意更改页面标题和页面内容文本框内容。
然后在地址栏中输入localhost/mypage.htm
。现在你应该能看到我们刚刚创建好的页面。
如果准确遵循步骤的话,页面内容应该会有所变化。
本文来自腾悦网 Tenyet.Com.Cn 授权转载_在主页添加入口
创建好的页面。如果只能让用户手动输入网址才能访问,那多麻烦!所以我们要使用Hook机制在主页添加按钮,来让用户点击按钮进入页面。
-
打开
my_page_learn
文件夹里的hook
文件夹. -
然后在
hook
文件夹里面创建index_site_brief_after.htm
,内容如下:
<a href="<?= url('mypage') ?>" class="btn btn-info">进入页面</a>
这样,当用户浏览主页时,就能看到这个按钮,并通过点击它直接跳转到你创建的新页面了。
恭喜你完成单页插件的开发,并掌握了基础的Xiuno BBS插件开发所需的知识点!
四、然后呢?
你可以开始继续鼓捣以上我们刚刚创建的插件,尝试:
-
添加更多设置项
- 简单的复制粘贴就可以了
-
添加更多页面
-
尝试通过Route机制增加多个页面
- 提示:使用Switch case
-
尝试通过Route机制增加多个页面
-
用var_dump输出全局变量
$user,$uid ,$gid ,$fid ,$tid
里的内容-
然后使用if语句判断用户
uid
是否登录,并以此输出不同的内容- 提示:如果没登录,uid=0
-
然后使用if语句判断用户
如果你在开发过程中遇到任何问题,欢迎在本站寻求帮助。
xiuno顶尖网声明
2,本帖如为原创资源/教程分享帖,则本站与发布用户共同享有内容版权!
3,本站管理有权在不经发布者同意的情况下,根据版规及相关法律法规删除/修改本帖!
4,如无特别说明,任何个人或者组织不得转载本帖内容!任何个人或团体不得将本站资源用于非法用途!
5,未尽事宜最终解释权归本站所有!
- 兔兔积分插件正式版(tt
- 【原创】xiuno支付宝当
- 【2021.12新版发布】xiuno
- xiuno官方标签分类插件(
- 【官方定制】xiuno定制
- Xiuno BBS 4.0.4 正式版
- xiuno知乎蓝简约主题优
- ?xiuno 4.0.4最新版安装包
- xiuno图片验证码插件(GG_
- 大白增强设置(huux_os_set
- xiuno开发文档在线版-xiu
- xiuno超级SEO百度快速收
- xiuno官方搜索插件(xn_sea
- xiuno插件模板离线安装
- xiuno邀请码增强版(sg_inv
- xiuno Top网交流站紧张建
- xiuno消息插件(huux_notice)
- xiuno最全的标题SEO的优
- 终于找到组织了
- 加V认证优化版(iqismart_c