Node.js + Express + mongodb 的博客项目之博客文章内容详情界面及文章评论(九)

前言

本节内容中将实现博客的文章详情页面还有文章的评论功能。博客的文章详情页面与博客的首页的页头是一样的,所以为了以后便于维护,就将两个页面的页头给分离出来作为一个单独的模块 header.ejs 然后在两个页面中引入该页头模块。博客的文章内容详情的路由设计为 /views?contentId=XXX ,前端通过 GET 方式向后端提交需要渲染出来的内容的 id 。而 Markdown 渲染文章的内容则是通过 marked 模块来将内容渲染成相应 html 后,再在模板引擎中使用 <- include()=""> 引用。


博客首页页头文件的模块

在项目目录下的视图文件夹 /views/main 中新建一个 header.ejs 文件,在将 /views/main/index.ejs 中的页头部分去掉,然后在/views/main/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
<!-- 博客首页页头模板 -->
<!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>TFLIN'BLOG</title>
</head>
<body>
<!-- 页头 -->
<header>
<header>
<nav class="navbar navbar-default">
<div class="navbar-header">
<a class="navbar-brand" href="/">TFL'BLOG</a>
</div>
<div class="container">
<ul class="nav navbar-nav">
<% if (!other.categoryId) {%>
<li class="active"><a href="/">首页</a></li>
<% } else {%>
<li><a href="/">首页</a></li>
<% } %>
<% for (let i = 0, len = other.categories.length; i < len; i++) {%>
<% if (other.categoryId === other.categories[i].id) {%>
<li class="active"><a href="/?categoryId=<%= other.categories[i].id %>"><%= other.categories[i].name %></a></li>
<% } else {%>
<li><a href="/?categoryId=<%= other.categories[i].id %>"><%= other.categories[i].name %></a></li>
<% } %>
<% } %>
</ul>
</div>
</nav>
</header>
</header>

<script src="/public/js/3.3.1-jquery-min.js"></script>
<script src="/public/js/bootstrap.min.js"></script>

再在 /views/main/index.ejs 中,头部引用 header.js 模块:

1
2
<!-- 引入页头 -->
<%- include("header") %>


然后新建一个视图文件 /views/main/views.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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<!-- 引入页头模板 -->
<%- include("header") %>
<!-- 主体 -->
<div class="container">
<!-- 内容部分 -->
<div class="col-md-8">
<div class="jumbotron">
<div class="container">
<h2><%= content.title %></h2>
<p>
<span class="text-info">阅读量:<%=content.views%> | 时间:<%=content.addTime%> | 作者:<%=content.author.username%> | 分类:<%=content.category.name%></span>
</p>
<p><%= content.description %></p>
<hr>
<p><%= content.content %></p>
</div>
</div>
</div>
<!-- 登录、注册、用户面板部分 -->
<div class="col-md-4">
<%if (!userInfo.userid) {%>
<!-- 登录面板 -->
<div id="login">
<h2>登录</h2>
<div class="input-group">
<span class="input-group-addon">用户</span>
<input type="text" class="form-control" name="username" placeholder="请输入用户名">
</div>
<br>
<div class="input-group">
<span class="input-group-addon">密码</span>
<input type="password" class="form-control" name="password" placeholder="请输入密码">
</div>
<br>
<button type="button" name="button" class="btn btn-primary form-control" id="login-btn">登录</button>
<br><br>
<a href="javascripts:;" class="col-md-offset-4">没有账号?点击注册</a>
<div class="alert alert-success alert-dismissable hide" role="alert">
<button class="close" type="button">×</button>
<span>恭喜您操作成功!</span>
</div>
</div>
<!-- 注册面板 -->
<div id="reg" class="hide">
<h2>注册</h2>
<div class="input-group">
<span class="input-group-addon">用户名称</span>
<input type="text" class="form-control" name="username" placeholder="请输入用户名">
</div>
<br>
<div class="input-group">
<span class="input-group-addon">输入密码</span>
<input type="password" class="form-control" name="password" placeholder="请输入密码">
</div>
<br>
<div class="input-group">
<span class="input-group-addon">确认密码</span>
<input type="password" class="form-control" name="repassword" placeholder="请再次输入密码">
</div>
<br>
<button type="button" name="button" class="btn btn-primary form-control" id="reg-btn">注册</button>
<br><br>
<a href="javascripts:;" class="col-md-offset-4">已有账号?点击登录</a>
<!-- 警告框 -->
<div class="alert alert-success alert-dismissable hide" role="alert">
<button class="close" type="button">×</button>
<span>恭喜您操作成功!</span>
</div>
</div>
<%} else {%>
<!-- 用户面板 -->
<div id="user-info">
<div class="panel panel-primary">
<div class="panel-heading">用户面板</div>
<div class="panel-body">
<%if (userInfo.isadmin) {%>
<h3>欢迎尊贵的管理员,<%=userInfo.username%>!</h3>
<p>
<a href="/admin" class="btn btn-primary form-control">进入控制台</a>
</p>
<%} else {%>
<h3>欢迎您,<%=userInfo.username%></h3>
<%}%>
<p>
<button type="button" name="button" id="logout" class="btn btn-danger form-control">注销</button>
</p>
</div>
</div>
</div>
<%}%>
</div>
</div>
<script src="/public/js/main.js"></script>
</body>
</html>

其实登录和注册及用户管理面板也可以抽离出来,分成一个单独的模块进行开发。


内容详情的路由

在 /routes/mian.js 中处理首页的相关路由,其中页头的分类信息是两个页面所共有的,所以要使用一个中间件来处理通用的信息。
将 /routes/mian.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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 引入相关模块
const express = require("express");
const categoryModel = require("../models/category");
const contentModel = require("../models/content");
// 引入自定义的分页渲染模块
const pagination = require("../my_modules/pagination");

// 实例化Router对象
const router = express.Router();

// 定义一个变量用来存放传递给模板的其他信息
let other = {};
// 分类查询条件
let where = {};

// 处理通用数据
router.use("/", (req, res, next) => {
// 接收前端传递过来的需要查询分类的id
if (req.query.categoryId) {
// 如果前端传有数据过来
other.categoryId = req.query.categoryId;
where.category = req.query.categoryId;
} else {
// 没有则置空,以方便模板引擎判断渲染不同的面板
where = {};
other.categoryId = null;
}
// 从数据库中查询出分类信息
categoryModel.find({}, (err, categories) => {
if (!err) {
// 如果不出错
other.categories = categories;
} else {
throw err;
}
});
// 继续向下一个中间件走
next();
});

// 首页路由配置
router.get("/", (req, res) => {

// 调用分页渲染模块渲染内容
pagination({
// 每页显示的条数
limit: 10,
// 需要操作的数据库模型
model: contentModel,
// 需要控制分页的url
url: "/",
// 渲染的模板页面
ejs: "main/index",
// 查询的条件
where: where,
// 给模板绑定参数的名称
res: res,
req: req,
populate: ["category", "author"],
// 其他数据
other: other
});
});

// 内容页面
router.get("/views", (req, res) => {
// 获取文章id
let contentId = req.query.contentId;
// 根据id从数据库中查询文章内容
contentModel.findById(contentId).populate(["category", "author"]).then((content) => {
// 渲染内容模板
res.render("main/views", {
userInfo: req.userInfo,
other: other,
content: content
});
// 阅读量增加
content.views ++;
content.save();
});
});

// 将其暴露给外部使用
module.exports = router;


内容详情的测试

此时,重启服务器,刷新客户端,往数据库中添加几条文章内容进行测试:
文章详情测试


使用 Markdown 语法渲染文章内容

在 Node.js 中一般有两个模块可以解析 Markdown ,一个是 Markdown 模块,还有一个是 Marked 模块,在这个博客项目中,我选用的是 Marked 模块。Marked 是一个 Markdown 解析、编译器,使用 marked.setOptions() 传入一个对象,进行配置,参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
-o, –output [output]: 指定输出文件,默认为当前控制台
-i, –input [input]: 指定输入文件或最后一个参数,默认为当前控制台输入
-t, –tokens: 输出token流代替HTML
–pedantic: 只解析符合markdown.pl定义的,不修正markdown的错误
–gfm: 启动Github样式的Markdown
–breaks: 支持Github换行符,必须打开gfm选项
–tables: 支持Github表格,必须打开gfm选项
–sanitize: 原始输出,忽略HTML标签
–smart-lists: 优化列表输出
–lang-prefix [prefix]: 设置前置样式
–no-etc: 选择的反正标识
–silent: 不输出错误信息
-h, –help: 帮助信息

更多信息请阅读官方文档 - Marked 的官方文档


首先在 /routes/main.js 中引入 Marked 模块,并进行相应配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
const marked = require("marked");

// 配置marked
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true,
tables: false,
breaks: false,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false
});

然后在修改 /routes/main.js 中文章内容详情的处理,在将文章内容从数据库中查询出来后通过 Marked 将 Markdown 语法渲染成 HTML 的格式后,再绑定给模板引擎去渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 内容页面
router.get("/views", (req, res) => {
// 获取文章id
let contentId = req.query.contentId;
// 根据id从数据库中查询文章内容
contentModel.findById(contentId).populate(["category", "author"]).then((content) => {
// 使用marked渲染内容成html
let contentHtml = marked(content.content);
// 渲染内容模板
res.render("main/views", {
userInfo: req.userInfo,
other: other,
contentHtml: contentHtml,
content: content
});
// 阅读量增加
content.views ++;
content.save();
});
});

接着我们在重启服务器后,新建一篇使用 Markdown 语法所写的文章,然后进行测试:
Markdown测试
提交后去博客首页查看发现已经渲染成功
Markdown渲染测试

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

版权声明

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 ),版权所有,侵权必究。