express+mongodb搭建简易博客系统

准备事项

安装Nodejs,express,git,mongodb以及各种依赖库的过程略去不提。使用eslint规范代码。

npm install -g eslint

也可以 –save-dev 选择本地依赖安装 。懒人必备,直接init一个eslint规则。

eslint --init

这会提供几个可选选项,根据你的选择创建规范规则文件。放张图。

20180616124006

express通过mongoose库配置mongodb连接。app.js文件添加

1
2
var mongoose =require("mongoose");
mongoose.connect("mongodb://127.0.0.1:27017/express_blog");

package.json修改script项实现自动重启服务,此处需要node-dev包。

1
2
3
"scripts": {
"start": "node-dev ./bin/www"
},

新建git仓库。

git init

新建 .gitignore 文件忽略node_modules文件夹。之后add文件,add . 匹配所有文件。

git add .

commit提交初始的环境配置。

git commit -m "init"

github上create a new repository后关联到远程仓库

git remote add origin git remote add origin https://github.com/flycatrix/express-blog.git

将本地仓库的内容推到远程库上

git push -u origin master

本地和远程分支合并

git pull origin master:master

准备OK。

数据库操作

mongoose(猫鼬)中间件

博客的功能很大程度上依赖数据库操作,使用mongoose库(猫鼬)简化mongodb操作。mongoose使用对象模型的方法对mongodb数据库进行操作,使用schema方法规范模型的变量个数,变量类型等。吐槽官方文档,一个技术文档写的那么萌。。

model文件夹存放要使用到的数据模型。简单的user模型

1
2
3
4
5
6
7
8
var mongoose =require("mongoose");
var Schema= mongoose.Schema;
var obj = {
username:String,
password:String
}
var model = mongoose.model("user" ,new Schema(obj));
module.exports = model;

建立模型需要引用mongoose模块,Schema构造函数将写好的数据模型obj包装成Schema对象,最终导出的model对象将继承Schema构造函数的原型中所定义的 访问和操作mongodb 的方法。

schema有十种数据类型,string是其中之一,MongoDB期望用户能按固定的规律存放数据,但实际上用户可以在MongoDB中的任一集合内存放任一类型的数据。mongoose采取事先说明好数据结构和类型的方式,规范用户写入MongoDB中的数据。同时,Schema也能在定义模型时定义MongoDB的索引,支持指定索引(index)或者唯一索引(unique)。对mongoose模型的使用将分散在各路由页面说明。

关于Schema的更多用法和解释可移步mongoose的文档,写文档的一定是个爱猫的程序员。

session - cookie用户验证

关于session

用户的的登录状态和权限等级的确认均需要用到cookie机制,然而用户在浏览器端可以轻易更改、伪造cookie。就如xss攻击可以在用户不知情的情况下获取记录在用户浏览器中的无httponly属性的cookie信息,攻击者一旦成功伪造cookie登入,便可操作受害人的资料数据等。然而xss有绕过httponly的方法,且httponly对中间人攻击无效。一年以前曾经测试过中间人攻击,直接登入受害人QQ邮箱。。不过cookie的secure属性可以只在https通信中传递cookie,可以有效防范中间人,嗅探什么的。。emm废话不说了。

此外,cookie在每次请求的时候都会被附加在请求头中,大量的cookie必然会影响传输效率。那么,session应运而生。

session是存储在服务器中的数据,通过session_id运作。当收到请求时,取出保存在cookie中的session,在服务端查看其是否存在登录属性,借此判断用户的登录状态。

express-session中间件

express-session中间件件实现会话。先npm install express-session –save-dev,然后app.js中添加

1
2
3
4
5
6
7
8
var session = require("express-session");
app.use(session({
name: "sessionid", // 在响应中cookie的name字段,默认是connect.sid
secret:"xcola", //用secret签名保证是本服务器生成的session
cookie: {maxAge: 1000*3600 }, //1小时
resave: true,// 将session强制保存在store中
saveUninitialized: true // 第一次访问即设置cookie
}));

当用户首次访问页面时,就为用户分配一个cookie,其中保存对应的sessionid。

登录逻辑:若用户登录成功,那么为这个session新添一个属性,当下次请求来临时,取得用户session中新增的属性字段,若为空则是未登录用户,若不为空则是已登录用户。

注销逻辑:使用destroy方法摧毁当前会话,在摧毁后会重新生成一个新的没有登录状态的session会话。

登录与注册功能

在express添加新路由需要三步。

  • app.js 中require 路由模块(存放在routes文件夹下)
  • app.js 中use注册路由
  • routes文件夹中添加新路由模块文件并设置路由规则

register - 注册页

register.ejs模版部分

views文件夹新建register.ejs模版。页面部件偷懒扒取自bootstrap。

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
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
<link rel="stylesheet" href="/bootstrap/css/bootstrap.css">
<script type="text/javascript" src="/bootstrap/js/jquery-3.3.1.min.js"></script>
<script src="/bootstrap/js/bootstrap.js"></script>
</head>
<body>
<%- include('./header.ejs',{title: "register",elsetitle:"login"})%>
<div class="container">
<form method="post" action="/register/validate">
<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input type="text" class="form-control" placeholder="Username" name="username">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password" name="password">
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Check me out
</label>
</div>
<button type="submit" class="btn btn-default">Submit</button>
<%if(show){%>
<div class="alert alert-danger" role="alert">用户名已被注册</div>
<%}%>
</form>
</div>
</body>
</html>

页面头部的导航栏写成独立的header.ejs文件,在使用到header的ejs中include一下,还可以在include的同时向header.ejs传入变量。想起来之前用php写公共部分再include,通过js改变页面内容的做法,简直不要太傻。

register.js路由模块部分

路由模块中设置路由规则,也承担根据用户输入连接和操作数据库的任务。

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
var express = require('express');
var router = express.Router();
var userModel = require("../model/userModel");

router.get("/",function(req,res){
res.render("register",{title:"Register",elsetitle:"Login",show:0});
})

router.post("/validate",function(req,res){

userModel.find({
username:req.body.username
},function(error,docs){

if(docs.length==0){
userModel.create({
username:req.body.username,
password:req.body.password
},function(error,info){

if(!error){
res.redirect("/");
}
})
}else{
res.render("register",{title:"Register",elsetitle:"Login",show:1});
}
})
})

module.exports = router;

路由文件中引入我们事先写好的userModel(mongoose模块),以便按userModel中规定的模型检索数据库。在register.js路由模块中,get方法负责渲染页面,post方法在二级路由中实现数据库查询以及写入的操作。通过find方法检索用户提交的用户名,在回调函数中处理查询结果,error用于抛出一个错误信息,docs为查询后得出的文档数组,若docs长度为0,表示这是一个未被注册的用户名,因此调用create方法写入一个新用户信息。若docs的长度不为0,则显示 “用户名已被注册” 的消息。

login - 登录页

login的ejs模版和register.ejs大同小异,不再贴出赘述。

login.js路由模块部分

login部分的路由,需要判断用户名和密码是否能在数据库中成功匹配。判断成功后,还应该为用户设置一条新的session属性,以便根据新添加的属性判断用户的登录状态,在数据库中取得用户信息。

login.js路由模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var express = require('express');
var router = express.Router();
var userModel = require("../model/userModel");
router.get("/",function(req,res){
res.render("login",{title:"Login",elsetitle:"Register",show:0});
})
router.post("/validate",function(req,res){
userModel.find({
email:req.body.email,
password:req.body.password
},function(error, docs){
if(docs.length==0){
res.render("login",{title:"Login",elsetitle:"Register",show:1});
}else{
req.session.userInfo = docs[0];
res.redirect("/")
}
})
})
module.exports = router;

get方法渲染登录页面,post请求中接收用户输入,若用户登录成功,那么为这个session新添一个属性userInfo并跳转至首页,若登录失败则在页面上显示错误消息。

博客的增删改查

准备博客模型

在用户登录之后,可以撰写、发表博客。博客内容和信息存储在MongoDB中,在model文件夹中建立mongoose模型

1
2
3
4
5
6
7
8
9
10
11
12
// article model
var mongoose =require("mongoose");
var Schema= mongoose.Schema;
var obj = {
author:String,
title:String,
content:String,
createTime:Date,
category: String
}
var model = mongoose.model("article" ,new Schema(obj));
module.exports = model;

author,title,content,createTime为博客的基本信息,category为博客添加分类信息。

首页显示博客列表

首页提供对未登录或非博主用户展示博客列表功能,对博主提供删改接口。此外,添加销毁路由的操作。

index.js路由模块部分

index路由判断用户登录状态及用户权限,读取保存在数据库中的博客列表传输给index.ejs模版,最终显示在用户界面上。

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
var express = require('express');
var router = express.Router();
var articleModel =require("../model/articlemodel");

/* GET home page. */
router.get('/', function(req, res, next) {
if(req.session.userInfo){
articleModel.find({},function(error,docs){
res.render('index', { title: 'Express' ,welcome:req.session.userInfo.username,
list:docs,show:1
});
})
}else{
articleModel.find({},function(error,docs){
res.render('index', { title: 'Express' ,welcome:null,
list:docs,show:0
});
})
}
});
//destroy session
router.get("/logout",function(req,res){
req.session.destroy(function(error){
if(!error){
res.redirect('/login');
}
})
})
module.exports = router;

get方法中首先读取用户登录状态,若存在则显示欢迎标签,同时在页面上增加对当前用户博客内容的删改功能,若是未登录用户,则仅能查看当前博客列表。

index.ejs模版部分

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
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/bootstrap/css/bootstrap.css">
<script type="text/javascript" src="/bootstrap/js/jquery-3.3.1.min.js"></script>
<script src="/bootstrap/js/bootstrap.js"></script>
</head>
<body>
<%- include('./header.ejs',{title: "index",elsetitle:"article"})%>

<div class="container">
<%if(show){%>
<p>Welcome back! <%= welcome %></p>
<%}%>
<table class="table table-striped table-bordered ">
<caption>Optional table caption.</caption>
<thead>
<tr>
<th>作者</th>
<th>标题</th>
<th>创建时间</th>
<th>分类</th>
<%if(show){%>
<th>操作</th>
<%}%>
</tr>
</thead>
<tbody>
<%for(var i=0;i<list.length;i++) { %>
<tr id="<%= list[i]._id %>">
<th scope="row"><%= list[i].author%></th>
<td><%= list[i].title%></td>
<td><%= list[i].createTime%></td>
<td><%= list[i].category%></td>
<%if(show){%>
<td>
<a class="btn btn-default" role="button" href="/update/<%= list[i]._id %>" id="update">更新</a>
<a class="btn btn-danger" role="button" href="/remove/<%= list[i]._id %>" id="delete">删除</a>
</td>
<%}%>
</tr>
<%}%>
</tbody>
</table>

</div>
</body>
</html>

在此指出,index.ejs模版中使用ejs语法中的for循环创建表格,同时将当次循环中的博客的id写入对应的tr的id属性以及删除按钮的href属性中去,这样当用户点击删除时才能告知服务器具体需要删除哪篇博客。后面查寻博客时则通过事件代理取tr的id值确定路由。

article模版展示编写博客的页面,新建博客和更新博客共用此模版。

article.ejs模版部分

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
<!DOCTYPE html>
<html>
<head>
<title></title>
<link rel="stylesheet" href="/bootstrap/css/bootstrap.css">
<script type="text/javascript" src="/bootstrap/js/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/bootstrap/js/bootstrap.js"></script>

</head>
<body>
<%- include("header.ejs",{title:"write",elsetitle:"logout",show:0})%>
<div class="container">
<form action="/article" method="post">

<div class="form-group">
<label for="exampleInputEmail1">标题</label>
<input type="text" class="form-control" id="exampleInputEmail1" name="title" placeholder="title" value="<%= art_title%>">
</div>
<div class="form-group">
<label for="exampleInputEmail1">分类</label>
<input type="text" class="form-control" id="exampleInputEmail1" name="category" placeholder="category" value="<%= art_category%>">
</div>
<div class="form-group">
<label for="exampleInputPassword1">内容 </label>
<textarea rows="4" class="form-control" name="content"><%= art_content%></textarea>
</div>
<button type="submit" class="btn btn-default">Submit</button>


</form>
</div>
</body>
</html>

增 - 编写博客

在routes文件夹中新建article.js路由模块,在views文件夹下新建article.ejs模版文件,生成编写博客页面。

article.js路由模块部分

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
var express = require('express');
var router = express.Router();
var articleModel =require("../model/articlemodel");
/* GET home page. */
router.get('/', function(req, res, next) {
if(req.session.userInfo){
res.render('article');
}else{
res.redirect("/login");
}

});

router.post("/",function(req,res){
articleModel.create({
author: req.session.userInfo.username,
title: req.body.title,
content: req.body.content,
createTime: new Date(),
category: req.body.category
},function(error,info){
if(!error){
res.redirect("/");
}
})
})
module.exports = router;

get方法中,先通过session的userInfo属性判断用户是否登入,若为已登录用户则加载article页面,若为未登录用户则跳转登录页。

post方法中,接收用户输入,将用户编写的博客内容保存到MongoDB中。

删 - 删除博客

使用动态路由的方式完成删除博客的操作。首页对已登录博主用户提供删除博客接口,当点击删除时,将被点击的博客的id当作路由地址通过get方式传给remove.js路由模块,在remove.js中通过deleteOne()方法安全删除博客。

remove.js路由模块部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var express = require('express');
var router = express.Router();
var articleModel =require("../model/articlemodel");
var userModel = require("../model/userModel");
/* GET home page. */
router.get("/:id",function(req,res){
// console.log(req.session.userInfo);
articleModel.deleteOne({
_id: req.params.id
},function(error,docs){
if(!error){
articleModel.find({},function(error,docs){
res.render('index', { title: 'Express' ,welcome:req.session.userInfo.username,
list:docs,show:1
});
});
}
})
})
module.exports = router;

在express中,获取不同方式提交的数据的方法分以下几种。

  • req.params获取动态路由字段。
  • req.query获取get查询字段。
  • req.body获取post字段。

在删除路由模块后,需要再执行一次find()操作,否则首页的博客列表将为空。

改 - 更新博客

更新博客也使用动态路由的方法。和删除操作很类似。需要注意的是更新博客需要获取原有的博客填充进article.ejs模版中。

update.js路由模块部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var express = require('express');
var router = express.Router();
var articleModel =require("../model/articlemodel");
var userModel = require("../model/userModel");
/* GET home page. */

router.get("/:id",function(req,res){
articleModel.find({
_id: req.params.id
},function(error,docs){

if(!error){
res.render('article',{art_title:docs[0].title,art_content:docs[0].content,art_category:docs[0].category});
}
})
})
module.exports = router;

更改之后仍使用编写博客时的方式提交博客,博客的createtime属性也将更新为最后一次提交更新的时间。

查 - 查看博客

在index.ejs中对展示博客列表的table标签添加事件代理。监听每个tr的点击事件跳转到check路由上。

index.ejs添加

1
2
3
4
5
<script type="text/javascript">
$('tbody').on('click',"tr",function(){
location.href = '/check/'+$(this).attr('id');
})
</script>

在for循环输出博客列表时,事先给每个tr的id属性都设置成了博客的id。

check.js路由模块部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var express = require('express');
var router = express.Router();
var articleModel =require("../model/articlemodel");
var userModel = require("../model/userModel");
/* GET home page. */
router.get("/:id",function(req,res){
articleModel.find({
_id: req.params.id
},function(error,docs){
if(!error){
res.render('check',{title:"blog",show:0,art_title:docs[0].title,art_content:docs[0].content,art_category:docs[0].category});
}
})
})
module.exports = router;

这应该是最简单的一个模块吧。find一下然后传给check.ejs模版就好了。

总结

至此,已经实现了一个简单的可以登录注册增删改查博文的小博客,但还缺少分类、评论等功能。第一次记叙小项目的实现过程,行文不免凌乱缺乏条理,章节安排参考了@liuxing的github项目node-blog,项目代码已上传我的github express-blog。此外,由于对代码的理解较浅,表述不当之处,还望不吝赐教。

0%