当前位置: 首页 > 工具软件 > blog-node > 使用案例 >

个人博客项目——Node.js+MongoDb+express

鲜于光赫
2023-12-01

博客项目做完有一段时间了,今天来回顾一下项目的完整流程及实现,顺便复习一下有关知识点。

一、博客架构分析

若想要实现一个能够满足客户要求的项目,不仅要注重细节的实现,更要在项目最初就设计好各模块以及各个接口,按照逻辑逐一实现其功能。
对于博客项目我们可以分为两大部分:一、前台(展示给客户的)二、后台服务器(提供给管理者数据)。
接口描述及定义
前台:
① 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
    }
}

项目到此就设计完成啦~

 类似资料: