初步实现思路
一个首页:有很多文章列表,点进去可以浏览这一篇文章,文章底下有评论区,可以发表评论;
一个登陆页面:输入账号密码登录进入自己的主页;
一个发表文章的页面:用户的登录之后,可以自己发表文章;
一个评论审核页面:用户登陆之后,可以浏览并且审核其他人在自己文章下的评论;
具体实现
1、设计博客文章和评论字段
博客文章数据库需要的字段有:
_id(自动生成)、文章分类、文章标题、作者、发布日期、文章内容、文章简介(简介只在首页列表下显示就行)、评论。
评论数据库的字段有:
_id(自动生成)、fid(from-id,记录此条评论是来自哪一个文章)、评论内容、发布评论的用户名、发布评论的日期。
pageDB和commentDB通过_id与fid联系在一起。
设计好字段,接下来我们开始实现博客项目。
2、首先安装并引入需要的模块和中间件
const express=require('express')
const bodyParser=require('body-parser')
const urlencodedParser=bodyParser.urlencoded({extended:false})
const cookieParser=require('cookie-parser')
//引入mongoControl
const mongoControl = require('./mongoControl')
// 初始化一个用于博客文章和评论的库和表
const pageDB = new mongoControl('blog', 'page')
const commentDB = new mongoControl('blog', 'comment')
const ejs=require('ejs')
3、初始化服务器
这里文章列表需要动态变化,不能使用express.static直接引入,需要自己手动写/接口的具体操作。
const app=express()
app.use(cookieParser())
app.use(express.static('./static',{index:false}))
app.listen(3000)
4、设计index.ejs模板
使用bootstrap简单设计样式,代码会变复杂。
给每个a标签添加href,以便点击跳转的时候使用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<link rel="stylesheet" href="./bootstrap-3.3.7-dist/css/bootstrap.min.css">
<script src="./bootstrap-3.3.7-dist/js/bootstrap.js"></script>
</head>
<body>
<div class="container">
<div class="page-header">
<h1>佩奇
<small>🐖の专栏</small>
</h1>
</div>
<ul class="list-group">
<%data.forEach(function(item,index){%>
<li class="list-group-item">
<a href="/p?_id=<%= item._id %>">
<h3> <%= item.title %> </h3>
</a>
<p>日期:
<%= item.date %>
</p>
<p><%= item.intro %></p>
</li>
<%})%>
</ul>
</div>
</body>
</html>
5、设计page.ejs模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<link rel="stylesheet" href="./bootstrap-3.3.7-dist/css/bootstrap.min.css">
<script src="./bootstrap-3.3.7-dist/js/bootstrap.js"></script>
</head>
<body>
<div class="container">
<div class="page-header">
<h1><%= data.title %>
<span class="label label-success" style="font-size: 14px;"><%= data.sort %></span>
</h1>
<h1>
<small><%= data.author %></small>
<small><%= data.date %></small>
</h1>
</div>
<div class="well"><%= data.content %></div>
</div>
</body>
</html>
写好了模板文件,接下来我们要写服务器文件。
6、设计首页接口请求
在博客文章数据库请求所有的数据,返回结果通过ejs的renderFile方法传递给首页模板文件(index.ejs),在index.ejs里编写js语句把数据填充到相应位置,再通过res.send方法发送给前端。
app.get('/',function(req,res){
pageDB.find({},function(jieguo){
// console.log(jieguo)
ejs.renderFile('./ejs-tpl/index.ejs',{data:jieguo},function(error,result){
// console.log(result)
res.send(result)
})
})
})
7、设计文章接口/p请求
首先获取地址栏的id,通过id在博客文章数据库pageDB中查找数据,查找返回的结果是一个数组,获取数组第一项,把它通过ejs.renderFile发送到page.ejs,让page.ejs把数据填充到相应位置,返回整个html字符串文件,通过res.send返回给前端。
app.get('/p',function(req,res){
var _id=req.query._id
pageDB.findOneById(_id,function(jieguo){
var data=jieguo[0]
// console.log(data)
ejs.renderFile('./ejs-tpl/page.ejs',{data:data,comment:jieguo1},function(err,html){
res.send(html)
// console.log(html)
})
})
})
到这里,我们能实现首页和文章页面的渲染以及首页链接跳转到相应的文章内容页面。
接下来我们实现评论的渲染和发布功能。
8、设计评论样式
评论版块需要包括两个输入框,一个输入发布评论的人员,一个输入评论的具体内容,点击按钮提交。
<div class="page-header">
<h1 style="font-size: 20px;">发表评论</h1>
</div>
<!-- 使用form表单提交 -->
<form method="POST" action="/submitComment?_id=<%= data._id %>">
<div class="form-group">
<label for="exampleInputEmail1">用户名</label>
<input type="email" class="form-control" id="exampleInputEmail1" name='name' placeholder="用户名">
</div>
<textarea class="form-control" rows="3" name='content'></textarea>
<button type="submit" class="btn btn-default">发布</button>
</form>
根据id来区分评论的是哪一篇文章。
9、在index.js中设计相应的/submitComment接口
先获取地址栏的_id,req.body中的输入框的名字和评论内容,再把获取到的信息插入到commentDB数据库中,插入成功之后刷新页面,把新插入的评论显示出来。
// 注意:post请求需要urlencodedParser
app.post('/submitComment',urlencodedParser,function(req,res){
var _id=req.query._id
var {name,content}=req.body
// console.log(_id,name,content)
// 下面的输入框和发布按钮都得放在form表单中才好使 这样点击提交才是form表单提交
// 把获取到的数据插入到评论数据库中,在刷新页面
commentDB.insertOne({
fid:_id,
content:content,
author:name,
date:moment().format('YYYY-MM-DD HH:mm:ss')
},()=>{
// 插入成功之后,重定向到此页面完成刷新
res.redirect('/p?_id='+_id)
})
})
到这里,我们完成了发布评论的功能,发布文章功能、评论审核功能没有实现。
10、后台管理界面设计
后台页面设计成左右的结构,左边是导航栏,右边显示点击某一个导航的具体内容。
使用bootstrap开发来设计页面。
表单需要的字段有分类、标题、作者、日期(需要手动去生成)、内容、简介。
要需要设计一个属性(data-wrap)让这三个区分开来,并且和右边的三个div建立联系。
<div class="container">
<div class="page-header">
<h1>个人博客管理员页面</h1>
</div>
<div class="row">
<div class="col-lg-3">
<ul class="nav nav-pills nav-stacked">
<li role="presentation" class=" list" data-wrap='home'>
<a href="#">主页
</a>
</li>
<li role="presentation" class="active list" data-wrap='publish'>
<a href="#">发表文章
</a>
</li>
<li role="presentation" class="list" data-wrap='examine'>
<a href="#">审核评论
</a>
</li>
</ul>
</div>
<div class="col-lg-9 wrap">
<div class="wrap-right " id="home">主页</div>
<div class="wrap-right active" id="publish">
<!-- 发表文章 -->
<form>
<div class="form-group">
<label for="title">文章标题</label>
<input type="text" class="form-control" id="title" placeholder="title" name="title">
</div>
<div class="form-group">
<label for="author">作者</label>
<input type="text" class="form-control" id="author" placeholder="author" name="author">
</div>
<div class="form-group">
<label for="sort">分类</label>
<input type="text" class="form-control" id="sort" placeholder="sort" name="sort">
</div>
<div class="form-group">
<label for="intro">简介</label>
<input type="text" class="form-control" id="intro" placeholder="intro" name="intro">
</div>
<label>内容</label>
<textarea class="form-control" rows="3" name="content"></textarea>
<br>
<button type="submit" class="btn btn-default">发布</button>
</form>
</div>
<div class="wrap-right" id="examine">审核评论</div>
</div>
</div>
</div>
11、设计页面样式
先让三个页面全都display: none,和自己id对应的按钮的时候添加class类名,通过改变类名来改变样式。
<style>
.wrap {
position: relative;
}
.wrap-right {
width: 100%;
min-height: 600px;
position: absolute;
display: none;
}
.wrap-right.active {
display: block;
}
</style>
设计完样式,对导航栏挂载监听器监听点击事件。
12、对导航栏挂监听器
先获取左边三个标签和右边对应的三个div
var listItems=$('.list')
var wrapRights=$('.wrap-right')
对标签挂监听器,监听click。
先获取点击的按钮的data-wrap属性值用来选择对应的右侧div,先把之前添加的active类名都删除掉,再去给对应的某一个div添加active类名。
当点击某个导航时,导航的样式也应该变成激活状态。使用es6语法$(this)选中点击出发了本回调函数的按钮,对他添加active类名。
listItems.on('click',function(){
var tag=$(this).attr('data-wrap')
wrapRights.removeClass('active')
var idd='#'+tag
$(idd).addClass('active')
listItems.removeClass('active')
$(this).addClass('active')
})
后台样式彻底完成,接下来设计接口。
13、设计发布文章的接口
在这里我们使用form表单提交,对form添加method和action属性
<form method="POST" action="/admin/submitPages">
</form>
去admin.js中处理接口。
注意:这里处理接口的admin.js和前面给元素挂监听器的admin.js不是同一个文件,只是名字相同。
当访问/admin/接口的时候直接显示博客后台页面,用不着ejs,直接把admin.html通过path的resolve方法发送就行。
router.get('/',function(req,res){
res.sendFile(path.resolve('./static/admin.html'))
})
点击发布文章,跳转到/submitPages接口,接下来处理/submitPages接口。
因为使用的是post提交,要引入urlencodedParser进行解析。
const bodyParser=require('body-parser')
const urlencodedParser=bodyParser.urlencoded({extended:false})
获取我们再输入框中输入的内容(这时候在admin.html中给每一个输入框添加name的好处就出来了。name 属性用于对提交到服务器后的表单数据进行标识),再把获取到的信息插入pageDB数据库(别忘了自己定义的日期date),文章发表成功时,我们重定向到首页查看文章列表就会更新。
(post传递的参数要从req.body中获取)
router.post('/submitPages',urlencodedParser,function(req,res){
var {sort,title,author,content,intro}=req.body
var now=moment().format('YYYY-MM-DD HH:mm:ss')
pageDB.insertOne({
sort:sort,
title:title,
author:author,
content:content,
intro:intro,
date:now
},(result)=>{
if(result.n==1&&result.ok==1){
res.redirect('/')
}
})
})
发布文章接口设计完成。
但现在任何人都能查看后台并且发布文章,所以我们需要一个登录功能进行身份验证。
14、简单设计login页面
使用form表单提交。
<div class="container">
<div class="page-header">
<h1>博客后台登陆</h1>
</div>
<form class="form-horizontal" method="POST" action="/admin/login">
<div class="form-group">
<label for="username" class="col-sm-2 control-label">账号</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="username" name="username" placeholder="账号">
</div>
</div>
<div class="form-group">
<label for="password" class="col-sm-2 control-label">密码</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="password" name="password" placeholder="密码">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">登录</button>
</div>
</div>
</form>
</div>
15、在admin.js处理接口
这里也不需要ejs,直接发送文件
router.get('/login', function (req, res) {
res.sendFile(path.resolve('./static/login.html'))
})
再提交数据时,以POST提交到/login。
先从req.body中提取username,password,用if判断是否正确,如果正确,使用cookie设置一个响应头,在进入后台页面之前把数据设置成登陆成功地标志(设置响应头,在响应头里设置一个令牌,表示已经登陆成功),登陆成功直接跳转到/admin的页面,如果登陆不成功,重定向到登陆页面。
router.post('/login', urlencodedParser, function (req, res) {
var { username, password } = req.body
if (username == 'admin' && password == 'admin') {
res.setHeader('Set-Cookie', ['token=' + token])
res.redirect('/admin')
} else {
res.redirect('/admin/login')
}
})
这种写死了的cookie容易造成泄露,所以我们专门写一个生成、回收cookie字符串的构造器。
class CookieParser{
constructor(){
this.tokenArr=[]
}
getToken(){
var token=''
var str='1234567890qwertyuioplkjhgfdsazxcvbnm'
for(var i=0;i<10;i++){
if(i%3==0){
token+='-'
}
token+=str[parseInt(Math.random()*10)]
}
this.tokenArr.push(token)
return token
}
removeToken(token){
for(var i=0;i<this.tokenArr.length;i++){
if(this.tokenArr[i]==token){
this.tokenArr.splice(i,1)
return true
}
}
return false
}
}
// 引出
module.exports=CookieParser
在admin.js里实例化一个cookie对象,并且对得到的数据进行处理。
const newToken=new cookieParser()
const token = newToken.getToken()
tokenn='token='+token
当直接在地址栏输入/admin接口的时候,还是会进入后台,所以我们需要一个判断,当已经登陆过(设置成功cookie的时候)才能进入后台,否则跳转到登陆页面。
router.get('/', function (req, res) {
if (req.headers.cookie==tokenn) {
res.sendFile(path.resolve('./static/admin.html'))
}else{
res.redirect('/admin/login')
}
})
发布文章的接口也需要进行同样的判断,不能是任何人都能发布。
router.post('/submitPages', urlencodedParser, function (req, res) {
if (req.headers.cookie==tokenn) {
......
})
}else{
res.redirect('/admin/login')
}
})
验证登录实现完成。
接下来实现评论的审核。
为每条评论设计一个状态state,用来标记审核是否通过。
state:0–待审核
state:1–审核通过
state:2–审核不通过
用户发表评论之后,不能直接显示到浏览器上,需要经过管理员的审核,审核通过,更改state为1,审核不通过,更改state为2。
管理员在后台只获取state为0的评论。文章页面渲染的时候,只渲染state为1的评论。
16、更改index.js文件下的/submitComment接口(发表评论的接口)插入数据时,初始化此条评论的状态为0
commentDB.insertOne({
fid:_id,
content:content,
author:name,
date:moment().format('YYYY-MM-DD HH:mm:ss'),
state:0
},()=>{
res.redirect('/p?_id='+_id)
})
17、设计审核评论页面的样式
使用bootstrap。
<div class="wrap-right active" id="examine">
<!-- 审核评论 -->
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">作者:李华</h3>
</div>
<div class="panel-body">
<div class="well well-lg">
评论内容
</div>
评论的文章标题:
<br>
评论的文章简介:
</div>
<div class="panel-footer">
<button type="button" class="btn btn-success">审核通过</button>
<button type="button" class="btn btn-danger">审核不通过</button>
</div>
</div>
</div>
当我们点击审核评论的时候,有两种重新渲染页面的方法:使用ejs重新渲染整个html(适合整个页面渲染);使用Ajax进行前后端交互请求数据(适合部分页面渲染)。
在这里我们使用Ajax。
18、使用Ajax获取数据
先获取审核评论的按钮,对这个按钮挂监听器,点击时执行获取所有待审核的评论的函数
var examineBtn = $('.examine')
examineBtn.on('click',function(){
getComment()
})
设计getComment方法,让它发起Ajax请求,向服务器对应接口获取数据。
var getComment = function () {
$.ajax({
type: 'GET',
url: '/admin/getComment',
data: {},
success: function (result) {
console.log(result)
}
})
}
在后台设计接口,在评论数据库中查找state为0的评论并返回给前端。
这个接口,不但要查找这条评论,还要查找这条评论是属于哪一篇文章的,以便审核,在这我们就只获取父文章的标题和简介。
对获取到的待审核评论数组进行遍历,取出每一项的fid,根据fid在page数据库中根据id查找对应的文章,把文章的title、intro赋值给评论的父标题和父简介,让这个评论添加上这两个属性。
因为只能发送一次,所以得确保所有的数据都已经添加属性完毕,需要设置哨兵变量count==result.length,当所有的评论都添加上了标题和简介时,才能把他们返回。
router.get('/getComment',function(req,res){
commentDB.find({state:0},function(result){
// res.send(result)
var count=0
// 设置哨兵变量
for(var i=0;i<result.length;i++){
let nowData=result[i]
var nowDataFid=nowData.fid;
pageDB.findOneById(nowDataFid,function(pageResult){
var page=pageResult[0]
nowData.f_title=page.title
nowData.f_intro=page.intro
conut++
if(count==result.length){
// 只有当所有的评论都添加上了标题和简介时,才能把评论们返回
res.send(result)
}
})
}
})
})
获取到所有的需要的数据,获取到的数据填充到页面上去fillComment(result)。
19、设计fillComment填充页面方法
var fillComment = function (arr) {
var html = ''
// 通过按钮和不通过按钮都要有一个id来确定通过/不通过的是哪一条评论
arr.forEach(function (item, index) {
html += `
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">作者:${item.author} 日期:${item.date}</h3>
</div>
<div class="panel-body">
<div class="well well-lg">
${item.content}
</div>
评论的文章标题:${item.f_title}
<br>
评论的文章简介:${item.f_intro}
</div>
<div class="panel-footer">
<button type="button" class="btn btn-success btn-pass" data_id="${item._id}">审核通过</button>
<button type="button" class="btn btn-danger btn-nopass" data_id="${item._id}">审核不通过</button>
</div>
</div>
`
// 拼接好字符串之后,把它渲染到页面
$('#examine').html(html)
})
}
实现点击审核评论,获取到待审核的评论。
点击通过/不通过改变对应评论的状态,下面设计通过审核/不通过审核的接口。
20、通过评论接口
router.get('/passComment',function(req,res){
res.setHeader('Access-Control-Allow-Origin','*')
var _id=req.query._id
commentDB.updateOneById(_id,{state:1},function(result){
res.send(result)
})
})
21、不通过评论的接口
// 不通过评论的接口
router.get('/nopassComment',function(req,res){
res.setHeader('Access-Control-Allow-Origin','*')
var _id=req.query._id
commentDB.updateOneById(_id,{state:2},function(result){
res.send(result)
})
})
接口设计完毕,挂监听器,点击通过审核/不通过审核,发起相应的Ajax请求。
22、设计addeventlistener方法
点击通过按钮,获取当前点击的按钮的属性data_id,发起Ajax请求,请求成功,重新获取页面,填充页面,完成页面刷新功能。
var addeventlistener = function () {
$('.btn-pass').on('click', function () {
var thisBtn = $(this).attr('data_id')
$.ajax({
type: 'get',
url: '/admin/passComment',
data: {
_id: thisBtn
},
success: function (result) {
console.log(result)
getComment()
// 通过之后刷新页面
}
})
})
$('.btn-nopass').on('click', function () {
var thisBtn = $(this).attr('data_id')
$.ajax({
type: 'get',
url: '/admin/nopassComment',
data: {
_id: thisBtn
},
success: function (result) {
console.log(result)
// window.location.reload()
getComment()//重新获取数据并填充页面
}
})
})
}
当getComment接口中,查找待审核评论数据result为[ ]时,也就不会执行底下的for循环,也就没有res.send()返回给前端,所以在进入for前判断获取到的结果数组是否为空。
在/getComment接口添加代码:
if(result.length==0){
res.send([])
return
}
填充页面函数fillComment也需要判断获取的数组是否为空。
在fillComment函数中添加代码:
if (arr.length == 0) {
$('#examine').html(`<div class="well well-lg">
没有待审核的评论
</div>`)
return
}
审核评论功能实现完成,还差一个权限功能,不是任何人都可以审核评论。
接下来实现审核评论的权限功能。
前面我们已经实现过权限功能,现在实现起来就比较简单。
23、对评论审核功能设置权限
对和评论有关的三个接口(获取评论、通过评论、不通过评论)添加判断条件,cookie是否对应的上
if (req.headers.cookie == tokenn) {
} else {
res.redirect('/admin/login')
}
我们想要实现的功能已经全部实现。
总结
开发思路整理
对页面来说,由两部分页面:
一个是提供给用户浏览的,用户可以浏览首页、浏览文章、发表评论;
接口有:
|
|—get请求—’/‘首页
|
|—get请求—’/p’每一篇文章的浏览页面
|
|—post请求—’/submitComment’用户提交评论
一个是提供给管理员管理博客的,管理员可以发表文章、审核评论。
接口有:
|
|—get请求—’/admin’后台管理首页
|
|—get请求—’/admin/login’管理员登陆页面
|
|—post请求—’/admin/login’管理员登陆接口
|
|—post请求—’/admin/submitPages’管理员发布文章
|
|—get请求—’/admin/getComment’获取所有待审核评论
|
|—get请求—’/admin/passComment’评论审核通过
|
|—get请求—’/admin/nopassComment’评论审核不通过
接口path | 请求类型 | 发送参数 | 返回结果 |
---|---|---|---|
/ | get | 无 | string |
/p | get | _id:文章的id | string |
/sunmitComment | post | _id:文章的id;name:发布评论的人的名字;content:评论的内容 | 重定向 |
/admin | get | 无 | string |
/admin/login | get | 无 | string |
/admin/login | post | username:账号;password:密码 | 重定向 |
/admin/submitPages | post | 写一篇文章需要的所有参数 | string |
/admin/getComment | get | 无 | json |
/admin/passComment | get | _id:评论的id | json |
/admin/nopassComment | get | _id:评论的id | json |