4.9.1 文章模型设计
我们只存储文章的作者 id、标题、正文和点击量这几个字段,对应修改 lib/mongo.js,添加如下代码:
lib/mongo.js
exports.Post = mongolass.model('Post', {author: { type: Mongolass.Types.ObjectId, required: true },title: { type: 'string', required: true },content: { type: 'string', required: true },pv: { type: 'number', default: 0 }})exports.Post.index({ author: 1, _id: -1 }).exec()// 按创建时间降序查看用户的文章列表
4.9.2 发表文章
现在我们来实现发表文章的功能。首先创建发表文章页,新建 views/create.ejs,添加如下代码:
views/create.ejs
<%- include('header') %><div class="ui grid"><div class="four wide column"><a class="avatar avatar-link"href="/posts?author=<%= user._id %>"data-title="<%= user.name %> | <%= ({m: '男', f: '女', x: '保密'})[user.gender] %>"data-content="<%= user.bio %>"><img class="avatar" src="/img/<%= user.avatar %>"></a></div><div class="eight wide column"><form class="ui form segment" method="post"><div class="field required"><label>标题</label><input type="text" name="title"></div><div class="field required"><label>内容</label><textarea name="content" rows="15"></textarea></div><input type="submit" class="ui button" value="发布"></form></div></div><%- include('footer') %>
修改 routes/posts.js,将:
// GET /posts/create 发表文章页router.get('/create', checkLogin, function (req, res, next) {res.send('发表文章页')})
修改为:
// GET /posts/create 发表文章页router.get('/create', checkLogin, function (req, res, next) {res.render('create')})
登录成功状态,点击右上角『发表文章』试下吧。
发表文章页已经完成了,接下来新建 models/posts.js 用来存放与文章操作相关的代码:
models/posts.js
const Post = require('../lib/mongo').Postmodule.exports = {// 创建一篇文章create: function create (post) {return Post.create(post).exec()}}
修改 routes/posts.js,在文件上方引入 PostModel:
routes/posts.js
const PostModel = require('../models/posts')
将:
// POST /posts/create 发表一篇文章router.post('/create', checkLogin, function (req, res, next) {res.send('发表文章')})
修改为:
// POST /posts/create 发表一篇文章router.post('/create', checkLogin, function (req, res, next) {const author = req.session.user._idconst title = req.fields.titleconst content = req.fields.content// 校验参数try {if (!title.length) {throw new Error('请填写标题')}if (!content.length) {throw new Error('请填写内容')}} catch (e) {req.flash('error', e.message)return res.redirect('back')}let post = {author: author,title: title,content: content}PostModel.create(post).then(function (result) {// 此 post 是插入 mongodb 后的值,包含 _idpost = result.ops[0]req.flash('success', '发表成功')// 发表成功后跳转到该文章页res.redirect(`/posts/${post._id}`)}).catch(next)})
这里校验了上传的表单字段,并将文章信息插入数据库,成功后跳转到该文章页并显示『发表成功』的通知,失败后请求会进入错误处理函数。
现在刷新页面(登录情况下),点击右上角 发表文章 试试吧,发表成功后跳转到了文章页但并没有任何内容,下面我们就来实现文章页及主页。
4.9.3 主页与文章页
现在我们来实现主页及文章页。修改 models/posts.js 如下:
models/posts.js
const marked = require('marked')const Post = require('../lib/mongo').Post// 将 post 的 content 从 markdown 转换成 htmlPost.plugin('contentToHtml', {afterFind: function (posts) {return posts.map(function (post) {post.content = marked(post.content)return post})},afterFindOne: function (post) {if (post) {post.content = marked(post.content)}return post}})module.exports = {// 创建一篇文章create: function create (post) {return Post.create(post).exec()},// 通过文章 id 获取一篇文章getPostById: function getPostById (postId) {return Post.findOne({ _id: postId }).populate({ path: 'author', model: 'User' }).addCreatedAt().contentToHtml().exec()},// 按创建时间降序获取所有用户文章或者某个特定用户的所有文章getPosts: function getPosts (author) {const query = {}if (author) {query.author = author}return Post.find(query).populate({ path: 'author', model: 'User' }).sort({ _id: -1 }).addCreatedAt().contentToHtml().exec()},// 通过文章 id 给 pv 加 1incPv: function incPv (postId) {return Post.update({ _id: postId }, { $inc: { pv: 1 } }).exec()}}
需要讲解两点:
- 我们使用了 markdown 解析文章的内容,所以在发表文章的时候可使用 markdown 语法(如插入链接、图片等等),关于 markdown 的使用请参考: Markdown 语法说明。
- 我们在 PostModel 上注册了
contentToHtml,而addCreatedAt是在 lib/mongo.js 中 mongolass 上注册的。也就是说contentToHtml只针对 PostModel 有效,而addCreatedAt对所有 Model 都有效。
接下来完成主页的模板,修改 views/posts.ejs 如下:
views/posts.ejs
<%- include('header') %><% posts.forEach(function (post) { %><%- include('components/post-content', { post: post }) %><% }) %><%- include('footer') %>
新建 views/components/post-content.ejs 用来存放单篇文章的模板片段:
views/components/post-content.ejs
<div class="post-content"><div class="ui grid"><div class="four wide column"><a class="avatar avatar-link"href="/posts?author=<%= post.author._id %>"data-title="<%= post.author.name %> | <%= ({m: '男', f: '女', x: '保密'})[post.author.gender] %>"data-content="<%= post.author.bio %>"><img class="avatar" src="/img/<%= post.author.avatar %>"></a></div><div class="eight wide column"><div class="ui segment"><h3><a href="/posts/<%= post._id %>"><%= post.title %></a></h3><pre><%- post.content %></pre><div><span class="tag"><%= post.created_at %></span><span class="tag right"><span>浏览(<%= post.pv || 0 %>)</span><span>留言(<%= post.commentsCount || 0 %>)</span><% if (user && post.author._id && user._id.toString() === post.author._id.toString()) { %><div class="ui inline dropdown"><div class="text"></div><i class="dropdown icon"></i><div class="menu"><div class="item"><a href="/posts/<%= post._id %>/edit">编辑</a></div><div class="item"><a href="/posts/<%= post._id %>/remove">删除</a></div></div></div><% } %></span></div></div></div></div></div>
注意:我们用了
<%- post.content %>,而不是<%= post.content %>,因为 post.content 是 markdown 转换后的 html 字符串。
修改 routes/posts.js,将:
routes/posts.js
router.get('/', function (req, res, next) {res.render('posts')})
修改为:
router.get('/', function (req, res, next) {const author = req.query.authorPostModel.getPosts(author).then(function (posts) {res.render('posts', {posts: posts})}).catch(next)})
注意:主页与用户页通过 url 中的 author 区分。
现在完成了主页与用户页,访问 http://localhost:3000/posts 试试吧,现在已经将我们之前创建的文章显示出来了,尝试点击用户的头像看看效果。
接下来完成文章详情页。新建 views/post.ejs,添加如下代码:
views/post.ejs
<%- include('header') %><%- include('components/post-content') %><%- include('footer') %>
打开 routes/posts.js,将:
routes/posts.js
// GET /posts/:postId 单独一篇的文章页router.get('/:postId', function (req, res, next) {res.send('文章详情页')})
修改为:
// GET /posts/:postId 单独一篇的文章页router.get('/:postId', function (req, res, next) {const postId = req.params.postIdPromise.all([PostModel.getPostById(postId), // 获取文章信息PostModel.incPv(postId)// pv 加 1]).then(function (result) {const post = result[0]if (!post) {throw new Error('该文章不存在')}res.render('post', {post: post})}).catch(next)})
现在刷新浏览器,点击文章的标题看看浏览器地址的变化吧。
注意:浏览器地址有变化,但页面看不出区别来(因为页面布局一样),后面我们添加留言功能后就能看出区别来了。
4.9.4 编辑与删除文章
现在我们来完成编辑与删除文章的功能。修改 models/posts.js,在 module.exports 对象上添加如下 3 个方法:
models/posts.js
// 通过文章 id 获取一篇原生文章(编辑文章)getRawPostById: function getRawPostById (postId) {return Post.findOne({ _id: postId }).populate({ path: 'author', model: 'User' }).exec()},// 通过文章 id 更新一篇文章updatePostById: function updatePostById (postId, data) {return Post.update({ _id: postId }, { $set: data }).exec()},// 通过文章 id 删除一篇文章delPostById: function delPostById (postId) {return Post.deleteOne({ _id: postId }).exec()}
注意:不要忘了在适当位置添加逗号,如 incPv 的结束大括号后。
注意:我们通过新函数
getRawPostById用来获取文章原生的内容(编辑页面用),而不是用getPostById返回将 markdown 转换成 html 后的内容。
新建编辑文章页 views/edit.ejs,添加如下代码:
views/edit.ejs
<%- include('header') %><div class="ui grid"><div class="four wide column"><a class="avatar"href="/posts?author=<%= user._id %>"data-title="<%= user.name %> | <%= ({m: '男', f: '女', x: '保密'})[user.gender] %>"data-content="<%= user.bio %>"><img class="avatar" src="/img/<%= user.avatar %>"></a></div><div class="eight wide column"><form class="ui form segment" method="post" action="/posts/<%= post._id %>/edit"><div class="field required"><label>标题</label><input type="text" name="title" value="<%= post.title %>"></div><div class="field required"><label>内容</label><textarea name="content" rows="15"><%= post.content %></textarea></div><input type="submit" class="ui button" value="发布"></form></div></div><%- include('footer') %>
修改 routes/posts.js,将:
routes/posts.js
// GET /posts/:postId/edit 更新文章页router.get('/:postId/edit', checkLogin, function (req, res, next) {res.send('更新文章页')})// POST /posts/:postId/edit 更新一篇文章router.post('/:postId/edit', checkLogin, function (req, res, next) {res.send('更新文章')})// GET /posts/:postId/remove 删除一篇文章router.get('/:postId/remove', checkLogin, function (req, res, next) {res.send('删除文章')})
修改为:
// GET /posts/:postId/edit 更新文章页router.get('/:postId/edit', checkLogin, function (req, res, next) {const postId = req.params.postIdconst author = req.session.user._idPostModel.getRawPostById(postId).then(function (post) {if (!post) {throw new Error('该文章不存在')}if (author.toString() !== post.author._id.toString()) {throw new Error('权限不足')}res.render('edit', {post: post})}).catch(next)})// POST /posts/:postId/edit 更新一篇文章router.post('/:postId/edit', checkLogin, function (req, res, next) {const postId = req.params.postIdconst author = req.session.user._idconst title = req.fields.titleconst content = req.fields.content// 校验参数try {if (!title.length) {throw new Error('请填写标题')}if (!content.length) {throw new Error('请填写内容')}} catch (e) {req.flash('error', e.message)return res.redirect('back')}PostModel.getRawPostById(postId).then(function (post) {if (!post) {throw new Error('文章不存在')}if (post.author._id.toString() !== author.toString()) {throw new Error('没有权限')}PostModel.updatePostById(postId, { title: title, content: content }).then(function () {req.flash('success', '编辑文章成功')// 编辑成功后跳转到上一页res.redirect(`/posts/${postId}`)}).catch(next)})})// GET /posts/:postId/remove 删除一篇文章router.get('/:postId/remove', checkLogin, function (req, res, next) {const postId = req.params.postIdconst author = req.session.user._idPostModel.getRawPostById(postId).then(function (post) {if (!post) {throw new Error('文章不存在')}if (post.author._id.toString() !== author.toString()) {throw new Error('没有权限')}PostModel.delPostById(postId).then(function () {req.flash('success', '删除文章成功')// 删除成功后跳转到主页res.redirect('/posts')}).catch(next)})})
现在刷新主页,点击文章右下角的小三角,编辑文章和删除文章试试吧。
