前言
本节内容中将实现博客的文章详情页面还有文章的评论功能。博客的文章详情页面与博客的首页的页头是一样的,所以为了以后便于维护,就将两个页面的页头给分离出来作为一个单独的模块 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"> <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");
const router = express.Router();
let other = {};
let where = {};
router.use("/", (req, res, next) => { 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: "/", ejs: "main/index", where: where, res: res, req: req, populate: ["category", "author"], other: other }); });
router.get("/views", (req, res) => { let contentId = req.query.contentId; 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.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) => { let contentId = req.query.contentId; contentModel.findById(contentId).populate(["category", "author"]).then((content) => { let contentHtml = marked(content.content); res.render("main/views", { userInfo: req.userInfo, other: other, contentHtml: contentHtml, content: content }); content.views ++; content.save(); }); });
|
接着我们在重启服务器后,新建一篇使用 Markdown 语法所写的文章,然后进行测试:
提交后去博客首页查看发现已经渲染成功