欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战

程序员文章站 2022-04-30 08:38:44
...

MyBlog实战

项目要求

  • a. 前台和后台的页面布局
    • 前台要求有首页、列表页、详情页面、登录、注册
    • 后台要求有登录页面、列表、添加修改页面
    • 页面要求简洁、美观、大方
  • b. 后台功能要求
    • 前台注册用户在后台的分页展示
    • 后台可以对分类进行管理
    • 后台可以对文章进行管理
    • 后台可以针对文章的评论进行展示
    • 后台需要登录才能进入后台管理系统
  • c. 前台功能要求
    • 首页按照分类展示对应的最新几条文章
    • 列表页可以根据不同的分类进行文章列表的切换
    • 详情页在登录的前提下,可以对文章进行评论(未实现,jQuery可能出问题了)
    • 前台用户可以正常的登录和注册

 
 

项目代码下载

MyBlog

 
 

实现效果展示

  • 总体效果展示[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 前台注册[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 前台登录[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 前台阅读文章
    [Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 前台文章分类[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 后台版块首页
    [Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 后台版块添加[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 后台版块修改[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 后台版块删除[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 后台内容首页[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 后台内容添加[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 后台内容修改[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 后台内容删除[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
  • 退出系统[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战

准备工作

博客系统解析图

[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
 

项目目录结构

[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战

各个文件的作用

[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战

js部分

  • 导入模块
var express = require('express');
//加载模板处理模块
var swig = require('swig');
var path = require('path');
var mongoose = require('mongoose');
var bodyParser = require('body-parser');//中间件/可从request中获取body数据
var Cookies = require('cookies');
var User = require('./models/User');
  • 设置静态文件托管
app.use('/public', express.static(__dirname + '/public'));
  • 路由控制
app.use('/', require('./routers/main'));
app.use('/admin', require('./routers/admin'));
app.use('/api', require('./routers/api'));
  • 监听Http请求
mongoose.connect('mongodb://localhost:27017/iBlog', function (err) {
    if (err) {
        console.log('数据库连接失败');
        return;
    }
    else {
        console.log('数据库连接成功');
        app.listen(8081, 'localhost');
        console.log('Server is running at http://localhost:8081');
    }
})

 
 

前台流程

用户注册前端页面

判断用户名是否为空
判断用户名是否被注册过
判断密码不能为空
在输入两次密码是要保持一致

routerApi.post('/user/register', function (req, res) {
    var username = req.body.username;
    var password = req.body.password;
    var repassword = req.body.repassword;

    //用户名判空
    if (username === '') {
        responseData.code = '1';
        responseData.message = '用户名不能为空';
        res.json(responseData);
        return;
    }
    //密码检测,不能为空
    if (password === '' || repassword === '') {
        responseData.code = '2';
        responseData.message = '密码不能为空';
        res.json(responseData);
        return;
    }
    //两次密码需一样
    if (password !== repassword) {
        responseData.code = '3';
        responseData.message = '两次密码不一致';
        res.json(responseData);
        return;
    }
    //检测用户名是否已经被注册,如果数据库存在同名数据表示用户名已经被注册
    User.findOne({
        username: username
    }).then(function (userInfo) {
        if (userInfo) {
            //表示数据库中有该记录
            responseData.code = '4';
            responseData.message = '用户名已被注册';
            res.json(responseData);
            return;
        }
        //保存用户注册的信息到数据库中
        else {
            // 无上述情况//则可注册保存用户注册信息到数据库中//返回注册成功
            var userRegisterData = new User({
                username: username,
                password: password,
                isSuperAdmin: false,
                isAdmin: true
            });
            userRegisterData.save();//
            return;
        }
    }).then(function (newUserInfo) {
        console.log(newUserInfo);//
        responseData.code = '0';
        responseData.message = '注册成功';
        res.json(responseData);
        return;
    });

});

 

用户登录

判断用户名或密码是否被注册过
判断用户名或密码是否正确

routerApi.post('/user/login', function (req, res) {
    var uName = req.body.username;
    var pWord = req.body.password;
    //空值等检测放在前端处理

    //后台数据验证处理
    User.findOne({
        username: uName,
        password: pWord
    }).then(function (userInfo) {
        // console.log(userInfo);
        if (!userInfo) {
            responseData.code = '1';
            responseData.message = '用户名或密码错误';
            res.json(responseData);
            return;
        }
        // else {
        //此处登录验证成功,进入用户首页有两种方式:
        //1、前端根据返回的成功数据进行Url重定向实现//这样相当于二次请求服务器 TODO 思考
        //2、后台 用redirect 进行路由重定向
        // res.redirect('/');
        responseData.code = '0';
        responseData.message = '成功';
        //添加返回用户cookie数据
        responseData.userInfo = {
            _id: userInfo._id,
            username: userInfo.username
        };
        req.cookies.set('userInfo', JSON.stringify({
            _id: userInfo._id,
            username: userInfo.username
        }));
        res.json(responseData);
        // console.log('这里打印 登录成功服务端返回给客户端的 返回信息 ' + responseData);
        return;
        // }
    });
});
function loginFunc() {
    var loginBox = $("#loginBox");
    var userInfoBox = $("#userInfoBox");
    var username = loginBox.find('input[name="username"]').val();
    var password = loginBox.find('input[name="password"]').val();
    //取表单填写数据//
    if (username === '' || password === '') {
        alert('你的信息未填写完整...')
    }
    else {
        //采用 jQuery AJax方式上传
        $.ajax({
            type: 'post',
            url: 'api/user/login',
            data: {
                username: username,
                password: password
            },
            dataType: 'json',
            success: function (resData) {
                if (resData.code === '0') {
                    //登录成功
                    // alert(resData.message);
                    window.location.reload();
                }
                else {
                    alert(resData.message);
                }
            },
            error: function () {
                alert('Error');
            }

        });
    }

}

 

使用cookie保存用户

获取当前登录用户的类型//是否是管理员
只有超级管理员可以进行//用户管理//普通用户//只能进行模块//内容//留言等管理

app.use(function (req, res, next) {
    req.cookies = new Cookies(req, res);
    // console.log('这里打印服务端返回客户端的cookies  ' + req.cookies.get('userInfo'));
    //解析用户登录的cookies信息
    req.userInfo = {};
    if (req.cookies.get('userInfo')) {
        try {
            req.userInfo = JSON.parse(req.cookies.get('userInfo'));
            //获取当前登录用户的类型//是否是管理员
            //只有超级管理员可以进行//用户管理//普通用户//只能进行模块//内容//留言等管理
            User.findById(req.userInfo._id).then(function (userInfo) {
                req.userInfo.isAdmin = Boolean(userInfo.isAdmin);
                req.userInfo.isSuperAdmin = Boolean(userInfo.isSuperAdmin);
                next();
            });
        }
        catch (e) {
            // console.log('Cookies have some Error');
            // next();
        }
    }
    else {
        // console.log('不存在用户cookie 数据!');
        next();
    }
});

 

文章加载获取指定文章的所有评论

查询当前这篇内容的信息

routerApi.get('/comment', function (req, res) {
    var contentID = req.query.contentID || '';
    //查询当前这篇内容的信息
    Content.findOne({
        _id: contentID
    }).then(function (content) {
        responseData.data = content.comments.reverse();
        res.json(responseData);
    });
});

 

留言评论提交

查询当前这篇内容的信息

routerApi.post('/comment/post', function (req, res) {
    //内容的ID
    var contentID = req.body.contentID || '';
    //定义评论 数组中字段
    var postData = {
        username: req.userInfo.username,
        postTime: new Date(),
        content: req.body.content
    }
    //查询当前这篇内容的信息
    Content.findOne({
        _id: contentID
    }).then(function (content) {
        content.comments.push(postData);
        return content.save();
    }).then(function (newContent) {
        responseData.message = "评论成功";
        responseData.data = newContent;
        res.json(responseData);
    });
});

 

渲染当前内容所有的评论内容

判断有没有上一页/下一页
判断有没有评论
在没有评论的时候要保证上一页/下一页要隐藏

function renderComments() {
    var length = 0;
    if (comments !== null) length = comments.length;
    //用于处理评论分页
    pages = Math.ceil(length / limit);
    $lis = $(".pager li");
    $lis.eq(1).html(page + '/' + pages);
    var start = (page - 1) * limit;
    var end = Math.min(start + limit, length);
    if (page < 1) {
        page = 1;
        $lis.eq(0).html('<span>没有上一页了</span>');
    }
    else {
        $lis.eq(0).html('<a href="javascript:;">上一页</a>')
    }
    if (page > pages) {
        page = pages;
        $lis.eq(2).html('<span>没有下一页了</span>');
    }
    else {
        $lis.eq(2).html('<a href="javascript:;">下一页</a>')
    }

    //渲染评论列表
    $("#messageCount").html(length);
    var htmlStr = '';
    if (comments.length === 0) {
        htmlStr += '<div class="messageBox"><p>暂时还没有评论!</p></div>';
        $(".pager").hide();
    }
    else {
        for (var i = start; i < end; i++) {
            var username = (comments[i].username === undefined) ? '游客' : comments[i].username;
            htmlStr += '<div class="messageBox">'
            htmlStr += '<p class="messageLine clear"><span class="floatLeft">' + username + '</span>'
            htmlStr += '<span class="floatRight">' + StringToDate(comments[i].postTime).format('yyyy-MM-dd hh:mm') + '</span> </p>'
            htmlStr += '<p>' + comments[i].content + '</p>'
            htmlStr += '</div>';
        }
    }
    $(".messageList").html(htmlStr);
}

 

绑定上一页/下一页

$('.pager').delegate('a', 'click', function () {
    // alert('点击了 上/下 一页');
    if ($(this).parent().hasClass("previous")) {
        //上一页
        page--;
    } else {
        //下一页
        page++;
    }
    renderComments(comments);
});

 

每次页面重载时获取一下该文章的所有评论

$.ajax({
    type: 'get',
    url: '/api/comment',
    data: {
        contentID: $('#contentID').val()
    },
    success: function (resData) {
        // console.log(resData);
        comments = resData.data;
        renderComments();
    },
    // error: function (err) {
    //     alert(err);
    // }
});

 

程序主入口

router.get('/', function (req, res, next) {
    // console.log('渲染首页模板的用户数据 ' + JSON.stringify(req.userInfo));

    var reqPage = Number((req.query.page) === undefined ? 0 : req.query.page);
    data.category = req.query.category || '';
    data.count = 0;
    //分页
    data.page = reqPage <= 0 ? 1 : reqPage;
    data.limit = 3;
    data.pages = 0;
    //查询筛选条件
    var whereStr = {};
    if (data.category) {
        whereStr.category = data.category;
    }
    //如果用户未登录//游客 则只显示 首页--即无自己定制版块


    //读取某用户所有分类信息
    Category.find().where(whereStr).then(function (count) {

        data.count = count;
        //总页数
        data.pages = Math.ceil(data.count / data.limit);
        //取值不能超过pages
        data.page = Math.min(data.page, data.pages);
        //取值不能小于 1
        data.page = Math.max(data.page, 1);

        var skip = (data.page - 1) * data.limit;
        return Content.where(whereStr).find().limit(data.limit).skip(skip).populate(['category', 'user']).sort({
            addTime: -1
        });

    }).then(function (contents) {
        data.contents = contents;
        // console.log(data);
        res.render('main/mainIndex', data);
    });

});

 

内容详情

在点击阅读全文时,保证获取到当前文章的所有数据

router.get('/view', function (req, res, next) {
    var contentID = req.query.contentID || '';
    Content.findOne({
        _id: contentID
    }).then(function (content) {
        data.content = content;
        // console.log('点击查阅具体内容详情数据 ' + data);
        console.dir(data);

        //用户点击 阅读文章详情,阅读数进行统计写入数据
        content.views++;
        content.save();
        res.render('main/viewDetail', data);
    });
});

时间的处理

利用getMonth()、getDate()、getHours()、getMinutes()、getSeconds()、getMilliseconds()获取当前时间的信息
利用字符串转成日期类型

//时间格式化函数
Date.prototype.format = function (format) {
    var o = {
        "M+": this.getMonth() + 1, //month
        "d+": this.getDate(), //day
        "h+": this.getHours(), //hour
        "m+": this.getMinutes(), //minute
        "s+": this.getSeconds(), //second
        "q+": Math.floor((this.getMonth() + 3) / 3), //quarter
        "S": this.getMilliseconds() //millisecond
    }
    if (/(y+)/.test(format)) {
        format = format.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
    }
    for (var k in o) {
        if (new RegExp("(" + k + ")").test(format)) {
            format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
        }
    }
    return format;
}


//+---------------------------------------------------
//| 字符串转成日期类型
//| 格式 MM/dd/YYYY MM-dd-YYYY YYYY/MM/dd YYYY-MM-dd
//+---------------------------------------------------
function StringToDate(DateStr) {

    var converted = Date.parse(DateStr);
    var myDate = new Date(converted);
    if (isNaN(myDate)) {
        var arys = DateStr.split('-');
        myDate = new Date(arys[0], --arys[1], arys[2]);
    }
    return myDate;
}

 
 

后台流程

管理首页

routerAdmin.get('/', function (req, res, next) {
    // res.send('后台管理首页');
    res.render('admin/adminIndex', {
        userInfo: req.userInfo
    });
});

 

用户管理

routerAdmin.get('/user', function (req, res, next) {
    // 从数据库中读取所有用户数据
    /*
     *   sort() 可对字段指定排序 传值 -1降序 1 升序
     *   对数据进行分页
     *   limit(number) 限制从数据库中取出条数
     *   skip(number) 忽略数据的条数
     *
     *   eg:每页显示2条
     *   第一页:1-2 skip 0  -> 当前页 -1 * limit
     *   第二页:3-4 skip 2 ->
     *   ...
     *
     * */
    // var page = 1;
    // console.log(req.query.page);
    var reqPage = Number((req.query.page) === undefined ? 0 : req.query.page);
    // console.log(reqPage);
    var page = reqPage <= 0 ? 1 : reqPage;
    var limit = 2;
    var pages = 0;
    var skip = (page - 1) * limit;
    //
    User.count().then(function (count) {
        // console.log(count);
        //总页数
        pages = Math.ceil(count / limit);
        //
        User.find().sort({_id: -1}).limit(limit).skip(skip).then(function (users) {
            // console.log(users);
            res.render('admin/user_index', {
                userInfo: req.userInfo,
                users: users,
                count: count,
                limit: limit,
                pages: pages,
                page: page
            });
        });
    });

});

 

分类首页

routerAdmin.get('/category', function (req, res, next) {
    // 从数据库中读取所有分类数据
    var reqPage = Number((req.query.page) === undefined ? 0 : req.query.page);
    var page = reqPage <= 0 ? 1 : reqPage;
    var limit = 2;
    var pages = 0;
    var skip = (page - 1) * limit;
    //
    Category.count().then(function (count) {
        // console.log(count);
        //总页数
        pages = Math.ceil(count / limit);
        //
        Category.find().sort({_id: -1}).limit(limit).skip(skip).then(function (categories) {
            // console.log('分类首页回显数据  ' + categories);
            res.render('admin/category_index', {
                userInfo: req.userInfo,
                categories: categories,
                count: count,
                limit: limit,
                pages: pages,
                page: page
            });
        });
    });


});

 

分类添加页面

routerAdmin.get('/category/add', function (req, res, next) {
    res.render('admin/category_add', {
        userInfo: req.userInfo
    });
});

 

分类添加数据上传

判断名字是否为空
判断分类是否存在

routerAdmin.post('/category/add', function (req, res, next) {
    // console.log(req.body);
    var name = req.body.name || '';
    if (name === '') {
        res.render('admin/error', {
            userInfo: req.userInfo,
            message: '名称不能为空',
            url: ''
        });
    }
    else {
        //名称部位空//验证数据库只能怪是否存在
        Category.findOne({
            name: name
        }).then(function (resData) {
            if (resData) {
                //数据库中已经存在
                res.render('admin/error', {
                    userInfo: req.userInfo,
                    message: '分类已经存在'
                });
            }
            else {
                //不存在则写入数据
                return new Category({
                    name: name
                }).save();
            }
        }).then(function (newCategory) {
            //返回新的分类数据
            res.render('admin/success', {
                userInfo: req.userInfo,
                message: '分类保存成功',
                url: '/admin/category',
                categories: newCategory
            });
        });
    }

});

 

分类编辑

判断分类信息是否存在

routerAdmin.get('/category/edit', function (req, res) {
    //获取要修改的数据//以表单形式展现出来
    var id = req.query.id || '';
    Category.findOne({
        _id: id
    }).then(function (category) {
        if (!category) {
            res.render('admin/error', {
                userInfo: req.userInfo,
                message: '分类信息不存在'
            });
            // Promise.reject(reason)方法返回一个用reason拒绝的Promise
            return Pramise.reject();
        }
        else {
            res.render('admin/category_edit', {
                userInfo: req.userInfo,
                category: category
            });
        }
    });

});

 

分类修改 保存

判断数据库是否有数据
用户没有做任何修改提交时候 判断处理【可放在前端判断】
要修改的分类名称是否在数据库中已存在

routerAdmin.post('/category/edit', function (req, res) {
    //获取要修改的分类信息
    var id = req.query.id || '';
    //获取Post提交过来的 修改的名称
    var newName = req.body.name || '';

    //判断数据库是否已有
    Category.findOne({
        _id: id
    }).then(function (category) {
        if (!category) {
            res.render('admin/error', {
                userInfo: req.userInfo,
                message: '分类信息不存在'
            });
            return Pramise.reject();
        }
        else {
            // 当用户没有做任何修改提交时候 判断处理【可放在前端判断】
            if (newName === category.name) {
                res.render('admin/error', {
                    userInfo: req.userInfo,
                    message: '修改成功',
                    url: '/admin/category'
                });
            }
            else {
                //要修改的分类名称是否在数据库中已存在
                return Category.findOne({
                    _id: {$ne: id},
                    name: newName
                });
            }

        }
    }).then(function (sameCategory) {
        if (sameCategory) {
            res.render('admin/error', {
                userInfo: req.userInfo,
                message: '数据库中已经存在同名分类',
            });
            return Pramise.reject();
        }
        else {
            return Category.update({
                    //条件-当前ID
                    _id: id
                }, {
                    //修改的内容- 更新的名称
                    name: newName
                }
            );
        }
    }).then(function () {
        res.render('admin/success', {
            userInfo: req.userInfo,
            message: '修改成功',
            url: '/admin/category'
        });
    });


});

 

分类删除

获取要删除的分类ID

routerAdmin.get('/category/delete', function (req, res) {
    //获取要删除的分类ID
    var id = req.query.id || '';
    Category.remove({
        _id: id
    }).then(function () {
        res.render('admin/success', {
            userInfo: req.userInfo,
            message: '删除成功',
            url: '/admin/category'
        })
    });
});

 

内容管理首页

routerAdmin.get('/content', function (req, res, next) {
    // res.render('admin/content_index',{});
    // 从数据库中读取所有分类数据
    var reqPage = Number((req.query.page) === undefined ? 0 : req.query.page);
    var page = reqPage <= 0 ? 1 : reqPage;
    var limit = 2;
    var pages = 0;
    var skip = (page - 1) * limit;
    //
    Content.count().then(function (count) {
        // console.log(count);
        //总页数
        pages = Math.ceil(count / limit);
        //
        Content.find().sort({addTime: -1}).limit(limit).skip(skip).populate('category').then(function (contents) {
            // console.log('分类首页回显数据' + contents);
            res.render('admin/content_index', {
                userInfo: req.userInfo,
                contents: contents,
                count: count,
                limit: limit,
                pages: pages,
                page: page
            });
        });
    });
});

 

内容添加

routerAdmin.get('/content/add', function (req, res, next) {
    //内容添加//下拉选择分类//从数据库取出分类数据
    Category.find().sort({_id: -1}).then(function (categories) {
        res.render('admin/content_add', {
            userInfo: req.userInfo,
            categories: categories
        });
    });
});

 

内容添加 数据上传

routerAdmin.post('/content/add', function (req, res, next) {
    //
    var postData = req.body;
    // console.log('添加内容传入的数据' + postData.category);
    // console.dir(postData.category);
    //字段检测等可放前端检测
    //前端检测 可对输入框等 进行响应交互等处理
    if (postData.category === '' || postData.title === '' || postData.content === '') {
        res.render('admin/error', {
            userInfo: req.userInfo,
            message: '有未填写的信息'
        });
        return;
    }
    else {
        //数据写入到数据库
        var newContent = new Content({
            category: postData.category,
            user: req.userInfo._id.toString(),
            title: postData.title,
            description: postData.description,
            content: postData.content
        });
        // console.log(newContent);
        newContent.save().then(function (rs) {
            res.render('admin/success', {
                userInfo: req.userInfo,
                message: '内容数据保存成功',
                url: '/admin/content'
            });
        });
    }
});

 

修改内容

判断内容信息是否存在

routerAdmin.get('/content/edit', function (req, res, next) {
    //
    var id = req.query.id || '';
    var resCategories = {};
    Category.find().sort({_id: -1}).then(function (categories) {
        resCategories = categories;
        return Content.findOne({
            _id: id
        }).populate('category').then(function (content) {
            if (!content) {
                res.render('admin/error', {
                    userInfo: req.userInfo,
                    message: '内容信息不存在'
                });
                // Promise.reject(reason)方法返回一个用reason拒绝的Promise
                return Pramise.reject();
            }
            else {
                res.render('admin/content_edit', {
                    userInfo: req.userInfo,
                    categories: resCategories,
                    content: content
                });
            }
        });
    });

});

 

保存修改内容

routerAdmin.post('/content/edit', function (req, res, next) {
    //
    var id = req.query.id || '';
    var postData = req.body;
    // console.log('添加内容传入的数据' + postData.category);
    //字段检测等可放前端检测
    //前端检测 可对输入框等 进行响应交互等处理
    if (postData.category === '' || postData.title === '' || postData.content === '') {
        res.render('admin/error', {
            userInfo: req.userInfo,
            message: '有未填写的信息'
        });
        return;
    }
    else {
        //保存数据到数据库
        Content.update({
            //条件
            _id: id
        }, {
            //更新的数据字段
            category: postData.category,
            title: postData.title,
            description: postData.description,
            content: postData.content
        }).then(function () {
            res.render('admin/success', {
                userInfo: req.userInfo,
                message: '内容数据修改成功',
                //保存成功可跳转到指定Url页面 eg:内容展示详情页面
                // url: '/admin/content/edit?id=' + id
                url: '/admin/content'
            });
        });
    }

});

 

内容删除

routerAdmin.get('/content/delete', function (req, res, next) {
    var id = req.query.id || '';
    Content.remove({
        //删除的条件
        _id: id
    }).then(function () {
        res.render('admin/success', {
            userInfo: req.userInfo,
            message: '删除成功',
            url: '/admin/content'
        });
    });
});