NodeJS簡易部落格系統(八)功能需求描述及使用者模組實現
阿新 • • 發佈:2018-11-02
一、功能需求描述
用一張導圖來說明:
二、頁面設計
頁面設計如下:
三、梳理下整個系統的業務流程
對這個小專案進行業務流程的梳理,流程圖大致如下:
四、使用者模組實現
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)博文詳情頁面
下篇博文將說明後臺管理的設計與實現。