Node.js + Express + mongodb 的博客项目之前端ejs模块化及博客分类管理、分页功能实现(六)

前言

模块化开发,主要是以提高代码的复用性,可维护性等。再这个博客项目中,到处体现有模块化开发的思想,如使用 MVC 的设计模式,路由的分文件处理等。在 ejs 模板引擎中,类似 jsp 一般可以将里面的各个内容分成不同的模块,然后再分别引入相关模块组成页面,这样一来就可以做到了代码的复用性,同时维护起来的成本也降低。


ejs 模板引擎的模块化

在 /views/admin 中新建一个名为 header.ejs 的模板,当做后台管理的页头,将 /views/admin/index.ejs 的导航部分剪切出来放到 header.ejs 中,header.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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!--引用bootstrap样式-->
<link rel="stylesheet" href="/public/css/bootstrap.min.css">
<title>控制台</title>
</head>
<body>
<header>
<nav class="navbar navbar-default navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/admin">后台管理</a>
</div>
<div>
<ul class="nav navbar-nav">
<li><a href="/admin/user">用户管理</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
分类管理
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="/admin/category">分类首页</a></li>
<li><a href="/admin/category/add">添加分类</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
内容管理
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="/admin/content">内容首页</a></li>
<li><a href="/admin/content/add">添加内容</a></li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav pull-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<%=userInfo.username%>
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="/">返回主页</a></li>
<li class="divider"></li>
<li><a href="javascript:;" id="logout">退出</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
</header>
<script src="/public/js/3.3.1-jquery-min.js"></script>
<script src="/public/js/bootstrap.min.js"></script>
<script>
// 管理员登出
$("#logout").on("click", function() {
$.ajax({
url: "/api/user/logout",
success: function(result) {
if (result) {
window.location = "http://localhost:8080/";
}
}
});
});
</script>

然后在后台首页界面中引用 header.ejs 模板:

1
2
3
<!-- 引用页头文件 -->
<%- include("header") %>
<!-- 以前的内容在下面 -->

同时用户管理页面将 header 标签的内容删掉后引入 header.ejs:

1
2
3
<!-- 引用页头文件 -->
<%- include("../header") %>
<!-- 以前的内容在下面 -->


内容显示分页的实现

分页功能是数据展示必备的功能,如果需要查询 1000 条数据,一次性的进行展示不仅浪费用户等待的时间,宽带,而且还占用服务器的 I/O 时间从而影响性能,用户一次性看到这么多的数据也很头疼,用户体验不好。所以就需要分页功能来展示数据,就像看书一样,一页展示一点数据,分页展示。


前端组件

分页功能估计后面需要经常用到,所以我们就将它抽离出来分成一个组件,一个模块。在 /views/ 目录下新建一个模板文件 pagination.ejs 当做分页组件,代码如下:

1
2
3
4
5
6
7
8
9
<nav aria-label="...">
<ul class="pager">
<li><a href="/admin/user?page=<%=page-1%>">上一页</a></li>
<li>
一共有 <%=count%> 条数据,每页显示 <%=limit%> 条数据,当前第 <%=page%>/<%=pages%>
</li>
<li><a href="<%=url%>?page=<%=page+1%>">下一页</a></li>
</ul>
</nav>


后端模块

前端通过 GET 的方式向后端提交当前所在的页数,后端进行相应的相应。在 mongoose 中,可以通过 skip() 跳过需要查询的数据条数,limit() 每次查询几条数据,sort() 排序,等方法进行相应的组合达到分页的效果。
同样,分页功能以后会在很多地方使用,所以将其写成一个独立的模块,以便以后重复的使用。在项目的根目录下新建一个目录 my_modules 用来存放我们自己编写的相应模块,在该目录下新建一个 pagination.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
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
/*
自定义分页渲染模块
需要传递一个对象进来,该对象的属性包括:
pagination = {
// 每页显示的条数
limit: 10,
// 需要操作的数据库模型
model: userModel,
// 需要控制分页的url
url: "/admin/user",
// 渲染的模板页面
ejs: "admin/user",
res: res,
req: req,
// 查询的条件
where: {},
// 联合查询的条件
populate: []
// 名称
docs
}
*/

const pagination = (object) => {
/*
实现分页
limit(Number): 限制获取的数据条数
skip(Numer): 跳过数据的条数
每页显示2条
*/
// 当前页数,使用get获取前端传递的当前页数
let page = object.req.query.page || 1;
// 每页显示数据条数默认为10
let limit = object.limit || 10;
// 总页数
let pages = 0;
// 跨集合查询的条件
let populate = object.populate || [];
// 查询该文档的数据条数
object.model.countDocuments(object.where).then((count) => {
// 根据总条数计算总页数
pages = Math.ceil(count / limit);
// 限制当前页数,避免溢出
// page不能超过pages
page = Math.min(page, pages);
// page不能小于1
page = Math.max(page, 1);
// 跳过数据的条数
let skip = (page - 1) * limit;
// 分页查询出数据
object.model.find(object.where).populate(populate).skip(skip).limit(limit).then((docs) => {
object.res.render(object.ejs, {
userInfo: object.req.userInfo,
docs: docs,
page: page,
pages: pages,
limit: limit,
url: object.url,
count: count
});
});
});
};

// 暴露给外部使用
module.exports = pagination;

自定义的分页模块完成后,在需要调用的地方引入它。在后台管理的路由文件 admin.js 中的用户管理首页中,将其代码修改为如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 用户管理首页
router.get("/user", (req, res, next) => {
// 调用自定义的分页渲染方法
pagination({
// 每页显示的条数
limit: 10,
// 需要操作的数据库模型
model: userModel,
// 需要控制分页的url
url: "/admin/user",
// 渲染的模板页面
ejs: "admin/user",
// 查询的条件
where: {},
res: res,
req: req
});
});

然后给数据库添加多添加几条用户数据,重启服务器进行进入客户端刷新进行测试:
用户分页管理
到此用户分页管理,暂时完成。


博客的分类管理

博客的分类管理主要是对博客的分类进行添加,修改和删除等。管理员用户在控制台进行相应的操作,服务器端则将对数据库进行相应的操作。


分类的数据结构

先创建博客文章分类在 mongodb 中的存储结构,在 schemas 中新建一个文件 categories.js 用来限制文章分类的数据结构,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 引入相关模块
const mongoose = require("mongoose");

/*
文章分类的数据结构
{
文章标题,字符串类型
}
*/
module.exports = new mongoose.Schema({
name: String
});

创建了分类的Schema对象后,我们通过Schema对象来创建文章分类的数据模型,通过数据模型来对数据库进行相应的操作。在 models 文件夹中新建一个 category.js 来当做文章分类的模型,在 category.js 中添加如下代码:

1
2
3
4
5
6
7
// 引入mongoose模块
const mongoose = require("mongoose");
// 引入文章分类的schema
const categoriesSchema = require("../schemas/categories");

// 创建文章分类模型
module.exports = mongoose.model("category", categoriesSchema);


分类管理首页

在文章分类的模型创建完毕后,就可以创建分类管理的首页了,分类管理首页的路由设计为 GET /admin/crtegory 。我们新建一个分类管理首页的视图文件,在 /views/admin 目录下,新建一个 /categroy/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
<!-- 引入页头模板 -->
<%- include("../header") %>
<!-- 内容 -->
<div class="container-fluid">
<ol class="breadcrumb">
<li><a href="/admin">管理首页</a></li>
<li class="active">分类首页</li>
</ol>
<table class="table table-hover table-striped table-bordered">
<thead>
<tr>
<th>ID</th>
<th>分类名称</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<%for(let i = 0, len = docs.length; i < len; i++){%>
<tr>
<td><%=docs[i]._id%></td>
<td><%=docs[i].name%></td>
<td>
<a href="/admin/category/edit?id=<%=docs[i]._id%>" class="btn btn-default">修改</a>
<a href="/admin/category/delete?id=<%=docs[i]._id%>" class="btn btn-danger">删除</a>
</td>
</tr>
<%}%>
</tbody>
</table>
</div>
<!-- 引入分页模板 -->
<%- include("../../pagination") %>
</body>
</html>

重启服务器刷新浏览器客户端,进入后台管理首页,点击分类管理:
分类管理首页


分类管理的添加

新建一个文章分类添加的页面 /views/admin/category/add.ejs 并添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 引入页头模板 -->
<%- include("../header") %>
<!-- 内容 -->
<div class="container-fluid">
<ol class="breadcrumb">
<li><a href="/admin">管理首页</a></li>
<li><a href="/admin/category">分类首页</a></li>
<li class="active">分类添加</li>
</ol>
<form method="post">
<div class="form-group">
<label for="name">分类名称</label>
<input type="text" class="form-control" id="name" name="name" placeholder="请输入分类名称">
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
</div>
</body>
</html>

同时在后台路由文件 routes/admin.js 中对其进行路由配置,分类添加的界面路由为 GET /admin/categorry/add 添加如下代码:

1
2
3
4
5
6
7
// 分类添加的首页
router.get("/category/add", (req, res, next) => {
// 渲染分类添加模板
res.render("admin/category/add", {
userInfo: req.userInfo
});
});

此时,添加文章的页面已经被渲染出来了,但是我们并没有编写保存的处理。前端使用 POST 方式向服务器提交数据,在后台进行相应的验证后,若不出错则保存进入数据库中。但在此之前,我们需要一个提示的界面,以此来提示用户相应的信息。
在 /views/admin 中新建一个 error.ejs 和 success.ejs 分别用来提示用户操作的成功与失败。
error.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
<%- include('header') %>

<div class="container-fluid">
<ol class="breadcrumb">
<li><a href="/admin">管理首页</a></li>
<li class="active">错误提示</li>
</ol>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">错误提示</h3>
</div>
<div class="panel-body">
<%=message%>
</div>
<div class="panel-footer">
<%if(url){%>
<a href="<%=url%>">跳转</a>
<%}else{%>
<a href="javascript:window.history.back();">返回上一步</a>
<%}%>
</div>
</div>
</div>
</body>
</html>

success.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
<%- include('header') %>

<div class="container-fluid">
<ol class="breadcrumb">
<li><a href="/admin">管理首页</a></li>
<li class="active">成功提示</li>
</ol>
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">成功提示</h3>
</div>
<div class="panel-body">
<%=message%>
</div>
<div class="panel-footer">
<%if(url){%>
<a href="<%=url%>">跳转</a>
<%}else{%>
<a href="javascript:window.history.back();">返回上一步</a>
<%}%>
</div>
</div>
</div>
</body>
</html>

最后我们再 /routes/admin.js 中添加如下代码,对前面添加的分类数据进行保存,保存的路由为 POST /category/add :

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
// 分类的保存
router.post("/category/add", (req, res, next) => {
// 获取分类名称,默认为""
let name = req.body.name || "";

// 如果名称为空
if (name === "") {
// 渲染一个错误提示
res.render("admin/error", {
userInfo: req.userInfo,
url: null,
message: "分类名称不能为空!"
});
return;
}
// 从数据库中查询该名称是否已存在
categoryModel.findOne({name: name}, (err, docs) => {
// 如果数库库中已存在该名称
if (docs) {
// 渲染一个错误提示
res.render("admin/error", {
userInfo: req.userInfo,
url: null,
message: "该分类名称已存在!"
});
return;
} else {
// 不存在则新建一个数据
categoryModel.create({
name: name
}, (err) => {
if (!err) {
// 渲染一个错误提示
res.render("admin/success", {
userInfo: req.userInfo,
message: "添加成功!",
// 跳转到该路由
url: "/admin/category"
});
return;
}
});
}
});
});

最后重启服务器进行测试:
添加分类
分类的添加完成。


分类管理的修改及删除

分类的修改是通过 GET 方式将需要修改的分类提交到服务器,然后服务器渲染出其修改的界面,管理员再对该数据进行相应的修改。
在 /views/admin/category 目录下新建一个 ejs 模板,分类修改的界面,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 引入页头模板 -->
<%- include("../header") %>
<!-- 内容 -->
<div class="container-fluid">
<ol class="breadcrumb">
<li><a href="/admin">管理首页</a></li>
<li><a href="/admin/category">分类首页</a></li>
<li class="active">分类修改</li>
</ol>
<form method="post">
<div class="form-group">
<label for="name">请输入新名称</label>
<input type="text" class="form-control" id="name" name="name" placeholder="请输入分类名称" value="<%=category.name%>">
</div>
<button type="submit" class="btn btn-default">提交</button>
</form>
</div>
</body>
</html>

接着在后台管理的路由文件 admin.js 中添加分类修改的路由 GET /admin/category/edit 处理,添加代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 分类的修改界面
router.get("/category/edit", (req, res, next) => {
// 获取用户提交过来的id
let id = req.query.id || "";
// 根据id从数据库中查询相关数据
categoryModel.findOne({_id: id}, (err, category) => {
if (category) {
// 如何数据存在则渲染修改界面
res.render("admin/category/edit", {
userInfo: req.userInfo,
category: category
});
} else {
// 若不存在渲染错误提示面板
res.render("admin/error", {
userInfo: req.userInfo,
url: null,
message: "该分类不存在!"
});
}
});
});

修改界面的路由完成之后,我们再写分类修改的保存,修改的保存是以 POST 的方式向服务器提交数据。在 admin.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
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
// 分类修改的保存
router.post("/category/edit", (req, res, next) => {
// 获取修改后的id及名称
let id = req.query.id;
let name = req.body.name;

// 根据id从数据库中查询相关数据
categoryModel.findById(id, (err, category) => {
if (category) {
// 若数据存在
// 简单验证---如果数据没修改
if (name === category.name) {
res.render("admin/success", {
url: "/admin/category",
userInfo: req.userInfo,
message: "修改成功!"
});
return;
}
// 查询用户修改的分类是否与数据库中的冲突
categoryModel.findOne({
_id: {$ne: id},
name: name
}, (err, docs) => {
if (docs) {
// 数据冲突
res.render("admin/error", {
userInfo: req.userInfo,
url: null,
message: "该分类已存在!"
});
return;
} else {
// 更新数据
categoryModel.update({_id: id}, {$set: {name: name}}, (err) => {
if (!err) {
// 不出错
res.render("admin/success", {
userInfo: req.userInfo,
url: "/admin/category",
message: "修改成功!"
});
return;
} else {
// 出错
res.render("admin/error", {
userInfo: req.userInfo,
url: null,
message: "修改失败!"
});
return;
}
});
}
});
} else {
// 若不存在
res.render("admin/error", {
userInfo: req.userInfo,
url: null,
message: "该分类不存在!"
});
return;
}
});
});


分类的删除同样也是通过 GET 方式将需要删除分类的 id 提交到服务器进行数据库删除,分类删除的路由设计为 GET /admin/category/delete 。在 admin.js 添加如下代码,处理删除分类逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 分类的删除
router.get("/category/delete", (req, res, next) => {
// 获取需要删除的分类id
let id = req.query.id || "";
// 从数据库中删除数据
categoryModel.remove({_id: id}, (err) => {
if (!err) {
// 删除成功
res.render("admin/success", {
url: "/admin/category",
userInfo: req.userInfo,
message: "删除成功!"
});
} else {
// 删除失败
res.render("admin/error", {
url: null,
userInfo: req.userInfo,
message: "删除失败!"
});
}
});
});

最后重启服务器,在客户端中进行测试:
分类完整测试
好了,到此分类的添加,删除,修改基本上已经完成了。

------ 本文结束 ------

版权声明

Tflin's Blog by Tan Feng Lin is licensed under a Creative Commons BY-NC-ND 4.0 International License.
谭丰林创作并维护的Tflin's Blog博客采用创作共用保留署名-非商业-禁止演绎4.0国际许可证
本文首发于Tflin's Blog 博客( http://tflin.com ),版权所有,侵权必究。