[Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
程序员文章站
2022-04-30 08:38:44
...
MyBlog实战
项目要求
- a. 前台和后台的页面布局
- 前台要求有首页、列表页、详情页面、登录、注册
- 后台要求有登录页面、列表、添加修改页面
- 页面要求简洁、美观、大方
- b. 后台功能要求
- 前台注册用户在后台的分页展示
- 后台可以对分类进行管理
- 后台可以对文章进行管理
- 后台可以针对文章的评论进行展示
- 后台需要登录才能进入后台管理系统
- c. 前台功能要求
- 首页按照分类展示对应的最新几条文章
- 列表页可以根据不同的分类进行文章列表的切换
- 详情页在登录的前提下,可以对文章进行评论
(未实现,jQuery可能出问题了)
- 前台用户可以正常的登录和注册
项目代码下载
实现效果展示
- 总体效果展示
- 前台注册
- 前台登录
- 前台阅读文章
- 前台文章分类
- 后台版块首页
- 后台版块添加
- 后台版块修改
- 后台版块删除
- 后台内容首页
- 后台内容添加
- 后台内容修改
- 后台内容删除
- 退出系统
准备工作
博客系统解析图
项目目录结构
各个文件的作用
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'
});
});
});
上一篇: LeTex关于数学公式的使用(3)
下一篇: 初步理解vuex
推荐阅读
-
基于jsp+servlet实现的简单博客系统实例(附源码)
-
Ghost:开源的Node.js博客系统
-
Java实战个人博客系统的实现流程
-
Java 实战项目锤炼之朴素风格个人博客系统的实现流程
-
基于CakePHP实现的简单博客系统实例
-
荐 [Node.js] 基于NodeJS+Express+mongoDB+Bootstrap的博客系统实战
-
node.js基于fs模块对系统文件及目录进行读写操作的方法详解
-
「免费开源」基于Vue和Quasar的前端SPA项目crudapi后台管理系统实战之文件上传(十)
-
基于MEAN全栈架构的多用户博客系统(Angular2+Node+MongoDB) Angular2
-
基于MEAN全栈架构的多用户博客系统(Angular2+Node+MongoDB) Angular2