NodeJS简易博客系统(八)功能需求描述及用户模块实现
一、功能需求描述
用一张导图来说明:
二、页面设计
页面设计如下:
三、梳理下整个系统的业务流程
对这个小项目进行业务流程的梳理,流程图大致如下:
四、用户模块实现
1、数据库设计及代码
(1)用户表(users)
(2)博文分类表(categories)
(3)博文评论列表(contents)
从title往下依次是博文标题,分类,浏览次数,所属用户id,评论列表(评论内容,评论所属用户id),发表时间,文章描述,文章详情,数据库版本。
2、用户模块
由上述流程图得用户模块有登录、注册、博文列表、阅读原文及评论功能。
(1)页面代码
首页的界面使用的是bootstrap+jquery框架设计,首页总共有三个div块,一个是header,content-details,login-register。下面是页面的代码:
main_template.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>简单博客系统</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <link rel="stylesheet" type="text/css" href="/public/css/bootstrap.css"> <link rel="stylesheet" type="text/css" href="/public/css/index.css"> <script type="text/javascript" src="/public/js/jquery.js"></script> <script type="text/javascript" src="/public/js/bootstrap.js"></script> <script type="text/javascript" src="/public/js/index.js"></script> </head> <body> <header> <div class="container-fluid header1"> <span>NodeJS简单博客系统</span> </div> <nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> </div> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> {%if category == ''%} <li><a href="/" class="focus">首页</a></li> {%else%} <li><a href="/">首页</a></li> {%endif%} {%for cate in categories%} {%if category == cate.id%} <li><a href="/?category={{cate.id}}" class="focus">{{cate.name}}</a></li> {%else%} <li><a href="/?category={{cate.id}}">{{cate.name}}</a></li> {%endif%} {%endfor%} </ul> </div> </div> </nav> </header> <section> <div class="container"> <div class="row"> <div class="col-lg-8 col-md-8 moveup" id="content"> <!--文章列表block--> {% block content %} {% endblock %} </div> <div class="col-lg-4 col-md-4" > {% if userInfo._id %} <div class="userinfo spindown" id="userinfo" > <h2>用户信息</h2> <h3 class="account">用户名:{{userInfo.username}}</h3> {% if userInfo.isadmin %} <p>您好,管理员! <a href="/admin/"> 点击这里</a>进入管理页面</p> {% else %} <p>你好,欢迎光临我的博客!</p> {% endif %} <p><a href="javascript:;" class="logout">退出</a></p> </div> {% else %} <div class="register spindown" id="register" style="display: none"> <h2>注册</h2> <div class="line"> <span>用户名:</span> <input type="text" name="username" title="username"> </div> <div class="line"> <span>密码:</span> <input type="password" name="password" title="password"> </div> <div class="line"> <span>确认:</span> <input type="password" name="repassword" title="repassword"> </div> <div class="line"> <input type="submit" name="submit" value="注册" > </div> <p class="warning"></p> <p>已有账号? <a href="javascript:;">点击登录</a></p> </div> <div class="login spindown" id="login" > <h2>登录</h2> <div class="line"> <span>用户名:</span> <input type="text" name="username" title="username"> </div> <div class="line"> <span>密码:</span> <input type="password" name="password" title="password"> </div> <div class="line"> <input type="submit" name="submit" value="登录" > </div> <h2 class="warning"></h2> <p>还没注册? <a href="javascript:;">点击注册</a></p> </div> {% endif %} </div> </div> </div> </section> <footer> <p>Copyright © 小马实验室 | 京ICP备11951015号 | 京公网安备11011105210084</p> <a href="#"><span class="glyphicon glyphicon-arrow-up"></span></a> </footer> </body> </html> |
文章详情article_detail.html
{% extends "main_template.html" %} {% block content %} <div class="papers"> <h2>{{contents.title}}</h2> <p class="paperabout"> 作者:<span class="paperinfo">{{contents.user.username}}</span>- 时间:<span class="paperinfo">{{contents.addtime|date('Y-m-d H:i:s', -8*60)}}</span>- 阅读:<span class="paperinfo">{{contents.num}}</span>- 分类于:<span class="paperinfo">{{contents.category.name}}</span>- 评论:<span class="paperinfo">{{contents.comment.length}}</span> </p> <dfn><p>{{contents.composition}}</p></dfn> <div class="readmore"><a href="javascript:window.history.back()">返回</a></div> </div> <div id="comment"> <h3 ><strong>评论 </strong> <span class="much"> 共 <em id="commentCount">0</em> 条评论</span></h3> <div style="font-size: 22px;"> <div> <textarea name="name" id="commentarea" placeholder="请填写评论" style="height: 150px;width: 100%;"></textarea> <input type="hidden" id="contentid" name="contentid" value="{{contents.id}}"> </div> <button type="submit" id="addcomment" class="btn btn-primary btn-lg">发表评论</button> </div> {% if userInfo._id %} {% else %} <h4 class="loginfo" >你还没有登录,请先登录!</h4> {% endif %} {% if contents.comment.length == 0 %} <h4 class="loginfo" >暂无评论,赶紧来评论吧!</h4> {% endif %} <div id="commentlist"> </div> <div class="pages"> <a id="prevpage" style="float: left;"><span>上一页</span></a> <span id="currentpage"></span> / <span id="totalpage"></span> <a id="nextpage" style="float: right;"><span>下一页</span></a> </div> </div> {% endblock %} |
首页index.html
<!--首页--> {% extends "main_template.html" %} {% block content %} {% for content in contents %} <div class="papers"> <h2>{{content.title}}</h2> <p class="paperabout"> 作者:<span class="paperinfo">{{content.user.username}}</span>- 时间:<span class="paperinfo">{{content.addtime|date('Y-m-d H:i:s', -8*60)}}</span>- 阅读:<span class="paperinfo">{{content.num}}</span>- 分类于:<span class="paperinfo">{{content.category.name}}</span>- 评论:<span class="paperinfo">{{content.comment.length}}</span> </p> <dfn><p>description:{{content.description}}</p></dfn> <div class="readmore"><a href="/article?contentid={{content.id}}">阅读全文</a></div> </div> {% endfor %} <div class="pages"> <a href="/?category={{category}}&page={{page-1}}" style="float: left;"><span>上一页</span></a> <span>{{page}}</span> / <span>{{pages}}</span> <a href="/?category={{category}}&page={{page+1}}" style="float: right;"><span>下一页</span></a> </div> {% endblock %} |
(2)页面对应的js、css代码
index.js
$(function(){ var loginbox = $("#login"); var registerbox = $("#register"); var userinfobox = $("#userinfo"); loginbox.find("a").on("click",function(){ loginbox.hide(); registerbox.show(); }); registerbox.find("a").on("click",function(){ loginbox.show(); registerbox.hide(); }); registerbox.find("input[name='submit']").on("click",function(){ $.ajax({ type: "post", url: "/api/user/register", dataType: "json", data: { username: registerbox.find('input[name="username"]').val(), password: registerbox.find('input[name="password"]').val(), repassword: registerbox.find('input[name="repassword"]').val()}, success :function(result){ console.log(result); registerbox.find(".warning").html(result.message); setTimeout(function(){ registerbox.find(".warning").html(""); },1500); if(!result.code){ setTimeout(function(){ loginbox.show(); registerbox.hide(); registerbox.find('input[name="username"]').val(""); registerbox.find('input[name="password"]').val(""); registerbox.find('input[name="repassword"]').val(""); },1500); } } }); }); loginbox.find("input[name='submit']").on("click",function(){ $.ajax({ type: "post", url: "/api/user/login", dataType: "json", data: { username: loginbox.find('input[name="username"]').val(), password: loginbox.find('input[name="password"]').val() }, success :function(result){ console.log(result); loginbox.find(".warning").html(result.message); setTimeout(function(){ loginbox.find(".warning").html(""); },1500); if(!result.code){ setTimeout(function(){ window.location.reload(); },1500); } } }); }); userinfobox.find(".logout").on("click",function(){ $.ajax({ url:"/api/user/logout", success:function(result){ console.log(result); if(!result.code){ window.location.reload(); } } }) }); //在页面加载时获取评论 $.ajax({ url: '/api/pinglun', type:"get", dataType:"json", data: { contentid: $('#contentid').val() }, success: function(result) { //console.log(111111); render(result.postdata); quanju=result.postdata; } }); //提交评论 $("#addcomment").on("click",function(){ $.ajax({ type:"post", url:"/api/comment", dataType:"json", data:{ comment: $("#comment").find("textarea").val(), contentid: $("#contentid").val() }, success:function(result){ //console.log(result); $("#commentarea").val(""); render(result.postdata); quanju=result.postdata; } }) }); var quanju=null; var page=1; var limit=3; var pagecount=0; $("#prevpage").on("click",function(){ page--; render(quanju); }); $("#nextpage").on("click",function(){ page++; render(quanju); }); function render(data) { var str = ""; var start=(page-1)*limit; var end = start+limit; var comments=data.comment.reverse(); var showcomments=comments.slice(start,end); pagecount = Math.ceil(data.comment.length/limit); page = Math.min(pagecount,page); page = Math.max(1,page); $("#totalpage").html(pagecount); $("#currentpage").html(page); $("#commentCount").html(comments.length); for (var i = 0; i < showcomments.length; i++) { str += `<div> <span class="commenter">${showcomments[i].user}</span> <span class="commenttime">${formatDate(showcomments[i].time)}</span> </div> <p class="contents">${showcomments[i].comment}</p>`; } $("#commentlist").html(str); } function formatDate(d) { var date1 = new Date(d); return date1.getFullYear() + '-' + (date1.getMonth()+1) + '-' + date1.getDate() + '- ' + date1.getHours() + ':' + date1.getMinutes() + ':' + date1.getSeconds(); } }); |
index.css
body{ background:#ebebeb; min-width: 650px; } body h2, body h3 { padding: 0; margin: 0; } a{ text-decoration: none !important; color: #fc6423; } .container .row .container-fluid { padding: 0; } .header1{ height: 200px !important; background:url(/public/img/backimg.jpg); background-size:cover; } .header1 span{ margin-left: 40px; line-height: 200px; font-size: 30px; color: #fc6423; opacity: 0.7; filter: alpha(opacity=70); } div[class*="col"]{ padding: 0; color:#000; } #content .papers{ height: 500px; margin-bottom: 20px; background:#fff; text-align: center; position: relative; } #content .papers h2{ /*height: 80px;*/ line-height: 50px; font-size: 25px; } #content .paperabout{ font-size: 18px; } #content .paperinfo{ color: #fc6423; } #content dfn p{ height: 300px; font-size: 25px; padding: 20px; background: #ddd; } #content .readmore{ width: 150px; height: 40px; line-height: 36px; font-size: 25px; background:#fff; border:2px solid #fc6423; position: absolute; bottom: 30px; left: 50px; } #content .readmore a{ display: inline-block; width: 150px; height: 40px; } #content .readmore:hover{ background:#fc6423; border:2px solid #fff; } #content .pages{ font-size: 18px; height: 40px; line-height: 40px; text-align: center; margin-bottom:20px; } #content .pages a{ color: #fc6423; display: block; height: 40px; line-height: 40px; width: 80px; border:1px solid #fc6423; } #content .pages a:hover{ color: #fff; background-color: #fc6423; } #content .pages>span{ color: #fc6423; } #register{ background:#fff; margin-left: 30px; padding-bottom: 20px; margin-bottom: 20px; transition: 0.5s ease-out; } #register h2{ height: 50px; margin-left: 30px; line-height: 50px; font-size: 24px; color: #fc6423; } #register .line{ height: 50px; text-align: center; line-height: 50px; font-size: 18px; } #register .line input{ height: 60%; outline: none; border-color: #fc6423; } #register .line input[type="submit"]{ width: 200px; border:none; padding: 0; background-color: #fc6423; line-height: 100%; } #register .warning{ height: 50px; text-align: center; line-height: 50px; font-size: 20px; color: #fc6423; } #register>p{ margin-left: 30px; } #login{ background:#fff; padding-bottom: 20px; margin-left: 30px; margin-bottom: 20px; transition: 0.5s ease-out; } #login h2{ height: 50px; margin-left: 30px; line-height: 50px; font-size: 24px; color: #fc6423; } #login .line{ height: 50px; text-align: center; line-height: 50px; font-size: 18px; } #login .line input{ height: 60%; outline: none; border-color: #fc6423; } #login .line input[type="submit"]{ width: 200px; border:none; padding: 0; background-color: #fc6423; line-height: 100%; } #login .warning{ height: 50px; text-align: center; line-height: 50px; font-size: 20px; color: #fc6423; } #login>p{ margin-left: 30px; } #userinfo, #community{ background:#fff; margin-left: 30px; margin-bottom: 20px; padding-bottom: 20px; } #userinfo h2, #community h2{ height: 50px; margin-left: 30px; line-height: 50px; font-size: 24px; color: #fc6423; } #userinfo h3, #community h3{ height: 35px; margin-left: 30px; line-height: 35px; font-size: 16px; } #userinfo>p{ text-align: center; font-size: 15px; } footer{ height: 60px; background:#333; position: relative; } footer p{ text-align: center; line-height: 60px; color: #fff; opacity: 0.7; } footer a{ display: block; width: 60px; height: 60px; position: absolute; top: -70px; right: 10px; background-color: #fc6423; border-radius: 30px; text-align: center; line-height:100%; } footer a span{ font-size: 30px; color: #fff; margin-top: 10px; } #bs-example-navbar-collapse-1 ul{ width: 100%; padding-left: 15%; text-align: center; } #bs-example-navbar-collapse-1 li{ height: 60px; } #bs-example-navbar-collapse-1 ul a{ margin: 0; padding: 0; color: #fc6423; width: 150px; height: 60px; line-height: 60px; font-size: 20px; } #bs-example-navbar-collapse-1 ul a:hover{ background: #fc6423; color:#fff; -webkit-transform: scale(1.1); transform: scale(1.1); transition: 0.5s ease-out; } #comment{ padding: 30px; background: #fff; } #comment h3{ text-align: left; height: 60px; line-height: 100%; color: #fc6423; } #comment h3 .much{ float: right; color: #888; font-size: 18px; } #comment h4{ color: red; text-align: center; } #commentlist{ margin-bottom: 30px; } #commentlist>p{ background: #eee; height: 80px; text-indent: 2em; padding:5px 20px; font-size: 18px; color: #555; margin-bottom: 10px; } #commentlist>div{ line-height: 40px; padding: 5px; height: 40px; } #commentlist>div .commenter{ float: left; margin-left: 20px; color: #fc6423; } #commentlist .commenttime{ float: right; margin-right: 20px; } @keyframes moveup { 0%{ -webkit-transform: translateY(100px); transform: translateY(100px); opacity: 0; filter: alpha(opacity=0); } 50%{ -webkit-transform: translateY(30px); transform: translateY(30px); opacity: 0.25; filter: alpha(opacity=25); } 75%{ -webkit-transform: translateY(13px); transform: translateY(13px); opacity: 0.5; filter: alpha(opacity=50); } 100%{ -webkit-transform: translateY(0); transform: translateY(0); opacity: 1; filter: alpha(opacity=100); } } @keyframes turndown { 0%{ opacity: 0; filter: alpha(opacity=0); } 100%{ opacity: 1; filter: alpha(opacity=100); } } .moveup{ -webkit-animation: moveup 0.7s ease-in; animation: moveup 0.7s ease-in; -webkit-animation-fill-mode: forwards; } .spindown{ -webkit-animation: turndown 0.7s ease-in; animation: turndown 0.7s ease-in; -webkit-animation-fill-mode: forwards; } |
3、后台代码
(1)项目配置
package.json
{ "name": "myblog", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "body-parser": "^1.18.2", "cookies": "^0.7.1", "express": "^4.16.2", "mongoose": "^4.12.4", "promise": "^8.0.1", "swig": "^1.4.2" } } |
app.js
//加载express模块 var express = require("express"); //加载swig模块 var swig = require("swig"); var User = require("./models/user"); //加载mongoose数据库,这个中间件是nodejs与mongoDB数据库的桥梁 var mongoose = require("mongoose"); var Cookies = require('cookies'); //创建一个新的服务器,相当于httpcreateServer var app = express(); //静态文件资源托管的,js css img等 app.use("/public",express.static( __dirname+"/public")); //定义应用使用的模板引擎,第一个参数:所要渲染模板文件的后缀,也是模板引擎的名称,第二个参数:渲染的方法 app.engine("html",swig.renderFile); //定义模板文件存放的路径,第一个参数必须是views,这是模块内指定的解析字段,第二个参数为路径:./表示根目录 app.set("views","./views"); //注册使用模板引擎;第一个参数不能变,第二个参数和上面的html一致 app.set("view engine","html"); //设置完就可以直接在res中渲染html文件了:res.render("index.html",{要渲染的变量})第一个参数是相对于views文件夹 //在开发过程中要取消模板缓存,便于调试 swig.setDefaults({cache : false}); //加载bodyparser模块,用来解析前端提交过来的数据 var bodyparser = require("body-parser"); app.use(bodyparser.urlencoded({extended:true})); app.use( function(req, res, next) { req.cookies = new Cookies(req, res); req.userInfo = {}; if(req.cookies.get('userInfo')){ var str1 = req.cookies.get('userInfo'); req.userInfo=JSON.parse(str1); User.findById(req.userInfo._id).then(function(userInfodata){ req.userInfo.isadmin = Boolean(userInfodata.isadmin); }); } next(); } ); //浏览器地址映射 app.use("/admin" ,require("./routers/admin")); app.use("/" ,require("./routers/main")); app.use("/api" ,require("./routers/api")); // 连接数据库 mongoose.connect("mongodb://localhost:27017/myBlog",{useMongoClient:true},function (err) { if(err){ console.log("数据库连接失败!"); }else{ console.log("数据库连接成功!"); app.listen(3000); } }); |
(2)schemas
users.js
var mongoose = require("mongoose"); module.exports = new mongoose.Schema({ username: String, password: String, isadmin:{ type:Boolean, default:false } }); |
categories.js
var mongoose = require("mongoose"); module.exports = new mongoose.Schema({ name: String }); |
contents.js
var mongoose = require("mongoose"); module.exports = new mongoose.Schema({ title: String, category : { type:mongoose.Schema.Types.ObjectId, ref : "Category" }, composition:{ type: String, default : "" }, description :{ type: String, default : "" }, user:{ type:mongoose.Schema.Types.ObjectId, ref : "User" }, num:{ type:Number, dafault:0 }, addtime:{ type:Date, default: new Date() }, comment:{ type:Array, default:[] } }); |
(3)models
user.js
var mongoose = require("mongoose"); var userschama = require("../schemas/users"); module.exports = mongoose.model("User",userschama); |
category.js
var mongoose = require("mongoose"); var userschama = require("../schemas/users"); module.exports = mongoose.model("User",userschama); |
contents.js
var mongoose = require("mongoose"); var contentschama = require("../schemas/contents"); module.exports = mongoose.model("Content",contentschama); |
(4)路由(routers)
api.js
var express = require("express"); var User = require("../models/user"); var Content = require("../models/content"); var router= express.Router(); //统一返回给前端的数据格式 var resdata; router.use(function(req,res,next){ resdata = { code:0, message:"" }; next(); }); router.post("/user/register",function(req ,res ){ var username = req.body.username; var password = req.body.password; var repassword = req.body.repassword; if(username == ""){ resdata.code=1; resdata.message="用户名不能为空!"; res.json(resdata); return; } if(password == ""){ resdata.code=2; resdata.message="密码不能为空!"; res.json(resdata); return; } if(password != repassword){ resdata.code=3; resdata.message="两次输入的密码不一致!"; res.json(resdata); return; } User.findOne({ username:username },function(err,userinfo){ if(err){ console.log(err); } if(userinfo){ resdata.code = 4; resdata.message = "该用户已被注册!"; res.json(resdata); return false; }else{ var newuser = new User({ username: username, password: password }); newuser.save(); resdata.message = "注册成功!"; res.json(resdata); } }); }); router.post("/user/login",function(req ,res ){ var username = req.body.username; var password = req.body.password; if(username == ""||password==""){ resdata.code=1; resdata.message="用户名和密码不能为空!"; res.json(resdata); return; } User.findOne({ username:username, password:password },function(err,userinfo){ if(err){ console.log(err); } if(!userinfo){ resdata.code = 2; resdata.message = "用户名或密码错误!"; res.json(resdata); return false; } resdata.message = "登录成功!"; resdata.userinfo={ id:userinfo._id , username:userinfo.username, isadmin:userinfo.isadmin }; req.cookies.set('userInfo', JSON.stringify({ "_id": userinfo._id, "username": userinfo.username, "isadmin":userinfo.isadmin })); res.json(resdata); }) }); router.get("/user/logout",function(req ,res ){ req.cookies.set('userInfo', null); res.message="退出成功!"; res.json(resdata); }); router.get('/pinglun', function(req, res) { var contentid = req.query.contentid || ''; Content.findOne({ _id: contentid }).then(function(content) { //content.comment.reverse(); resdata.postdata = content; //resdata.data.comments.reverse(); res.json(resdata); }) }); router.post("/comment",function(req,res){ var id = req.body.contentid; var commentdata={ comment:req.body.comment||"", user:req.userInfo.username, time: new Date() }; Content.findOne({_id:id}).then(function(thiscon){ if(commentdata.comment!=""){ thiscon.comment.push(commentdata); } //thiscon.comment.reverse(); thiscon.save().then(function(newcon){ resdata.postdata = newcon; resdata.message="评论成功!"; res.json(resdata); //console.log(newcon); }); }); }); module.exports=router; |
main.js
var express = require("express"); var router= express.Router(); var Category = require("../models/category"); var Content = require("../models/content"); var data; //处理通用的数据,首页,分类页,每篇文章详情页均需要的变量 router.use(function (req, res, next) { data = { userInfo: req.userInfo, categories: [] }; Category.find().then(function(categories) { data.categories = categories; next(); }); }); //渲染首页 router.get("/", function(req, res) { data.category = req.query.category ||""; data.count = 0; data.page = Number(req.query.page || 1); // 默认两条数据 data.limit = 2; data.pages = 0; var where = {}; if (data.category) { where.category = data.category } Content.where(where).count().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(where).find().limit(data.limit).skip(skip).populate(['category', 'user']).sort({ addtime:-1 }); }).then(function(contents) { data.contents = contents; //console.log(data); res.render('main/index', data); }) }); //进入详细阅读部分 router.get("/article",function(req,res){ var id = req.query.contentid||""; Content.findOne({_id:id}).populate(["category","user"]).then(function(content){ data.contents = content; content.num++; content.save(); //console.log(data); res.render("main/article_detail",data); }); }); module.exports = router; |
(五)运行效果
(1)首页
(2)博文详情页面
下篇博文将说明后台管理的设计与实现。