博客项目做完有一段时间了,今天来回顾一下项目的完整流程及实现,顺便复习一下有关知识点。
一、博客架构分析
若想要实现一个能够满足客户要求的项目,不仅要注重细节的实现,更要在项目最初就设计好各模块以及各个接口,按照逻辑逐一实现其功能。
对于博客项目我们可以分为两大部分:一、前台(展示给客户的)二、后台服务器(提供给管理者数据)。
接口描述及定义
前台:
① get ’ /’ 提供首页
② get ‘/p’ 提供某篇文章的描述
③ post ’/submitComment‘ 表单提交文章评论
④get ‘/message’获取留言板
⑤post‘/submitMessage’表单提交留言内容
后台:
①get ‘/admin/’ 后台管理首页
②get’/admin/login‘ 管理员登录界面
③post’/admin/login‘ 登录接口
④get ‘/admin/getComment’ 获取评论
⑤get ‘/admin/passComment’ 通过评论
⑥get ‘/admin/nopassComment’ 不通过评论
⑦post ‘/Page’发布文章
在我们正式写这个项目之前先来思考几个个问题:
前端的渲染方式可以有几种呢?
①利用a标签,手动写N个页面,虽然可以实现,但这无疑是不可取的…
②利用ajax(它不能实现跨页) 跳到某接口后,在此页面拉取数据,利用字符串拼接
③利用ejs实现点击跳转。
现在我们想要实现的应该是:一个博客首页,点击页面的某篇文章,就实现到此文章页面的跳转,那无疑我们要运用第三种方法。
首先我们引入express模块,然后app=express()
第一部分:前台’/'接口+页面实现:
在我们正式写此项目前,明确一下要用到的模块
1.express模块:
const express = require('express') //express模块的引入
const app = express() //应用express模块
2.MongoControl模块:(此模块封装方法见通讯录项目博文)
const MongoControll = require('./tools/MongoControll').MongControll //引入mongocontrol control
var blogPage = new MongoControll('blog', 'page') //创建博客文章
var comment = new MongoControll('blog', 'comment') //评论
var content = new MongoControll('blog', 'content') //评论内容
var message=new MongoControll('blog','message') //留言板
3.body-parser模块
const bodyParser = require('body-parser') //解析post请求
const unlencodeParser = bodyParser.urlencoded({ extended: true })
4.ejs模块
const ejs = require('ejs') //用于大型后端页面渲染,存在于多页面跳转 渲染整体html
1.首页
因为我们要在首页渲染出博文的某些信息,所以在处理get‘/’请求时,首先要去数据库中读取我们的文章相关内容,(利用Mongodb的find方法,在读取到数据之后便可利用该数据,使用ejs方法实现页面的渲染
app.get('/', function(req, res) { //首页渲染
blogPage.find({}, (err, data) => {
if (err) {
res.status(500).send(err)
console.log(err)
return
}
ejs.renderFile('./ejs-tpl/index.ejs', { data: data }, (err, html) => { //利用ejs渲染
if (err) {
res.status(500).send(err)
console.log(err)
}
res.send(html) //渲染整个页面
})
})
})
值得注意的是:在利用ejs渲染时,如果我们的data数据为一个数组,那就利用forEach属性遍历填充页面,填充格式如下:
<ul class="list-group">
<% data.forEach(function(e){%>
<li class="list-group-item">
<a style="text-decoration: none;" href="/p?_id=<%=e._id %>">
<div class="red">前往阅读♥</div>
</a>
<div class="content">
<h3>
<%=e.title%>
</h3>
<h5>日期:
<%=e.date%>
</h5>
<p>
<%=e.intro%>
</p>
</div>
<div class="hideen">
</div>
</li>
<% })%>
2.get ‘/p’跳转到 某篇文章的描述页面:
上部分已经阐述了首页的渲染过程,在我们利用ejs实现页面渲染的时候,利用a标签+monggodb.find()将id作为它的跳转地址的携带参数,明白基本原理,回到node.js文件中,我们利用app.get() 中的req.query直接解析出来其中的参数——>即为在数据库中的存储id,调用mongodb的findById方法,即可获得相关data,但这里和上部分有一些不同,我们不仅要渲染出文章,还要渲染出对应的评论内容,那么我们在利用query中的id实现渲染文章的同时,还要利用它到数据库中查找到相应的评论内容,然后依旧老套路利用ejs渲染。有一个注意点:在用ejs进项渲染时,通过monggo中的方法,数据都是数组形式存储的,如果我们获取出来的数据若为多项则利用forEach方法去遍历数组,否则直接引用其第一项,利用第一项直接渲染
代码:
app.get('/p', function(req, res) { //a标签点击跳转时,页面间跳转 依旧为ejs整体渲染
var id = req.query._id //在a标签中 将想要获取的文章的id携带于头 并利用req获取
blogPage.findById(id, (err, result) => { //查找
if (err) {
res.status(500).send(err)
console.log(err)
return
}
if (result.length == 0) {
res.status(404).send('欢迎来到我的秘密花园')
}
var data = result[0] //利用ejs渲染时 直接引用取零项 每页文章内容是固定 所以直接引用零项 .
//但是获取评论并渲染到页面时 有多条评论 ,用foreach遍历 不用零项
comment.find({ comment_id: id, status: 1 }, function(err, result) {
var commentes = result
ejs.renderFile('./ejs-tpl/page.ejs', { data: data, commentes: commentes }, (err, html) => {
if (err) {
res.status(500).send(err)
console.log(err)
}
res.send(html)
})
})
})
})
3.post ’/submitComment‘ 用户提交评论
在提交评论的时候,采用表单POST提交数据,利用中间件解析我们的req.body 从而获得对应的文章信息,(获得文章信息的目的是为了将评论插入到数据库当中,插入时要带着评论文章的id才能保证日后渲染评论区时数据相对应),更新完数据库后我们利用redirect重定向到当前页面。
代码:
app.post('/submitComments', unlencodeParser, function(req, res) {
//提交评论时 为post表单
var id = req.query._id
//解析的body来源于name属性(ejs渲染时 将id设置为当前数据库内的id值)
var { content, email ,name} = req.body
console.log(id, content, email,name)
if (!id) {
res.status(404).send('错误!')
return
}
// if (!email || !content||!name) {
// res.status(404).send('错误s!')
// return
// }
comment.insertOne({ //将评论插到数据库当中
date: moment().format('YYYY-MM-DD HH-mm-ss'),
comment_id: id,
author: email,
content: content,
name:name,
status: 0
}, (err, result) => {
if (err) {
res.status(500).send('错误a')
return
}
res.redirect('/p?_id=' + id) //插入之后 重定向到当前页面 =一个页面刷新
})
})
PS:由于首页是利用ejs渲染而来的,所以我们静态不使用index.html但是我们还要静态使用其他的如js、css文件,那么就可以利用此语句:
app.use(express.static('./static', { //静态不用index.html
index: false
}))
至此,前台所有接口已经实现,接下来看后台。
**
后台接口
我们统一以/admin/XXX的形式,那么,为了代码模块化,我们可以将此模块写于另一个node.js文件,实现方法如下:
app.use('/admin', require('./admin'))
1.get’/admin/login‘ 管理员登录界面
这个接口的实现没有什么复杂的,此接口直接发送一个html文件,进行账号密码的输入从而验证身份。
2.post’/admin/login‘ 登录接口
我们在登录界面设置了一个form表单,输入账号密码后,进行检验用户身份,从而决定是否跳转到后台管理页面,验证的逻辑这里设置的很简单直接解析出username和password进行检验就可以了,检验成功后,为其设置一个cookie,因为http无状态,所以利用cookie检验用户身份;检验不成功的话,直接重定向回当前页面。
3.'/Page’后台发布文章模块
这里和上述的前台发布评论功能没有什么大区别,都是利用mongo的insertOne方法,将文章内容插入到数据库当中,然后liyongejs渲染页面。有一点不同的是,为了保证安全性,我们要在每一次插入前进行cookie的身份验证,只有验证成功,才可完成插入操作,(cookie生成、验证在下文会说明)
//后台发布文章模块管理
router.post('/Page', unlencodeParser, function(req, res) { //处理表单提交的文章
if (admin.checkToken(req.cookies.token)) {
var { title, author, sort, intro, content } = req.body
var date = moment().format('YYYY-MM-DD HH-mm-ss') //生成时间
blogPage.insertOne({ title: title, author: author, sort: sort, intro: intro, date: date, content: content }, (err, result) => {
if (err) {
console.log(err)
}
ejs.renderFile('./ejs-tpl/admin.ejs', (err, html) => {
res.send(html) //将html文档发过去
})
})
} else {
res.redirect('/admin/login')
}
})
4.审核评论接口,get ‘/getComment’(重点)
为了维护博客的环境,我们不可能让人随意评论,在评论展示前,必须对评论进行审核,所以在插入评论时,为评论设置一个初始状态status:0,在审核评论时 我们只用find方法查找status为0的评论,此外,有一个很关键的点,就是我们在后台进行审核评论时,我们需要知道每一条评论来自于哪篇文章(包括文章的内容、标题、简介),那么在数据库查找数据的过程中,就要将此类数据也附加到当前评论上,所以要在comment.find({status:0})的基础之上,再去通过此评论的所在文章id去进一步查找,但是这里问题就来了,在我们实现blogPage.fingById(commentID)时,我们要查找N条数据的与父文章相关的所有信息,现在想要把查找到的每条信息都res.send()到相应的评论展示出来,但是res.send()只有一次,直接写在循环内部会导致只发送出去一条数据,而写在循环外部也不可以,因为是异步的,它并不会等待所有的都执行完在统一发送出去,综上 我们在循环内部设置一个哨兵,每次查找数据都令他++,当他等于数据长度再统一发送出去 ,此外 我们的循环要利用for (let i = 0; i < data.length; i++)即利用let去写这个循环体。
这里要补充一点额外的知识了,先来看两段代码:
//使用var声明,得到3个3
var a = [];
for (var i = 0; i < 3; i++) {
a[i] = function () {
console.log(i);
};
}
a[0](); //3
a[1](); //3
a[2](); //3
//使用let声明,得到0,1,2
var a = [];
for (let i = 0; i < 3; i++) {
a[i] = function () {
console.log(i);
};
}
a[0](); //0
a[1](); //1
a[2](); //2
首先需要明确var和let的区别:
1.var声明变量是函数作用域,而let声明变量是语句块作用域;
2.var提升到函数定义顶部,此处是全局作用域顶部;let提升到语句块顶部,此处是for循环第一行。
3.for( let i = 0; i< 5; i++) 这句话的圆括号之间,有一个隐藏的作用域(用var时没有)。 在每次执行循环体之前,JS 引擎会把 i 在循环体的上下文中重新声明及初始化一次。
在运行函数时
1.i的声明被提升。
2.当运行for循环时为i赋值。
3.当声明a[i]时( 注意:此时for循环执行完了),现在需要控制台打印i的值,于是i便沿着作用域链寻找它的值。
4.当用var声明时,i会在全局作用域中找到它的值,为5.
5.当用let声明时,i会在for的第一行找到它的值,每次的值不一样,分别为0、1、2、3、4.
—————————————————————————————
接下来来看下我们的代码
router.get('/getComment', (req, res) => {
// res.setHeader('Access-Control-Allow-Origin', '*') //设置跨域
if (admin.checkToken(req.cookies.token)) {
comment.find({ status: 0 }, (err, data) => { //查找待审核的评论
var count = 0
if (data.length == 0) {
res.send([]) //没有待审核的评论 发送空
return
}
for (let i = 0; i < data.length; i++) {
let nowData = data[i]
// console.log(nowData, '这是数据')
let commentID = nowData.comment_id //每个评论有一个父类 为每个数据渲染时应携带父类信息获取现在的数据的父id
blogPage.findById(commentID, (err, result) => {
//查找父类 信息 父类信息只有一条取0位
// console.log(result.length)
var result = result[0]
nowData.content_parent = result.content
nowData.title_parent = result.title
nowData.intro_parent = result.intro
// res.send(data)只能send一次 直接写就会出现只送出去一条数据的现象,
//写在循环外也不行 因为异步,他不会等待所有都执行完在发送会导致还没执行完循环就发送了
//所以设置哨兵标志
count++
if (count == data.length) {
res.send(data)
console.log(data)
}
})
}
})
} else {
res.send('你没有权限')
return
}
})
至此,后端服务器的响应已经写好,因为这里是一个ajax请求,所以我们要想实现前后端交互就还要再html的js文件中发起ajax请求,并利用这些服务器端发送的数据渲染页面,依旧类似于通讯录项目,利用jQuery去实现ajax:
我们实现一个getComment函数,在此中发起ajax请求:
var getComment = function() {
$.ajax({
type: 'get',
url: '/admin/getComment',
data: {},
success: function(res) {
console.log(res)
renderComment(res)
}
})
}
通过此代码我们也知道,请求到数据后要利用此数据进行渲染,那么就在success中调用renderComment方法。
renderComment方法又是一个经典的ajax请求成功渲染函数,依旧是利用字符串的拼接,通过对res(即上述后台服务器发来的data,它是一个数组的形式),利用forEach方法遍历该数组,将我们页面的相应区进行替换,最后将该字符串赋值给相应的html区。
var renderComment = function(array) {
var html = ''
console.log(array)
if (array.length == 0) {
html = `没有人想评论你写的东西`
$('.shenhe-p').html(html)
}
array.forEach(element => {
html += `
<div class="panel-heading">
<h3 class="panel-title">作者:${element.author}</h3>
</div>
<div class="panel-body shenhe">
<div class="wall">评论的内容:${element.content}</div>
<div class="wall">评论的时间:${element.date}</div>
</div>
<div class="panel-heading">
</div>
<div class="wall">评论的文章标题:${element.title_parent}</div>
<div class="wall">评论的文章的简介:${element.intro_parent}</div>
<div class="wall">评论的文章内容:${element.content_parent}</div>
<div class="panel-footer">
<div class="btn-group" role="group" aria-label="...">
<button type="button" class="btn btn-default btn-pass btn-success" data-id=${element._id}>通过</button>
<button type="button" class="btn btn-default btn-nopass btn-danger" data-id=${element._id}>不通过</button>
</div>
</div>`
});
// console.log(html)
$('.shenhe-p').html(html)
addEventListener()
}
最后我们对当前页面中的‘审核评论‘按钮挂载监听器,每次点击都触发上述函数,发起ajax请求。
5.get ‘/admin/passComment’ 通过评论
这里的实现很简单,即通过更改status的值实现通过与否,若通过则为1,不通过为2,初始未审核为0。知道原理我们也就清楚代码的实现思维:当服务器端接收到ajax请求,利用update方法,将其状态码更新,以passComment为例:
router.get('/passComment', (req, res) => { //通过审核的时候
res.setHeader('Access-Control-Allow-Origin', '*')
if (admin.checkToken(req.cookies.token)) { //注意cookie身份验证
var id = req.query.id
comment.updateById(id, { status: 1 }, (err, result) => { //更新id
res.send({ result: 'ok' })
})
} else {
res.send('你没有权限')
return
}
})
此外。细心的伙伴肯定已经发现了之前的评论渲染代码中运用了一个addEventListner()方法,这是什么功能呢?目的其实就是为了接下来两部分的实现,我们设置评论内容区其实是为了审核其通过与否,那么就要两个button从而实现对评论状态的设定,这依旧是在单页面操作,所以依旧是ajax请求,但是我们要知道一点,我们的评论区也是通过渲染得来的,所以我们若想对评论后的内容button设置监听器就要在渲染后挂载此事件,这也就是addEventListner存在的意义。
var addEventListener = function() {
$('.btn-pass').on('click', function() {
console.log($(this).attr('ll'))
passComment($(this).attr('data-id')) //属性设置为=号
})
$('.btn-nopass').on('click', function() {
console.log($(this).attr('data-id'))
nopassComment($(this).attr('data-id'))
})
接下来就是这几个与ajax有关的函数的实现了
var passComment = function(id) {
$.ajax({
type: 'get',
url: '/admin/passComment',
data: {
id: id
},
success: function(res) {
getComment()
console.log(res)
renderComment(res)
}
})
}
PS:一定要注意请求成功后要进行渲染页面
⑥get ‘/admin/nopassComment’ 不通过评论
三、cookie生成检验:
为了安全性以及方便性,我在这里设置了一个cookie Class
这里实现了几个方法
①:
getToken:
getToken() { //生成token的方法
var token = ''
var str = '123456789qwertyuiopasdfghjklzxcvbnm' //在字符串中获取字符
for (var i = 0; i < 16; i++) {
if (i % 5 == 0 && i < 16) {
token += '-'
}
token += str[parseInt(Math.random() * str.length)] //生成一个随机的16位cookie
}
this.tokenArr.push(token) //放到数组中
return token
}
2、检查token
checkToken(token) { //检查
for (var i = 0; i < this.tokenArr.length; i++) {
if (this.tokenArr[i] == token) {
return true
}
}
return false
}
3、移除cookie
removeToken(token) {
for (var i = 0; i < this.tokenArr.length; i++) {
if (this.tokenArr[i] == token) {
this.tokenArr[i].splice(i, 1)
return true
}
}
return false
}
}
项目到此就设计完成啦~