随手用 PHP 做了个论坛
废话
这是小学放暑假时的一个练手项目了,本来打算录制制作过程发到 B 站,但无奈电脑存不下录好的视频,只好删掉原视频,更改计划去发布到 GitHub 并写成博客。
其实早就在旧博客写过一个不完整版,但语文实在差劲,没写多少就勉强发布了。
现在语文还是差劲,其它科学得都可以说轻轻松松,唯独语文还在折磨我,拖我的后腿——不,已经快把整个下半身扯掉了。
之前是逐文件介绍代码的,发现并不好,现在还是逐功能介绍吧。
小声说句,怎么感觉后面的话不像地道的中文?就感觉像直接翻译的一样。
论坛简介
本文会以 PHP 为后端语言,实现一个简单的论坛软件,其支持以下功能:
- 登陆注册
- 发帖回复
- 点赞回复
- Markdown
- 置顶帖
响应式本文不涉及样式SQLite本文采用 JSON 数据库- VIP 用户
当然,我打算先从接近后端的部分讲起,随后再讲解前端部分。如果你就是想搭建一个论坛的话,请到 GitHub直接下载完整版 ,连样式都已经弄得漂漂亮亮的了。
JSON 数据库
由于博主在编写此论坛时未能解决 自动拼接 SQL 语句 和防止 SQL 注入 这两个问题,所以蠢蠢的写了一套 JSON 数据库驱动器实现了很多功能。
原理也很简单,就是用 PHP 自带的json_encode
和json_decode
函数对 JSON 文件进行解析。举个读的栗子:
class Database{
public static function ls($table){
//列出所有项,$table为表名。
return json_decode(file_get_contents("./storage/".$table.".json"),true);
}
}
缺点也很明显:效率低、占用高、存储空间大、功能不足……
接下来,只要根据此原理实现常规 SQL 中的 create/drop table, select, insert, update, delete 等指令就可以用了。啥?你问我咋实现?去我的 GitHub 项目 看下initailize.php
就可以了解了。
Markdown
论坛采用 Markdown 和 HTML 结合排版,由于本人无力制作 Markdown 引擎,所以采用了 ParseDown,所以代码是酱紫的:
<?php
require("markdown.php");
$unparsed_text=<<<EOF
# 这里是一个小栗子
不要在意这些细节
EOF;
echo (new Parsedown)->text($unparsed_text);
登录注册
在聊代码之前,我认为我应该说下注册和登录的流程。注册:
- 填写信息(并验证合法)
- 提交到后端
- 验证合法性
- 存入数据库
- 返回信息
登录:
- 填写信息
- 提交到后端
- 验证合法性
- 返回信息(如果成功创建 Cookies)
从注册聊起,因为没有注册就没法登录。填写信息的话只要在网页上留几个框就可以了。
<form onsubmit="register();return false;">
<input id="username" placeholder="用户名"/>
<input id="password" placeholder="密码"/>
<!--其它信息此处不做探讨-->
<button type="submit">注册</button>
</form>
此处由于个人也厌恶那些“恶心”的跳转,所以采用 AJAX 提交表单:
function register(){
var xhr = new XMLHttpRequest();
xhr.setRequestHeader("Content-Type","application/json");//本人喜欢提交JSON信息
xhr.onreadystatechange=function(){
if(xhr.readyState==4) switch(xhr.status){
case 0:
alert("网络错误");
break;
case 200:
window.location.href="./";
break;
case 406:
alert(xhr.responseText);
break;
case 500:
case 503:
alert("服务器错误:"+xhr.responseText);
break;
default:
alert("未知错误("+xhr.status+"):"+xhr.responseText);
break;
}
}
xhr.open("POST","api.php?operation=register",true);
xhr.send(JSON.stringify({
username:document.querySelector("#useranme").value,
password:document.querySelector("#password").value
}));
}
后端的话,在项目中个人喜欢封装成 class,但这里我要拆开:
<?php
function generate_guid() {
/*GUID是目前辨识数据库KEY的最好方式*/
$charid = strtoupper(md5(uniqid(mt_rand(), true)));
$hyphen = chr(45);
$uuid = chr(123)
.substr($charid, 0, 8).$hyphen
.substr($charid, 8, 4).$hyphen
.substr($charid,12, 4).$hyphen
.substr($charid,16, 4).$hyphen
.substr($charid,20,12)
.chr(125);
return $uuid;
}
$_POST=json_decode(file_get_contents("php://input"),true);//解析JSON
if(!isset($_POST['username'])||!preg_match('/[a-zA-Z0-9_-]{2,16}/ig',$_POST['username'])){
header("HTTP/1.1 406 Unacceptable");
die("用户名非法");
}
if(!isset($_POST['password'])){
header("HTTP/1.1 406 Unacceptable");
die("密码非法");
}
if(Database::select('users','username',$username)){
header("HTTP/1.1 406 Unacceptable");
die("用户已存在");
}
$u = Database::insert('users',[//插入数据
'userid'=>generate_guid(),
'username'=>$_POST['username'],
'password'=>sha256($_POST['password']),
'vip'=>false,//VIP信息
'token'=>'{'.$_POST['username'].'-'.mt_rand().'-'.time().'}',//登录校验用token
'registered'=>time()
]);
对于登录,前端同理,后端:
<?php
$_POST=json_decode(file_get_contents("php://input"),true);//解析JSON
if(!isset($_POST['username'])){
header("HTTP/1.1 406 Unacceptable");
die("用户名是必须的");
}
if(!isset($_POST['password'])){
header("HTTP/1.1 406 Unacceptable");
die("密码是必须的");
}
if(!$u = Database::select('users','username',$_POST['username'])){
header("HTTP/1.1 404 Not Found");
die("用户不存在");
}
if($u['password']!=sha256($_POST['password'])){
header("HTTP/1.1 401 Unauthorized");
die("密码错误");
}
setrawcookie("token",$u['token'],time()+2147483647,"/");//设置Cookies
echo "成功登陆";
想退出登录了,只需清除 Cookies:
function logout(){
setrawcookie("token","{undefined}",0,"/");
}
验证登录的话,就是从数据库中寻找带有相应 token 的用户:
<?php
function user_logged(){
if($u=Database::select('user','token',empty($_COOKIE['token'])?null:$_COOKIE['token'])) return $u;
else return false;
}
发帖回复
和注册的流程差不多,所以为了减小文章就不发实现代码了,不过我这里要展示一下帖子和回复的数据库结构:
/*帖子*/
{
"postid":"ID",
"title":"标题",
"content":"正文",
"sticky":false,//置顶
"user":"发布者ID",
"datetime":5201314,//发布日期
"likes":["点赞者ID"],
"replies":0,//回复量
"views":0//阅读量
}
/*回复*/
{
"replyid":"ID",
"content":"正文",
"user":"发布者ID",
"datetime":5201314,//发布日期
"likes":["点赞者ID"],
"floor":2,//楼层
"repliedTo":null,//被回复者,
"repliedTo":{
"user":"被回复回复者ID",
"floor":"被回复楼层"
}//当回复某人时
}
置顶 &VIP 用户
读到这里,你或许看到了,在数据库预设中,有sticky
、vip
这样的字段,它们的确就是本小标题的功能。而判定是否置顶、VIP 的话,只需要判定 TF:
<?php
/*以VIP用户为例*/
if($u=user_loggeed()){
echo $u['vip']?'VIP':'非VIP';
}else{
echo "没登录判定个毛线";
}
前端显示如上功能
用尽量通俗的语言说完了“轶可赛艇”的后端功能后,还需要前端来显示它们,当然,此处仅输出最简单的内容,额外内容需自己幻想。
列出帖子:
<h1>帖子列表</h1>
<ul><?php
$posts = Database::ls('posts');
foreach($posts as $key=>$value){?>
<li><a href="<?php echo htmlspecialchars("./view/".$value['postid']);?>"><?php echo htmlspecialchars($value['title']);?></a></li>
<?php }?>
</ul>
详细内容:(回复、点赞自行添加)
<?php
$post = Database::select('posts','postid',$_GET['post']);
if(!$post) {
header("HTTP/1.1 404 Not Found");
die("帖子不存在");
}
$replies = Database::select('replies','post',$post['postid']);
?>
<h1><?php htmlspecialchars($post['title']);?></h1>
<main><?php echo (new Parsedown)->text($post['content']); ?></main>
<?php
if($replies) foreach($replies as $key=>$value){
?>
<hr />
<p><?php echo (new Parsedown)->text($value['content']);?></p>
<?php
} else {?><hr />
无回复<?php }
?>
终于写完了 2333,如果有什么问题评论或其它方式告诉我。
想看看测试██是什么样子的。