前言:

这是算是一个前端萌新第一次涉及到前后端的全栈开发项目,可能涉及到的技术栈用的并不是深入,对许多中间件的总结所述必然会有所欠缺,但本文旨在浅析整个项目过程,同时去还原深化一些相关的概念。而且第一次写文章思路排版等不是很明确,有不对的地方欢迎各位大佬们指正!


  • 项目实质: 一个基于nodejs、express框架及mongoDB数据库搭建的简易博客系统

  • 效果实现 :主体分为前后页面,前台包括用户注册登录面板,文章内容的分页、分类展示;内容详情页有文章内容展示,底部有评论信息展示。后台管理页面包括管理首页、注册用户详细信息、文章分类管理页、文章分类添加页、所有文章信息页、添加文章页。实现对整站整站所有内容的增删改查。整站部分页面用bootstrap组件搭建,天然响应式,但是样式很一般。

  • 页面预览

前台

前台

评论区

评论区

后台

后台

数据库

数据库

  • 技术栈

    • nodeJs 搭建基本的后端环境

    • express 实现页面路由设计、页面渲染、后端数据处理、静态资源的托管等

    • mongoose nodejs后端与MongoDB数据库连接的桥梁,定义数据库表结构、构建表模型、通过操作表模型实现对数据库的增删改查。

    • ajax 实现用户注册、登录相关逻辑判断与验证、无刷新提交平论、获取评论

    • body-parser 用于处理前端post请求提交过来的数据

    • cookies 保持用户登录状态,作为中间变量传递给模板实现逻辑上的渲染

    • es6 模板字符串渲染评论,后端数据回馈的大面积promise操作

    • swig 模板渲染引擎,实现页面的引用、继承、代码的复用从而提高页面性能

  • 开发环境

    • webstorm 同时在这里推荐一下这个强大的IED集成开发环境,比如版本控制、依赖安装、初始化构建、代码提示等等,特别适合初学者用来开发前后端项目

    • mongoDB 这是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的,所以特别适合用来实践前后端项目

  • 源码地址https://github.com/formattedzzz/blogsystem

  • 获取方式: 初始化一个本地仓库,fork上面地址的库然后git下来。安装MongoDB数据库(最好也装一个可视化工具比较直观查看到数据源),在项目根目录下新建一个db文件夹作为本站数据库然后连接起来(具体做法可以参考mongoDB官方文档),最后运行入口文件app.js在浏览器输入localhost:8888就ok了。如果大家有问题欢迎到我个人博客联系我一起探讨。


项目解剖

文件结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-project    
db //数据库文件,存取整站页面上所有数据
-models //数据库模型,和schemas下的表结构一一对应
user.js
category.js
content.js
-schemas //表结构,一个js文件对应一张表,定义每张表的数据结构
users.js
categories.js
contents.js
node_modules
-public //静态资源存放区
css
img
font
-js
jquery.js
bootstrap.js
index.js
-routers //三个路由模块,分别处理不同的业务逻辑
api.js //api模块;负责处理前台页面登录注册及提交评论等
main.js //负责接收前台操作请求、渲染前台页面
admin.js //负责接收后台管理操作请求、渲染后台页面
-views //所有浏览请求后端返回的页面都从这里取
mian
-admin
index.html
layout.html
view.html
app.js //入口文件,运行它就等于开启了我们的服务器
package.json //在这里可以查询你安装的中间件及其版本号

入口文件解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* Created by Administrator on 2017/10/24.
*/
//加载express模块
var express = require("express");

//加载swig模块
var swig = require("swig");

//加载mongoose模块,这个中间件是nodejs与mongoDB数据库的桥梁
var mongoose = require("mongoose");

//加载用户表模型,模型从表结构构建出来,然后我们操作模型操作数据
var User = require("./models/user");

//加载kooies模块,用于在登录成功后再req中写入cookie,然后就可以再刷新或请求页面时
//将cookie变量传递给模板用于渲染验证
var Cookies = require('cookies');

//创建一个新的服务器,相当于httpcreateServer
var app = express();

//静态文件资源托管,js css img等,浏览器在解析页面是遇到的所有url都会发送请求给后端,
//我们不可能在后端给每个js、css或img的url都设置路由监听,这样以/public开头的请求都会被
//指引到public目录下去调取资源并返回
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});

//var User = require("./models/user");

//加载bodyparser模块,用来解析前端post方式提交过来的数据
//详细文档:https://github.com/expressjs/body-parser
var bodyparser = require("body-parser");
app.use(bodyparser.urlencoded({extended:true}));


//app.use里的函数是一个通用接口,所有的页面的刷新及请求都会执行这个函数
app.use( function(req, res, next) {
req.cookies = new Cookies(req, res);
//在req对象下建立一个cookie属性,在登录成功后就会被附上用户的信息,之后页面的刷新和
//请求的请求头里都会附带这个cookie发送给后端,且其会一直存在直到退出登录或关闭浏览器,
//当然也可以设置它的有效时间

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");
var db = mongoose.connection;
db.once("open", function () {
console.log("Mongo Connected");
app.listen(8888);
});
db.on("error", console.error.bind(console, "Mongoose Connection Error"));

在schemas/users.js中定义用户信息的数据结构:

1
2
3
4
5
6
7
8
9
var mongoose = require("mongoose");
module.exports = new mongoose.Schema({
username: String,
password: String,
isadmin:{
type:Boolean,
default:false
}
});

在models/user.js中构建用户表模型

1
2
3
4
5
var mongoose = require("mongoose");

var userschama = require("../schemas/users");

module.exports = mongoose.model("User",userschama);

这样我们在路由js文件中根据相对路径引入user.js用户表模型就能用mongoose的语法来对这张表的数据进行增删改查了,以后端登录验证代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
router.post("/user/login",function(req ,res ){
var username = req.body.username;
//通过body-parser中间件post提交的数据都在req对象下的bodys属性中
var password = req.body.password;

if(username == ""||password==""){
resdata.code=1;
resdata.message="用户名和密码不能为空!";
res.json(resdata);
return;
}
//User为引入的用户信息表模型,根据前端提交的数据作为第一个参数进行查询
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
};
//登录成功后给cookie设置对象字符串
req.cookies.set('userInfo', JSON.stringify({
"_id": userinfo._id,
"username": userinfo.username
}));
res.json(resdata);
})

});

给渲染文件传递变量,模板就会根据变量来决定渲染那些部分

1
2
3
4
5
6
7
8
9
//渲染首页
router.get("/", function(req, res) {

res.render('main/index', {
userInfo:req.userInfo
//req.userInfo在入口文件中根据cookie做过统一配置,里面包含当前用户是否为管理员的属性
});

});

在页面模板中

1
2
3
4
5
{% if userInfo.isadmin %}
<p>尊敬的管理员! <a href="/admin/"> 点击这里</a>进入管理页面</p>
{% else %}
<p>你好,欢迎光临我的博客!</p>
{% endif %}

在文件目录中我们看到,前台页面写了三个,layout,index,view,分别是头部和侧边栏共用的布局模板,主页面及内容详情页。这里用到了模板的继承,后面会用到分页的引用,两者差不多,都是将共用的部分写到layout.html里面,然后在主页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
layout.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!--共用部分-->

{% block content %}
{% endblock %}

<!--共用部分-->
</head>
<body>

</body>
</html>

index.html:

{% extends "layout.html" %}

{% block content %}

<!--不同的部分-->

{% endblock %}

用户信息的分页处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
router.get("/user",function(req,res){
<!--page的值在分页按钮url的hash值中设定-->
var page = Number(req.query.page||1);
var limit = 8;
var skip = (page-1)*limit;
var total;
var counts;
User.count().then(function(count){
total = Math.ceil(count/limit);
page = Math.max(1,page);
page = Math.min(page,total);
counts = count;
});
<!--limit为限制获取的条数,skip为跳过多少条选取-->
User.find().limit(limit).skip(skip).then(function(users){

res.render("admin/userindex",{
userInfo:req.userInfo,
users:users,
page:page,
total:total,
counts:counts
})
});

});

<!--之后在模板中根据接收到的数据进行渲染:-->
<tr >
<th>用户ID</th>
<th>用户名</th>
<th>密码</th>
<th>是否为管理员</th>
</tr>
{% for user in users %}
<tr>
<td>{{user._id.toString()}}</td>
<td>{{user.username}}</td>
<td>{{user.password}}</td>
<td>{{user.isadmin}}</td>
</tr>
{% endfor %}

文章或分类的添加、修改则在每篇文章或分类的链接hash值键入响应的ID,这样在get页面的时候,数据才能根据ID来提取响应的信息赋给新页面渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
router.get("/category/edit",function(req,res){
var cateid = req.query.id||"";
//获取ID,然后根据ID查询
Category.find({id:cateid}).then(function(cateinfo){
res.render("admin/categoryedit",{
userInfo:req.userInfo ,
name:cateinfo.name
});
});

});

router.post("/category/edit",function(req,res){
var name =req.body.name||"";
var id = req.query.id||"";

if(name==""){
res.render("admin/error",{userInfo:req.userInfo});
return false;
}else{
Category.findOne({_id:id},function(err,info){
if(err){
console.log(err);
}
if(info){
console.log(info);
info.name = name;
info.save();
res.render("admin/success",{userInfo:req.userInfo});
}

});
}
});

表字段的关联与引入,在文章表结构中,其作者跟分类都是与用户表和分类表关联的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var mongoose = require("mongoose");

module.exports = new mongoose.Schema({
title: String,
category : {
type:mongoose.Schema.Types.ObjectId,
ref : "Category"
},
// 分类数据类型为对象id,关联了Category,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:[]
}
});
//get所有文章主页面中,用.populate方法就能相对应的值了
router.get("/content",function(req,res){
Content.find().populate(["category","user"]).sort({_id:-1}).then(function(contents){
//console.log(contents);
res.render("admin/content",{
userInfo:req.userInfo,
contents:contents
});
});

});

踩坑指南

1.三个分模块处理的app.use一定要放在设置cookie的后面,如上,否则会导致cookie加载不上。

2.要深刻理解get和post的区别,get方式不能改变后端数据的任何数据,只能获取,在用ajax获取评论的时候,为了能在最上面显示最新的评论,在赋给resdata数据的时候变直接做了反转,此时两者存在引用关系,也就改变了原有的顺序,导致浏览器端报500错误,也是郁闷了好久

1
2
3
4
5
6
7
8
9
10
router.get('/pinglun', function(req, res) {
var contentid = req.query.contentid || '';
Content.findOne({
_id: contentid
}).then(function(content) {
resdata.postdata = content;
//resdata.data.comments.reverse();
res.json(resdata);
})
});

3.通用模块处理的时候不要忘了next()函数的执行,如:

1
2
3
4
5
6
7
8
9
//统一返回给前端的数据格式
var resdata;
router.use(function(req,res,next){
resdata = {
code:0,
message:""
};
next();
});

4.在评论分页部分,是直接用ajax请求过来的数据在前端js中完成的,比如每页显示n条,当评论数小于n的时候,需要再对数组做进一步的处理

5.index.html中的js应先引入jq然后bootstrap,这里js不多,所以当项目很大的时候我们就能体会到webpack等前端自动化构建工具的强大了

  • 项目收获 :初步熟悉了全栈项目的开发流程,加深前后端数据交互方面的概念,了解了一些中间件的特性,体会了es6语法特性的强大及严谨性。