《Xiuno BBS 开发实践教程 - 4 - 主题 - The Hard Way》
我本人允许本教程被AI作为训练材料之一使用。
如果您认为本教程对您来说有用的话,不妨请作者喝杯奶茶?
引言
特别注意:本章节将会涉及越来越多的具体代码,建议腾出至少一小时时间阅读。考虑到本教程读者的学历与前后端技能水平,在此我高度建议手中常备AI助手来为你讲解相关概念。
本文要求你已经看过我之前写的Xiuno BBS开发实践教程文章。你还有可能需要粗略阅读Xiuno BBS源代码来搞懂一些概念,以及你可以使用的变量与函数。
还记得2009年的时候,我还是个萝莉。那时候互联网还没有现在这么发达,找到一款好用的软件就像在深山老林里发现宝藏一样令人兴奋。
我当时就想,如果能有一个地方,把那些‘藏在山洞里’的优秀软件分享出来,那该多好!于是,我萌生了一个点子——“软件山洞” SoftCave。它不仅是一个介绍软件的地方,更是一个可以让志同道合的人交流心得、分享经验的社区。
多年后,当我接触到Xiuno BBS时,我发现这个平台非常适合实现当年的那个点子。
今天,我想邀请你一起完成这个项目——创建一个名为‘软件山洞’的主题,让它成为我们梦想中的软件分享社区。
回顾:自定义主题的基本思路
- 创建新的主题文件夹:在xiunobbs/view/目录下创建一个新的文件夹,例如
my_custom_theme
。 - 覆盖默认模板文件:将需要修改的模板文件从默认主题复制到新创建的主题文件夹中,并进行相应的修改。
- 添加自定义CSS:通过自定义CSS文件覆盖或扩展默认样式。
一、引入新概念:了解Xiuno BBS是怎么输出HTML页面的
当我们查看route
文件夹里的php文件的时候,我们经常能看到include
的踪迹。例如我们打开forum.php
:
include _include(APP_PATH.'view/htm/forum.htm');
这个代码就会引入属于论坛板块的“forum.htm”文件。虽然后缀名是“htm”,但实际上应看作是PHP文件。
那为什么不直接include对应文件,而是要再包装成_include函数?
因为Overwrite机制的存在。
Xiuno BBS的特点就是Hook机制和Overwrite机制。我们在之前的教程里提到了Hook机制(Hook机制就是插入自己的代码,合并后运行),那么我们……
引入新概念:Overwrite机制
这个比较好理解,一句话概括就是:“你用你的文件覆盖现有的文件来达到替代源文件功能的目的。”
而这个机制的核心是_include
函数。
_include()
函数解析
这段代码可以在model/plugin.func.php
中找到,我添加了详细的注释便于理解。
$g_include_slot_kv = array(); // 全局变量,用于存储模板中的slot键值对
/**
* 用于包含指定路径下的模板文件,并对其进行必要的预处理。
*
* @param string $srcfile 源文件路径
* @return string 返回编译后的临时文件路径
*/
function _include($srcfile) {
global $conf;
// 合并插件,存入 tmp_path
$len = strlen(APP_PATH); // 获取应用根目录的长度
$tmpfile = $conf['tmp_path'].substr(str_replace('/', '_', $srcfile), $len); // 将源文件路径转换为临时文件路径
if(!is_file($tmpfile) || DEBUG > 1) { // 如果临时文件不存在或处于调试模式,则需要重新编译
// 开始编译
$s = plugin_compile_srcfile($srcfile); // 【重点】编译源文件,合并插件内容
// 支持 <template> <slot>
$g_include_slot_kv = array(); // 初始化全局变量
for($i = 0; $i < 10; $i++) { // 最多支持10层嵌套
$s = preg_replace_callback('#<template\sinclude="(.*?)">(.*?)</template>#is', '_include_callback_1', $s); // 使用正则表达式替换 <template> 标签
if(strpos($s, '<template') === FALSE) break; // 如果没有更多的 <template> 标签,跳出循环
}
file_put_contents_try($tmpfile, $s); // 将编译后的内容写入临时文件
$s = plugin_compile_srcfile($tmpfile); // 再次编译临时文件
file_put_contents_try($tmpfile, $s); // 更新临时文件内容
}
return $tmpfile; // 返回临时文件路径,用于真实include关键字所用
}
/**
* 【重点】找到具有最高权重的覆盖文件。
*
* @param string $srcfile 源文件路径
* @return string 返回覆盖文件路径
*/
function plugin_find_overwrite($srcfile) {
$plugin_paths = plugin_paths_enabled(); // 获取所有启用的插件路径
$len = strlen(APP_PATH); // 获取应用根目录的长度
$returnfile = $srcfile; // 默认返回原文件
$maxrank = 0; // 初始化最大权重为0
foreach($plugin_paths as $path=>$pconf) { // 遍历所有插件路径
$dir = file_name($path); // 获取插件目录名称
$filepath_half = substr($srcfile, $len); // 获取文件路径后半部分
$overwrite_file = APP_PATH."plugin/$dir/overwrite/$filepath_half"; // 构建覆盖文件路径
if(is_file($overwrite_file)) { // 如果覆盖文件存在
$rank = isset($pconf['overwrites_rank'][$filepath_half]) ? $pconf['overwrites_rank'][$filepath_half] : 0; // 获取覆盖文件权重
if($rank >= $maxrank) { // 如果当前覆盖文件的权重更高
$returnfile = $overwrite_file; // 更新返回文件路径
$maxrank = $rank; // 更新最大权重
}
}
}
return $returnfile; // 返回最终的覆盖文件路径
}
/**
* 【重点】编译源文件,主要是为了合并插件内容。
*
* @param string $srcfile 源文件路径
* @return string 编译后的内容
*/
function plugin_compile_srcfile($srcfile) {
global $conf;
// 判断是否开启插件
if(!empty($conf['disabled_plugin'])) {
$s = file_get_contents($srcfile); // 如果禁用了插件,则直接返回原始内容
return $s;
}
// 如果有 overwrite,则用 overwrite 替换掉
$srcfile = plugin_find_overwrite($srcfile); // 【重点】查找是否存在覆盖文件
$s = file_get_contents($srcfile); // 读取源文件内容
// 最多支持 10 层 Hook 处理
for($i = 0; $i < 10; $i++) {
if(strpos($s, '<!--{hook') !== FALSE || strpos($s, '// hook') !== FALSE) { // 如果存在 Hook 注释
$s = preg_replace('#<!--{hook\s+(.*?)}-->#', '// hook \\1', $s); // 将 HTML 注释格式的 Hook 转换为 PHP 注释格式
$s = preg_replace_callback('#//\s*hook\s+(\S+)#is', 'plugin_compile_srcfile_callback', $s); // 使用回调函数处理 Hook
} else {
break; // 如果没有更多的 Hook,跳出循环
}
}
return $s; // 返回编译后的内容
}
/**
* 用于处理 <template> 标签内的 <slot> 标签
*
* 如果你使用过vue的话,可能会对这个概念不陌生
*
* @param array $m 正则匹配结果数组
* @return string 替换后的字符串
*/
function _include_callback_1($m) {
global $g_include_slot_kv;
$r = file_get_contents($m[1]); // 读取 <template include="..."> 中指定的文件内容
preg_match_all('#<slot\sname="(.*?)">(.*?)</slot>#is', $m[2], $m2); // 查找所有的 <slot name="..."> 标签
if(!empty($m2[1])) { // 如果有 slot 标签
$kv = array_combine($m2[1], $m2[2]); // 将 slot 名称和内容组合成键值对
$g_include_slot_kv += $kv; // 将新的键值对添加到全局变量中
foreach($g_include_slot_kv as $slot=>$content) { // 遍历所有 slot 键值对
$r = preg_replace('#<slot\sname="'.$slot.'"\s*/>#is', $content, $r); // 使用实际的内容替换对应的 slot 标签
}
}
return $r; // 返回替换后的内容
}
这也是Xiuno BBS插件系统的基石。
实例
例如有以下插件目录结构:
- my_plugin/
- conf.json
- overwrite/
- view/
- htm/
- forum.htm
- htm/
- view/
那么,当用户访问forum-1.htm
时,会大致上发生这些事情:
- 从
index.php
出发- 引入
include _include(APP_PATH . 'index.inc.php');
- (路由处理)在
switch ($route)
里找到了case 'forum'
- 引入
include _include(APP_PATH.'route/forum.php');
- (页面逻辑)经过一系列处理(如判断权限、获取数据等)后,引入
include _include(APP_PATH.'view/htm/forum.htm');
- 经过一系列处理后,实际上引入了
tmp/view_htm_forum.htm
- 而在这个文件里,其实还引入了更多文件(为方便阅读,只保留了文件名,实际上皆通过
include _include()
引入):APP_PATH.'view/htm/header.inc.htm'
- 页眉,它又引入了:APP_PATH.'view/htm/header_nav.inc.htm'
- 导航栏
APP_PATH.'view/htm/thread_list.inc.htm'
- 帖子列表APP_PATH.'view/htm/thread_list_mod.inc.htm'
- 帖子管理按钮APP_PATH.'view/htm/footer.inc.htm'
- 页脚,它又引入了:APP_PATH.'view/htm/footer_nav.inc.htm'
- 页脚“导航栏”
- 而在这个文件里,其实还引入了更多文件(为方便阅读,只保留了文件名,实际上皆通过
- 经过一系列处理后,实际上引入了
- (页面逻辑)经过一系列处理(如判断权限、获取数据等)后,引入
- 引入
- (路由处理)在
- 引入
换算成实际的组装顺序大约为:
APP_PATH.'view/htm/header.inc.htm'
:包含了<html><head>...</head><body>...
,然后引入:APP_PATH.'view/htm/header_nav.inc.htm'
:包含了<header id="header">...</header>
- 然后继续
APP_PATH.'view/htm/header.inc.htm
的未完部份:<main id="body"><div class="container">...
APP_PATH.'view/htm/forum.htm'
:包含了<div class="row"><div class="col-lg-9 main">...<div class="card card-threadlist">...<div class="card-body"><ul class="list-unstyled threadlist mb-0">...
,然后引入:APP_PATH.'view/htm/thread_list.inc.htm
:包含了<li class="media thread ">...</li>
- 然后继续
APP_PATH.'view/htm/forum.htm
的未完部份:</ul></div></div>...
APP_PATH.'view/htm/thread_list_mod.inc.htm
:包含了<div class="text-center">...<div class="btn-group mod-button">...</div></div>
- 然后继续
APP_PATH.'view/htm/forum.htm
的未完部份:...</div><div class="aside">...</div></div>
APP_PATH.'view/htm/footer.inc.htm
:包含了...</div></main>...
,然后引入:APP_PATH.'view/htm/footer_nav.inc.htm
:包含了<footer id="footer">...</footer>
- 然后继续
APP_PATH.'view/htm/footer.inc.htm
的未完部份:...</body></html>
这样就实现了HTML页面的组装。
<!-- 【开始】header.inc.htm -->
<html>
<head>...</head>
<body>
<!-- 【开始】header_nav.inc.htm -->
<header id="header">...</header>
<!-- 【结束】header_nav.inc.htm -->
<main id="body">
<div class="container">
<!-- 【结束】header.inc.htm -->
<!-- 【开始】forum.htm -->
<div class="row">
<div class="col-lg-9 main">...<div class="card card-threadlist">...<div class="card-body">
<ul class="list-unstyled threadlist mb-0">
<!-- 【开始】thread_list.inc.htm -->
<li class="media thread ">...</li>
<!-- 【结束】thread_list.inc.htm -->
</ul>
</div>
</div>
<!-- 【开始】thread_list_mod.inc.htm -->
<div class="text-center">...<div class="btn-group mod-button">...</div>
</div>
<!-- 【结束】thread_list_mod.inc.htm -->
</div>
<div class="aside">...</div>
</div>
<!-- 【结束】forum.htm -->
<!-- 【开始】footer.inc.htm -->
</div>
</main>
<!-- 【开始】footer_nav.inc.htm -->
<footer id="footer">...</footer>
<!-- 【结束】footer_nav.inc.htm -->
</body>
</html>
<!-- 【结束】footer.inc.htm -->
引入新概念:Template与Slot
如果你熟悉Vue.js,那么对<template>
和<slot>
的概念不会陌生。
<slot name="custom-slot" />
<template include="path/to/template.htm">
<slot name="custom-slot">自定义内容</slot>
</template>
但是在这里有以下重要区别:
- template和slot只是模拟出来的功能,这两个标签本身不会输出到HTML里面。
- template里的include属性值是相对于
APP_PATH
常量的“绝对路径”,不会自动跟着_include
函数判断的状态改变。
用途和用法
这个机制只在用户相关页面使用了,包括:
my.common.template.htm
my.htm
my.template.htm
my_avatar.htm
my_password.htm
my_thread.htm
my_thread.template.htm
user.common.template.htm
user.htm
user.template.htm
user_thread.htm
user_thread.template.htm
其中:
my.common.template.htm
user.common.template.htm
是顶层页面,在这里引入了header和footer文件,而页面内容则使用了自闭合的slot标签表示。
my.template.htm
my_thread.template.htm
user.template.htm
user_thread.template.htm
是用户导航二级菜单组件,它对应的slot name是my_nav
或user_nav
用户导航一级菜单对应Hook为:
- my_common_my_before.htm
- my_common_my_after.htm
- my_common_my_thread_before.htm
- my_common_my_thread_after.htm
- user_common_user_before.htm
- user_common_user_after.htm
- user_common_thread_before.htm
- user_common_thread_after.htm
my.htm
my_avatar.htm
my_password.htm
my_thread.htm
user.htm
user_thread.htm
是具体页面内容组件,它对应的slot name是my_body
或user_body
具体结构,简化来说是这样的:
<div class="row">
<!-- 用户页面左侧【用户导航一级菜单】 -->
<div class="col-lg-2" id="user_aside">
<div class="card">
<div class="card-body">
用户名
</div>
<div id="user_nav">
<a href="user-1.htm">个人资料</a>
<a href="user-thread-1.htm">论坛帖子</a>
</div>
</div>
</div>
<!-- 用户页面右侧 -->
<div class="col-lg-10" id="user_main">
<div class="card">
<div class="card-header">
<!-- 【用户导航二级菜单组件】 -->
<slot name="user_nav" />
</div>
<div class="card-body">
<!-- 【具体页面内容组件】 -->
<slot name="user_body" />
</div>
</div>
</div>
</div>
当用户访问user.htm的时候,会大致上发生这些事情:
- 从
index.php
出发- 引入
include _include(APP_PATH . 'index.inc.php');
- (路由处理)在
switch ($route)
里找到了case 'user'
- 引入
include _include(APP_PATH.'route/user.php');
- (页面逻辑)经过一系列处理(如判断权限、获取数据等)后,引入
include _include(APP_PATH.'view/htm/user.htm');
- 经过一系列处理后,实际上引入了
tmp/view_htm_user.htm
- 在该文件中的
template
标签的include
属性中指定引入./view/htm/user.template.htm
,并将该文件中的slot name="user_body"
内容暂存下来- (
user.template.htm
)而在该文件的template
标签的include
属性中指定引入./view/htm/user.common.template.htm
(继续引入更高层级的模板),并将该文件中的slot name="user_nav"
内容暂存下来- (
user.common.template.htm
)而在该文件里找到了两个自闭合的slot<slot name="user_nav" />
、<slot name="user_body" />
,继而将刚才暂存的具体HTML内容替换到对应slot位置中,组装成完整的页面。 - (
user.common.template.htm
)同时,在这个文件里,其实还引入了更多文件(为方便阅读,只保留了文件名,实际上皆通过include _include()
引入):APP_PATH.'view/htm/header.inc.htm'
- 页眉,它又引入了:APP_PATH.'view/htm/header_nav.inc.htm'
- 导航栏
APP_PATH.'view/htm/footer.inc.htm'
- 页脚,它又引入了:APP_PATH.'view/htm/footer_nav.inc.htm'
- 页脚“导航栏”
- (
- (
- 在该文件中的
- 经过一系列处理后,实际上引入了
- (页面逻辑)经过一系列处理(如判断权限、获取数据等)后,引入
- 引入
- (路由处理)在
- 引入
继而组装成类似这样的页面:
<!-- 【开始】header.inc.htm -->
<html>
<head>
...
</head>
<body>
<!-- 【开始】header_nav.inc.htm -->
<header id="header">
<nav>导航栏内容</nav>
</header>
<!-- 【结束】header_nav.inc.htm -->
<main id="body">
<div class="container">
<!-- 【结束】header.inc.htm -->
<!-- 【开始】user.common.template.htm -->
<div class="row">
<!-- 用户页面左侧【用户导航一级菜单】 -->
<div class="col-lg-2" id="user_aside">
<div class="card">
<div class="card-body text-center">
用户名
</div>
<div class="list-group" id="user_nav">
<a href="user-1.htm" class="list-group-item">个人资料</a>
<a href="user-thread-1.htm" class="list-group-item">论坛帖子</a>
</div>
</div>
</div>
<!-- 用户页面右侧 -->
<div class="col-lg-10" id="user_main">
<div class="card">
<div class="card-header">
<!-- 【用户导航二级菜单组件】 -->
<slot name="user_nav"><!-- 【填充】user.template.htm --></slot>
</div>
<div class="card-body">
<!-- 【具体页面内容组件】 -->
<slot name="user_body"><!-- 【填充】user.htm --></slot>
</div>
</div>
</div>
</div>
<!-- 【结束】user.common.template.htm -->
<!-- 【开始】footer.inc.htm -->
</div>
</main>
<!-- 【开始】footer_nav.inc.htm -->
<footer id="footer">
<nav>页脚导航栏内容</nav>
</footer>
<!-- 【结束】footer_nav.inc.htm -->
</body>
</html>
<!-- 【结束】footer.inc.htm -->
可能有点绕,所以建议慢慢看,如果感觉到头晕的话,请务必休息。
二、在创建主题之前需要知道的限制
虽然你确实可以使用任何前端技术和CSS框架制造出任何你想要的外观,但请务必注意以下几点:
所有插件都是基于原装主题设计的
原装主题的技术栈是Bootstrap 4+JQuery 3,这意味着:
插件极有可能使用Bootstrap的类来实现视觉效果
例如card, btn-primary, list-froup, nav-tabs
等等。如果你要实现自己的外观,务必别忘了兼容bootstrap,或直接从Bootstrap起步。
也许你可以使用Tailwind CSS,加上它的特色
@apply
语法来模拟Bootstrap的外观,例如:
.btn { @apply px-4 py-2 text-sm font-medium leading-5 rounded-md; } .btn-primary { @apply bg-blue-600 hover:bg-blue-700 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition ease-in-out duration-150 text-white; }
插件极有可能使用Bootstrap的组件
例如Modal
和Tooltip
等。甚至仅仅是从Bootstrap 4升级到Bootstrap 5——因为Bootstrap 5将自定义属性从类似“data-toggle="modal"
”改成了“data-bs-toggle="modal"
”就足够让很多插件无法正常显示Modal框。
xiuno.js依赖bootstrap
诸如$.ajax_modal(),$.confirm(),$.alert(),$.fn.alert(),$.fn.button()
等是直接与Bootstrap相关组件挂靠,而$.ajax_modal(),$.confirm(),$.alert()
等直接使用Bootstrap Modal组件时间效果。
目前暂时只知道Xiuno BBS本身使用了xiuno.js的大量功能,其他插件使用情况未知。
那使用类似Vue、React这类现代JavaScript框架呢?(含开发独立App)
如果选择使用类似 Vue 或 React 这样的现代JavaScript框架(或者使用Flutter这样的跨平台前端开发框架,或者是开发小程序等),开发难度将会大幅增加,原因如下:
一、高度依赖Xiuno BBS的API功能:
- 你需要依赖Xiuno BBS的API来获取数据并与后台交互。
- 公开注明的API节点数量可能不足以满足所有需求,可以观察实际页面表单的内容并通过抓包来模仿这些请求(例如登录、注册、发帖、回帖等操作)。
- 安装API插件后,可以通过在URL后面加上
?ajax=1
来返回JSON格式内容。 - 仍需编写更多代码来暴露你所需的功能,并确保敏感信息不会被泄露。
二、Xiuno BBS的API本身也能让你诱发高血压,所以我必须先讲清楚你可能会遇到的坑:
- API返回的内容中,
code
可能是int也可能是string,其中:- 如果是int:
0
是成功1
通常是用户出错(例如没有权限等)-1
通常是服务器出错(例如因为某些原因没能将提交的内容成功写入数据库)- 其他任何值都应该视作用户出错,除非这个错误码是你定义的
- 示范:
{code: 0, message: "登录成功"}
- 如果是string:
- 例如
username
的内容应当全部当作是用户出错(例如用户名出错) - 但是邪门的就来了:有时候,可能是因为PHP版本缘故,会将int转换成string,所以你还得单独写逻辑来处理这种问题
- 示范:
{code: "-1", message: "登录失败"}
- 示范:
- 例如
- 如果是int:
- 而
message
可能会返回任意类型(主要包括string,int,array,object),务必注意这一点(希望你的JSON解析库足够坚挺,不会因为这种问题而坏掉)- 因为有时候即使
code
是0,它也可能会返回你意想不到类型的东西 - 例如回复帖子的节点
post-create.htm
,它可能会返回“发帖成功”也可能返回你刚刚创建好的帖子的JSON表达
- 因为有时候即使
- 你必须自己总结API节点的请求方式、参数、enctype(是
application/x-www-form-urlencoded
还是multipart/form-data
)、返回内容示范,因为Xiuno BBS的作者……一言难尽,仅仅告诉你类似
和发表新主题:/thread-create.htm fid, subject, doctype, message doctype 值参考: install/install.sql
和版主管理:参看 route/mod.php
这样的十分粗略的介绍,而没有更具体的参数和返回内容示范。其他请参看代码 route/*.php
- (非广告)我个人强烈建议使用Apifox来完成API节点的记录。
-
注意:我没有任何义务替你总结这些API节点,you're on your own.
三、安全隐患:
- 例如,某些API节点可能未经过滤,会输出密码等敏感信息(即使密码已经是密文,但仍然是重大安全隐患)。因此,必须仔细检查每个页面的API返回内容,并手动 unset 敏感字段。
- Xiuno BBS为了“将计算量放在用户侧”仅使用了MD5加密,而没有使用更复杂的方式,或没有将整个加密部分放在后端,这可能导致安全性不足。不过,这也符合一些安全要求(例如传输密码时不能明文传输)。
如果决定使用Vue或React之类的框架,可以考虑使用第三方库(如 BootstrapVue 或 React-Bootstrap)来利用Bootstrap的样式和组件,以保持一定程度的兼容性。
我很希望有人能当这个“第一个吃螃蟹的人”。我也很欣慰我已经看到有一些人尝试做App了。
结论
十分遗憾的是,除非你有极大毅力将你自己预计使用的插件都转化为适合你自己的“另一套技术栈”风格,否则最好选择Bootstrap,只因为这个生态系统与Bootstrap绑定。
三、创建主题
我们将会使用知名的“Argon” Bootstrap 主题。该主题与Xiuno BBS一样,采用MIT License,很适合本教程所需。
涉及到什么页面
主要
我们要对这些页面做大翻新:
header.inc.htm
header_nav.inc.htm
footer.inc.htm
footer_nav.inc.htm
forum.htm
index.htm
thread.htm
次要
然后对这些页面进行微调:
message.htm
my_thread.htm
post.htm
post_list.inc.htm
user-login.htm
user-create.htm
user_resetpw.htm
user_resetpw_complete.htm
user_thread.htm
其余页面不变。
预计目录结构
- my_theme_softcave/
- conf.json
- overwrite/
- view
- htm
- header.inc.htm
- header_nav.inc.htm
- footer.inc.htm
- footer_nav.inc.htm
- forum.htm
- index.htm
- thread.htm
- thread_list.inc.htm
- message.htm
- my_thread.htm
- post.htm
- post_list.inc.htm
- user_login.htm
- user_create.htm
- user_resetpw.htm
- user_resetpw_complete.htm
- user_thread.htm
- htm
- view
- view
- css
- argon-dashboard.css
- bbs.css
- css
下载所需的静态资源
使用电脑浏览器访问 https://technext.github.io/argon-dashboard/index.html ,然后直接按ctrl+s保存网页,“保存类型”选中“网页,全部”,然后找一个安全的地方保存。
在文件管理器里,找到刚才下载好的文件所在的文件夹,找到名字类似“Argon Dashboard - Free Dashboard for Bootstrap 4 by Creative Tim_files”的文件夹,打开,其中有一个文件叫“argon-dashboard.css”,复制它到“xiunobbs根目录/plugin/my_theme_softcave/view/css/”文件夹里。
文件内容,及可以在对应文件里使用的变量详解
因为文件内容很多,我在此处主要阐述做了什么修改内容。随附的主题内会包含详细注释。
【一直可用的变量】
$conf
:Xiuno BBS配置信息(全部页面)- 详见
conf/conf.php
文件
- 详见
$header
:title
:string类型;页面标题,对应<title>
标签mobile_title
:string类型;页面移动端标题,通常会在导航栏显示mobile_link
:string类型;页面移动端链接,对应导航栏Logo网址,多数时候是主页,偶尔会在帖子页面为板块页面keywords
:string类型;SEO关键字,用半角逗号分隔;如果要使用的话,请务必确保每次都是用.=
而不是=
,否则你会覆盖其他插件设置的关键字description
:string类型;SEO描述文字navs
:array类型;本意是导航菜单内容,但在Xiuno BBS里没用到,可能为预留
$user
:array类型;当前登录的用户(全部页面),大致包括:uid
:int类型;用户IDgid
:int类型;用户组IDemail
:string类型;邮箱;【禁止对外显示】username
:string类型;用户名,不可以重复;【禁止对外显示】realname
:string类型;真实姓名;为未来需求预留;【禁止对外显示】idnumber
:string类型;真实身份证号码;为未来需求预留password
:string类型;密码Hash结果;【禁止对外显示】password_sms
:string类型;手机发送的 sms 验证码(又叫OTP);为未来需求预留;【禁止对外显示】salt
:string类型;密码的盐值;【禁止对外显示】mobile
:int类型;手机号;为未来需求预留;【禁止对外显示】qq
:string类型;qq号码;为未来需求预留;【禁止对外显示】threads
:int类型;发帖数posts
:int类型;回帖数credits
:int类型;积分类型1(经验),与积分插件一起使用golds
:int类型;积分类型2(金币),与积分插件一起使用rmbs
:int类型;积分类型3( ),与积分插件一起使用create_ip
:int类型;创建时IP,使用ip2long
函数保存;【禁止对外显示】create_date
:int类型;创建时间 时间戳;【禁止对外显示】login_ip
:int类型;登录时IP,使用ip2long
函数保存;【禁止对外显示】login_date
:int类型;登录时间 时间戳;【禁止对外显示】logins
:int类型;登录次数;【禁止对外显示】avatar
:int类型;用户最后更新头像时间 时间戳create_ip_fmt
:string类型;创建时IP,使用long2ip
函数转换后的样子;【禁止对外显示】create_date_fmt
:string类型;创建时间,人类可读格式;【禁止对外显示】login_ip_fmt
:string类型;登录时IP,使用long2ip
函数转换后的样子;【禁止对外显示】login_date_fmt
:string类型;登录时间,人类可读格式;【禁止对外显示】groupname
:string类型;用户组名称avatar_url
:string类型,用户头像地址online_status
:int类型;如果用户在线,会是1(建议转换成bool类型)
$uid
:int类型;当前登录的用户ID,0为游客(全部页面)$gid
:int类型;当前登录的用户的用户组ID(全部页面)$fid
:int类型;当前所在的论坛板块ID(用于板块、帖子详情、编辑帖子页面)$tid
:int类型;当前所在的帖子ID(用于帖子详情、编辑帖子页面)$pid
:int类型;当前所在的回帖ID(用于编辑回帖页面)$route
:string类型;当前所在的路由名称,如“index、forum、user”等(全部页面)$forumlist_show
:array类型;可以对外显示的论坛板块数组(全部页面)- 详见header_nav.inc.htm里的
$_forum
- 详见header_nav.inc.htm里的
$static_version
:string类型;静态资源版本号(全部页面)
// 游客的$user内容
$guest = array (
'uid' => 0,
'gid' => 0,
'groupname' => lang('guest_group'),
'username' => lang('guest'),
'avatar_url' => 'view/img/avatar.png',
'create_ip_fmt' => '',
'create_date_fmt' => '',
'login_date_fmt' => '',
'email' => '',
'threads' => 0,
'posts' => 0,
);
下文中,除非特殊标注,一律都包括“一直可用的变量”。
主要文件内容
header.inc.htm
【修改内容简介】
增加了主题自己的CSS文件,改变页面结构,增加“header部分”。
【本文件内的变量】
$bootstrap_css
:string类型;Bootstrap框架网址$bootstrap_bbs_css
:string类型;论坛专用样式网址
header_nav.inc.htm
【修改内容简介】
重新设计了导航栏内容。
【本文件内的变量】
$_forum
:array类型;单个论坛板块:fid
:int类型;板块IDfup
:int类型;上一级版块;为其他插件预留name
:string类型;版块名称rank
:int类型;显示顺序,注意数字越大越靠前threads
:int类型;主题数todayposts
:int类型;今日发回帖数量todaythreads
:int类型;今日发主题贴数量brief
:string类型;版块简介(允许HTML,但你可能需要使用htmlspecialchars_decode
函数解决)announcement
:string类型;版块公告(允许HTML,但你可能需要使用htmlspecialchars_decode
函数解决)accesson
:int类型;是否开启权限控制;(建议转换成bool类型)orderby
:int类型;默认列表排序方式,其中:0 = 最后回复时间时间last_date
、1 = 发帖时间tid
create_date
:int类型;板块创建时间 时间戳icon
:int类型;板块是否有 icon,如果有,则这里为最后更新时间 时间戳,如果没有,则为0moduids
:string类型;版块的版主用户ID,最多10个,逗号分隔seo_title
:string类型;该板块的 SEO 标题,如果设置会代替版块名称;为其他插件预留seo_keywords
:string类型;该板块的 SEO 关键字;为其他插件预留create_date_fmt
:string类型;板块创建时间,人类可读格式icon_url
:string类型;板块图标地址accesslist
:array类型;特定于该板块的权限控制。- 它对应后台板块编辑页面里的“用户权限”复选框。
- 如果没有选中的话,accesslist是空白array,权限会与当前用户组设定一样
- 如果选中的话,会是无序数组,其中有:
fid
:int类型;板块IDgid
:int类型;用户组ID- 可以用来与当前用户的
$gid
比对
- 可以用来与当前用户的
allowread
:int类型;是否允许看帖(建议转换成bool类型)allowthread
:int类型;是否发主题贴(建议转换成bool类型)allowpost
:int类型;是否允许回帖(建议转换成bool类型)allowattach
:int类型;是否允许上传附件(建议转换成bool类型)allowdown
:int类型;是否允许下载附件(建议转换成bool类型)
modlist
:array类型;版块的版主用户数组- 详见header.inc.htm里的
$user
- 详见header.inc.htm里的
footer.inc.htm
【修改内容简介】
与header.inc.htm相对应,也改变了页面结构,并增加了谷歌Code Prettify来改善代码块的外观,以及一个动态给body
元素添加“scrolled” class的辅助JS代码。
【本文件内的变量】
$db
:Object类型;代表数据库连接,其中可能包括:conf
:array类型;数据库连接配置;【禁止对外显示】master
:array类型;主数据库数据库连接配置;【禁止对外显示】host
:string类型;数据库主机地址user
:string类型;数据库用户名password
:string类型;数据库密码name
:string类型;数据库名称tablepre
:string类型;数据库表的前缀charset
:string类型;数据库的字符集engine
:string类型;数据库表的引擎;每张表应该使用相同的引擎,但也说不定,因为有些插件会创建自己的表,选择自己认为合适的引擎
slaves
:array类型;从数据库数据库连接配置;【禁止对外显示】- 内容同上。
-
我知道,你会觉得这个词不好听,但请注意,这个程序是在2020年10月1日(也就是GitHub更改所有新的Repository的默认分支名称从master改为main)之前发布的。我个人其实更喜欢用“servant”,因为fate
rconf
:array类型;数据库连接配置;【禁止对外显示】- 内容同
$db[conf][master]
。
- 内容同
errno
:int类型;数据库错误码,如果没有错误是0errstr
:string类型;数据库错误信息,如果没有错误则为空sqls
:array类型;执行的SQL语句;【禁止对外显示】tablepre
:string类型;数据库表的前缀;【禁止对外显示】
$starttime
:float类型;程序开始执行时间$time
:int类型;现在时间 时间戳$ip
:string类型;当前用户IP地址$useragent
:string类型;当前访客的User Agent值$forumlist
:array类型;完整论坛板块列表- 详见header_nav.inc.htm里的
$_forum
- 详见header_nav.inc.htm里的
$forumarr
:array类型;用户可见的论坛板块简略数组,其中:- 键是板块ID,值是板块名称
$fid
:int类型;当前所在的论坛板块ID(用于板块、帖子详情、编辑帖子页面)$conf
:Xiuno BBS配置信息(全部页面)- 详见
conf/conf.php
文件
- 详见
$static_version
:string类型;静态资源版本号(全部页面)$browser
:array类型;从User Agent中提取的用户浏览器信息数组,不建议完全相信device
:string类型;用户可能使用的设备,取值范围为:pc, mobile, pad
name
:string类型;浏览器名称,取值范围为:chrome, ie, robot
等version
:int类型;浏览器版本,注意可能为0-
为什么不建议完全相信?因为该变量的内容,也就是
get__browser
函数,是针对中国市场精心设计的。-
代码注释中提及“中国国情下的判断浏览器类型,简直就是五代十国,乱七八糟”,这里的“五代十国”是一种形象化的比喻,用于描述中国市场上浏览器类型多样、判断复杂的情况。
-
互联网发展过程中,不同浏览器厂商采用不同的技术标准和内核,同时还存在多种兼容模式,这就使得开发者在判断用户使用的浏览器类型时面临诸多困难,就如同 “五代十国” 时期政治局势混乱、各方势力割据一样。
-
footer_nav.inc.htm
【修改内容简介】
将原有的Xiuno BBS页脚部分修改成没有背景颜色、居中显示的样式。
【本文件内的变量】
$db
:Object类型;代表数据库连接- 详见footer.inc.htm里的
$db
- 详见footer.inc.htm里的
$starttime
:float类型;程序开始执行时间
forum.htm
【修改内容简介】
配合thread_list.inc.htm修改成单列风格(视觉效果清爽),将板块介绍移动到header.inc.htm的“header部分”。
【本文件内的变量】
$active
:string类型;当前选中的大类别(如新帖、精华等)$orderby
:string类型;排序方式,对应get请求参数中的“orderby”$threadlist
:array类型;帖子列表,其中内容大致见thread_list.inc.htm
里的$_thread
index.htm
【修改内容简介】
配合thread_list.inc.htm修改成单列风格(视觉效果清爽),将网站简介移动到header.inc.htm的“header部分”。
【本文件内的变量】
$runtime
:array类型;论坛统计信息,可能包括:users
:int类型;用户总数posts
:int类型;帖子总数(含回帖+主题帖)threads
:int类型;主题贴总数todayposts
:int类型;今日范围内新发布的回帖数todaythreads
:int类型;今日范围内新发布的主题帖数onlines
:int类型;当前在线用户数(注意:单个用户,在两个设备上登陆,算作两个用户)cron_1_last_date
:int类型;定时任务1执行时间戳cron_2_last_date
:int类型;定时任务2执行时间戳- 有时,这些数字会不准确,具体原因未知
$threadlist
:array类型;帖子列表,见forum.htm
中的$threadlist
$active
:string类型;当前选中的大类别(如新帖、精华等)
thread.htm
【修改内容简介】
修改成单列风格(视觉效果清爽),将帖子标题、作者等信息移动到header.inc.htm的“header部分”。
【本文件内的变量】
$first
:array类型;楼主发布的内容,其中包含,详见post_list.inc.htm
中的$_post
$postlist
:array类型;回帖列表,详见post_list.inc.htm
中的$_post
thread_list.inc.htm
【修改内容简介】
完全重新设计了。仿照常见的博客风格文章列表,设计了:
- 头图
- 标题
- 可能存在的简介文字
- 作者名称与板块
- 发布日期、查看量、回帖量等
【本文件内的变量】
$_thread
:单个帖子内容,其中大致包括:tid
:int类型;帖子IDfid
:int类型;帖子所属版块idtop
:int类型;置顶级别,取值为:0 = 普通主题、1 = 板块置顶、3 = 全站置顶uid
:int类型;发帖人用户iduserip
:int类型;发帖时用户ip(使用ip2long
)转换成数字subject
:string类型;主题create_date
:int类型;发帖时间last_date
:int类型;最后回复时间views
:int类型;查看次数,posts
:int类型;回帖数images
:int类型;附件中包含的图片数files
:int类型;附件中包含的文件数mods
:int类型;预留:版主操作次数,如果 > 0, 则查询 mo dlog,显示版主的评分closed
:int类型;帖子是否关闭/锁定;(建议转换成bool类型)firstpid
:int类型;首贴 pid;【重要:这个PID表示帖子正文内容】lastuid
:int类型;最近参与的用户的用户idlastpid
:int类型;最后回复的回帖的PIDcreate_date_fmt
:string类型;帖子创建时间,人类可读形式last_date_fmt
:string类型;最后一个回帖创建时间,人类可读形式user
:array类型;发帖人用户信息数组,等于user_read($_thread['uid'])
- 详见header.inc.htm里的
$user
- 【注意:这里面会包含密码等敏感信息,务必再次确认敏感信息是否已被去掉。如果没去掉的话,请务必去掉!】
- 详见header.inc.htm里的
username
:string类型;发帖人用户名user_avatar_url
:string类型;发帖人用户头像地址forumname
:string类型;该帖子所属板块名称lastuid
:int类型;最后一个回帖的用户IDlastusername
:string类型;后一个回帖的用户名url
:string类型;该帖子的网址user_url
:string类型;该帖子发帖人的网址top_class
:string类型;可以直接在HTML中输出的“该帖子的置顶等级class值”pages
:int类型;该帖子有多少页回帖
次要文件内容
message.htm
【修改内容简介】
修改成了全屏风格。
【本文件内的变量】
$code
:提示信息码,0为成功,1为用户出错,-1为服务器出错,可能有其他值$message
:提示信息内容
my_thread.htm
【修改内容简介】
配合thread_list.inc.htm更改了展示方式。
【本文件内的变量】
$threadlist
:array类型;帖子列表,见forum.htm
中的$threadlist
post.htm
【修改内容简介】
发帖/编辑帖子页面重新调整宽度。
【本文件内的变量】
$form_title
:string类型;表单标题$form_action
:string类型;表单action属性的值$form_submit_txt
:string类型;表达提交按钮文字$form_subject
:string类型;表单内的subject值(帖子标题)$form_message
:string类型;表单内的message值(帖子内容)$form_doctype
:int类型;表单内的doctype值$isfirst
:int类型;是否为主题帖,1表示“是主题帖”$quotepid
:int类型;被引用的pid,0表示没有引用$location
:string类型;在提交后,用户会到达的网址$filelist
:array类型;附件列表
post_list.inc.htm
【修改内容简介】
配合thread.htm进行外观微调。
【本文件内的变量】
-
$_post
:array类型;回帖内容,其中大致包括:tid
:int类型;所属主题贴id;如果isfirst是0,则这里会有值,否则为0pid
:int类型;帖子/回帖本身iduid
:int类型;发帖者用户idisfirst
:int类型;是否为首帖,与$thread['firstpid'] 呼应;是1的话表示为主题贴,是0的话表示回帖create_date
:int类型;发贴时间 时间戳userip
:int类型;发帖时用户ip(使用ip2long
函数)images
:smallint类型;附件中包含的图片数,预留files
:smallint类型;附件中包含的文件数,预留doctype
:tinyint类型;message的内容类型,取值范围:- 0 = html
- 在内部会使用
xn_html_safe
函数进行反XSS攻击过滤
- 在内部会使用
- 1 = 纯文本
- 在内部会使用
xn_txt_to_html
函数完成HTML转化
- 在内部会使用
- 2 = markdown
- 预留,可以自行使用Parsedown库完成HTML转化
- 3 = ubb(BBCode)
- 预留,可以使用其他库完成HTML转化
- 0 = html
quotepid
:int类型;引用哪个 pid,如果没有引用的话为0,仅在isfirst为0时有效message
:string类型;用户输入的原始帖子内容message_fmt
:string类型;转成HTML之后的帖子内容
-
create_date_fmt
:类型;发贴时间,人类可读形式 username
:string类型;发帖者用户名user_avatar_url
:string类型;发帖者用户头像网址user
:array类型;详见$user
floor
:类型;楼层序号allowupdate
:int类型;是否允许更新帖子,1是允许(建议转换成bool类型)allowdelete
:int类型;是否允许删除帖子,1是允许(建议转换成bool类型)user_url
:string类型;发帖者用户网址files
:int类型;附件数量filelist
:array类型;附件列表,其中有:aid
:int类型;附件idtid
:int类型;对应的主题贴idpid
:int类型;对应的帖子id;如果tid存在的话,则pid为$thread['firstpid']
uid
:int类型;附件上传者用户idfilesize
:int类型;文件尺寸,单位字节width
:int类型;图片宽度,如果宽度大于0则为图片height
:int类型;图片高度filename
:string类型;文件名称,更具体点说是“保存后的文件名”,不包含URL前缀$conf['upload_url']
(会过滤,并且截断)orgfilename
:string类型;上传的原文件名(用户在设备上选择文件时的文件名)filetype
:string类型;文件类型,取值范围如下:video
= 'av','wmv','wav','wma','avi','rm','rmvb','mp4'music
= 'mp3','mp4'exe
= 'exe','bin'flash
= 'swf','fla','as'image
= 'gif','jpg','jpeg','png','bmp'office
= 'doc','xls','ppt','docx','xlsx','pptx'pdf
= 'pdf'text
= 'c','cpp','cc', 'txt'zip
= 'tar','zip','gz','rar','7z','bz'book
= 'chm'torrent
= 'bt','torrent'font
= 'ttf','font','fon'all
= 以上全部- 以及隐藏默认值“other”,如果filetype是空白的话,会在
thread_list.inc.htm
中被处理成“other” -
该值会在
thread_list.inc.htm
中的$_thread['subject']
附件以图标形式显示,类似于<i class="icon filetype image"></i>
-
这些类型的定义在
conf/attach.conf.php
里,可自行修改。
create_date
:int类型;文件上传时间 时间戳comment
:string类型; 文件注释,预留- 可以作为存储元数据的地方,因为整个Xiuno BBS没有使用这个
downloads
:int类型;下载次数credits
:int类型;需要的积分类型1(经验),预留golds
:int类型;需要的积分类型2(金币),预留rmbs
:int类型;需要的积分类型3( ),预留isimage
:int类型;是否为图片(建议转换成bool类型)- create_date_fmt:string类型;文件上传时间,人类可读形式
- url:int类型;文件网址,等于
$conf['upload_url'].'attach/'.$attach['filename']
classname
:string类型;可以直接在HTML中输出的class值
user-login.htm
【修改内容简介】
配合新的header样式进行外观微调。
【本文件内的变量】
- 无
user-create.htm
【修改内容简介】
配合新的header样式进行外观微调。
【本文件内的变量】
- 无
user_resetpw.htm
【修改内容简介】
配合新的header样式进行外观微调。
【本文件内的变量】
- 无
user_resetpw_complete.htm
【修改内容简介】
配合新的header样式进行外观微调。
【本文件内的变量】
- 无
user_thread.htm
【修改内容简介】
配合thread_list.inc.htm更改了展示方式。
【本文件内的变量】
$threadlist
:array类型;帖子列表,见forum.htm
中的$threadlist
注意事项
原版Xiuno BBS喜欢这样的写法:
<a href="<?php echo url("thread-$_thread[tid]"); ?>">...</a>
我完全不赞成你这样写。因为:
- 使用双引号包含PHP变量(特别是
$_thread[tid]
这种形式)可能会导致解析问题或被误解为常量而非数组键名- 虽然在双引号里面这样不会出错,但万一你没有使用双引号呢?
我希望你使用更规范的方式写,例如:
<a href="<?php echo url('thread-' . $_thread['tid']); ?>">...</a>
因为:
- 明确使用单引号包裹字符串字面量,并且正确引用数组元素(如$_thread['tid']),这有助于避免因误解或误用而导致的错误。
我个人在整个主题中使用了<?=
而不是<?php echo
是有其合理性的,因为:
- 简洁:
<?=
是<?php echo
的简写形式,它可以使你的代码更加简洁和易于阅读。例如,<?= $hello ?>
比<?php echo $hello; ?>
要简洁得多。 - 效率:虽然从性能角度来看,使用
<?=
与<?php echo
之间完全没有差异,但是你仅需敲击五次键盘(shift
,<
,?
,(松开shift)
,=
,空格
)而不是十次,间接提升效率。(假设你的编辑器能智能帮你补全另一半,我的VSCode可以) - 无需担心兼容性:自PHP 5.4.0起,
<?=
标记总是可用的,无论php.ini
中的short_open_tag
设置如何。这意味着你可以安全地使用这种简写方式而不用担心服务器配置问题。- 虽然
<?(不带 =)
短标签是被废弃了,但<?=
没有废弃,后者实为 PHP 社区推荐的现代语法。
- 虽然
仅当需兼容 PHP ≤5.3 的史前环境(当前全球占比 <0.1%)时,
<?=
才可能因short_open_tag=Off
失效,但此情况在 2023 年后可忽略不计。
恭喜你!
恭喜你已经迈出了创建个性化Xiuno BBS主题的重要一步!通过这份详尽的指南,你不仅学习了如何利用Overwrite机制来定制你的BBS站点外观,还了解了如何确保这些改动与现有的插件和功能兼容。
希望你能将这里学到的知识应用到实践中,创造出既美观又实用的在线社区。不要害怕遇到挑战,每一个难题都是提升技能的机会。当你最终完成这个项目时,你会发现所有的努力都是值得的。
不过,记住,一个好的社区不仅仅在于它的外观,更在于它能为成员提供有价值的内容和友好的交流环境。
如果你在开发过程中遇到了任何问题或需要进一步的帮助,记得身边有一个可靠的伙伴——AI助手随时准备为你解答疑惑、提供建议。
祝你在编程之旅中一切顺利,享受构建自己独特“数字空间”的乐趣吧!