4.9.1 文章模型设计

我们只存储文章的作者 id、标题、正文和点击量这几个字段,对应修改 lib/mongo.js,添加如下代码:

lib/mongo.js

  1. exports.Post = mongolass.model('Post', {
  2. author: { type: Mongolass.Types.ObjectId, required: true },
  3. title: { type: 'string', required: true },
  4. content: { type: 'string', required: true },
  5. pv: { type: 'number', default: 0 }
  6. })
  7. exports.Post.index({ author: 1, _id: -1 }).exec()// 按创建时间降序查看用户的文章列表

4.9.2 发表文章

现在我们来实现发表文章的功能。首先创建发表文章页,新建 views/create.ejs,添加如下代码:

views/create.ejs

  1. <%- include('header') %>
  2. <div class="ui grid">
  3. <div class="four wide column">
  4. <a class="avatar avatar-link"
  5. href="/posts?author=<%= user._id %>"
  6. data-title="<%= user.name %> | <%= ({m: '男', f: '女', x: '保密'})[user.gender] %>"
  7. data-content="<%= user.bio %>">
  8. <img class="avatar" src="/img/<%= user.avatar %>">
  9. </a>
  10. </div>
  11. <div class="eight wide column">
  12. <form class="ui form segment" method="post">
  13. <div class="field required">
  14. <label>标题</label>
  15. <input type="text" name="title">
  16. </div>
  17. <div class="field required">
  18. <label>内容</label>
  19. <textarea name="content" rows="15"></textarea>
  20. </div>
  21. <input type="submit" class="ui button" value="发布">
  22. </form>
  23. </div>
  24. </div>
  25. <%- include('footer') %>

修改 routes/posts.js,将:

  1. // GET /posts/create 发表文章页
  2. router.get('/create', checkLogin, function (req, res, next) {
  3. res.send('发表文章页')
  4. })

修改为:

  1. // GET /posts/create 发表文章页
  2. router.get('/create', checkLogin, function (req, res, next) {
  3. res.render('create')
  4. })

登录成功状态,点击右上角『发表文章』试下吧。

发表文章页已经完成了,接下来新建 models/posts.js 用来存放与文章操作相关的代码:

models/posts.js

  1. const Post = require('../lib/mongo').Post
  2. module.exports = {
  3. // 创建一篇文章
  4. create: function create (post) {
  5. return Post.create(post).exec()
  6. }
  7. }

修改 routes/posts.js,在文件上方引入 PostModel:

routes/posts.js

  1. const PostModel = require('../models/posts')

将:

  1. // POST /posts/create 发表一篇文章
  2. router.post('/create', checkLogin, function (req, res, next) {
  3. res.send('发表文章')
  4. })

修改为:

  1. // POST /posts/create 发表一篇文章
  2. router.post('/create', checkLogin, function (req, res, next) {
  3. const author = req.session.user._id
  4. const title = req.fields.title
  5. const content = req.fields.content
  6. // 校验参数
  7. try {
  8. if (!title.length) {
  9. throw new Error('请填写标题')
  10. }
  11. if (!content.length) {
  12. throw new Error('请填写内容')
  13. }
  14. } catch (e) {
  15. req.flash('error', e.message)
  16. return res.redirect('back')
  17. }
  18. let post = {
  19. author: author,
  20. title: title,
  21. content: content
  22. }
  23. PostModel.create(post)
  24. .then(function (result) {
  25. // 此 post 是插入 mongodb 后的值,包含 _id
  26. post = result.ops[0]
  27. req.flash('success', '发表成功')
  28. // 发表成功后跳转到该文章页
  29. res.redirect(`/posts/${post._id}`)
  30. })
  31. .catch(next)
  32. })

这里校验了上传的表单字段,并将文章信息插入数据库,成功后跳转到该文章页并显示『发表成功』的通知,失败后请求会进入错误处理函数。

现在刷新页面(登录情况下),点击右上角 发表文章 试试吧,发表成功后跳转到了文章页但并没有任何内容,下面我们就来实现文章页及主页。

4.9.3 主页与文章页

现在我们来实现主页及文章页。修改 models/posts.js 如下:

models/posts.js

  1. const marked = require('marked')
  2. const Post = require('../lib/mongo').Post
  3. // 将 post 的 content 从 markdown 转换成 html
  4. Post.plugin('contentToHtml', {
  5. afterFind: function (posts) {
  6. return posts.map(function (post) {
  7. post.content = marked(post.content)
  8. return post
  9. })
  10. },
  11. afterFindOne: function (post) {
  12. if (post) {
  13. post.content = marked(post.content)
  14. }
  15. return post
  16. }
  17. })
  18. module.exports = {
  19. // 创建一篇文章
  20. create: function create (post) {
  21. return Post.create(post).exec()
  22. },
  23. // 通过文章 id 获取一篇文章
  24. getPostById: function getPostById (postId) {
  25. return Post
  26. .findOne({ _id: postId })
  27. .populate({ path: 'author', model: 'User' })
  28. .addCreatedAt()
  29. .contentToHtml()
  30. .exec()
  31. },
  32. // 按创建时间降序获取所有用户文章或者某个特定用户的所有文章
  33. getPosts: function getPosts (author) {
  34. const query = {}
  35. if (author) {
  36. query.author = author
  37. }
  38. return Post
  39. .find(query)
  40. .populate({ path: 'author', model: 'User' })
  41. .sort({ _id: -1 })
  42. .addCreatedAt()
  43. .contentToHtml()
  44. .exec()
  45. },
  46. // 通过文章 id 给 pv 加 1
  47. incPv: function incPv (postId) {
  48. return Post
  49. .update({ _id: postId }, { $inc: { pv: 1 } })
  50. .exec()
  51. }
  52. }

需要讲解两点:

  1. 我们使用了 markdown 解析文章的内容,所以在发表文章的时候可使用 markdown 语法(如插入链接、图片等等),关于 markdown 的使用请参考: Markdown 语法说明
  2. 我们在 PostModel 上注册了 contentToHtml,而 addCreatedAt 是在 lib/mongo.js 中 mongolass 上注册的。也就是说 contentToHtml 只针对 PostModel 有效,而 addCreatedAt 对所有 Model 都有效。

接下来完成主页的模板,修改 views/posts.ejs 如下:

views/posts.ejs

  1. <%- include('header') %>
  2. <% posts.forEach(function (post) { %>
  3. <%- include('components/post-content', { post: post }) %>
  4. <% }) %>
  5. <%- include('footer') %>

新建 views/components/post-content.ejs 用来存放单篇文章的模板片段:

views/components/post-content.ejs

  1. <div class="post-content">
  2. <div class="ui grid">
  3. <div class="four wide column">
  4. <a class="avatar avatar-link"
  5. href="/posts?author=<%= post.author._id %>"
  6. data-title="<%= post.author.name %> | <%= ({m: '男', f: '女', x: '保密'})[post.author.gender] %>"
  7. data-content="<%= post.author.bio %>">
  8. <img class="avatar" src="/img/<%= post.author.avatar %>">
  9. </a>
  10. </div>
  11. <div class="eight wide column">
  12. <div class="ui segment">
  13. <h3><a href="/posts/<%= post._id %>"><%= post.title %></a></h3>
  14. <pre><%- post.content %></pre>
  15. <div>
  16. <span class="tag"><%= post.created_at %></span>
  17. <span class="tag right">
  18. <span>浏览(<%= post.pv || 0 %>)</span>
  19. <span>留言(<%= post.commentsCount || 0 %>)</span>
  20. <% if (user && post.author._id && user._id.toString() === post.author._id.toString()) { %>
  21. <div class="ui inline dropdown">
  22. <div class="text"></div>
  23. <i class="dropdown icon"></i>
  24. <div class="menu">
  25. <div class="item"><a href="/posts/<%= post._id %>/edit">编辑</a></div>
  26. <div class="item"><a href="/posts/<%= post._id %>/remove">删除</a></div>
  27. </div>
  28. </div>
  29. <% } %>
  30. </span>
  31. </div>
  32. </div>
  33. </div>
  34. </div>
  35. </div>

注意:我们用了 <%- post.content %>,而不是 <%= post.content %>,因为 post.content 是 markdown 转换后的 html 字符串。

修改 routes/posts.js,将:

routes/posts.js

  1. router.get('/', function (req, res, next) {
  2. res.render('posts')
  3. })

修改为:

  1. router.get('/', function (req, res, next) {
  2. const author = req.query.author
  3. PostModel.getPosts(author)
  4. .then(function (posts) {
  5. res.render('posts', {
  6. posts: posts
  7. })
  8. })
  9. .catch(next)
  10. })

注意:主页与用户页通过 url 中的 author 区分。

现在完成了主页与用户页,访问 http://localhost:3000/posts 试试吧,现在已经将我们之前创建的文章显示出来了,尝试点击用户的头像看看效果。

接下来完成文章详情页。新建 views/post.ejs,添加如下代码:

views/post.ejs

  1. <%- include('header') %>
  2. <%- include('components/post-content') %>
  3. <%- include('footer') %>

打开 routes/posts.js,将:

routes/posts.js

  1. // GET /posts/:postId 单独一篇的文章页
  2. router.get('/:postId', function (req, res, next) {
  3. res.send('文章详情页')
  4. })

修改为:

  1. // GET /posts/:postId 单独一篇的文章页
  2. router.get('/:postId', function (req, res, next) {
  3. const postId = req.params.postId
  4. Promise.all([
  5. PostModel.getPostById(postId), // 获取文章信息
  6. PostModel.incPv(postId)// pv 加 1
  7. ])
  8. .then(function (result) {
  9. const post = result[0]
  10. if (!post) {
  11. throw new Error('该文章不存在')
  12. }
  13. res.render('post', {
  14. post: post
  15. })
  16. })
  17. .catch(next)
  18. })

现在刷新浏览器,点击文章的标题看看浏览器地址的变化吧。

注意:浏览器地址有变化,但页面看不出区别来(因为页面布局一样),后面我们添加留言功能后就能看出区别来了。

4.9.4 编辑与删除文章

现在我们来完成编辑与删除文章的功能。修改 models/posts.js,在 module.exports 对象上添加如下 3 个方法:

models/posts.js

  1. // 通过文章 id 获取一篇原生文章(编辑文章)
  2. getRawPostById: function getRawPostById (postId) {
  3. return Post
  4. .findOne({ _id: postId })
  5. .populate({ path: 'author', model: 'User' })
  6. .exec()
  7. },
  8. // 通过文章 id 更新一篇文章
  9. updatePostById: function updatePostById (postId, data) {
  10. return Post.update({ _id: postId }, { $set: data }).exec()
  11. },
  12. // 通过文章 id 删除一篇文章
  13. delPostById: function delPostById (postId) {
  14. return Post.deleteOne({ _id: postId }).exec()
  15. }

注意:不要忘了在适当位置添加逗号,如 incPv 的结束大括号后。

注意:我们通过新函数 getRawPostById 用来获取文章原生的内容(编辑页面用),而不是用 getPostById 返回将 markdown 转换成 html 后的内容。

新建编辑文章页 views/edit.ejs,添加如下代码:

views/edit.ejs

  1. <%- include('header') %>
  2. <div class="ui grid">
  3. <div class="four wide column">
  4. <a class="avatar"
  5. href="/posts?author=<%= user._id %>"
  6. data-title="<%= user.name %> | <%= ({m: '男', f: '女', x: '保密'})[user.gender] %>"
  7. data-content="<%= user.bio %>">
  8. <img class="avatar" src="/img/<%= user.avatar %>">
  9. </a>
  10. </div>
  11. <div class="eight wide column">
  12. <form class="ui form segment" method="post" action="/posts/<%= post._id %>/edit">
  13. <div class="field required">
  14. <label>标题</label>
  15. <input type="text" name="title" value="<%= post.title %>">
  16. </div>
  17. <div class="field required">
  18. <label>内容</label>
  19. <textarea name="content" rows="15"><%= post.content %></textarea>
  20. </div>
  21. <input type="submit" class="ui button" value="发布">
  22. </form>
  23. </div>
  24. </div>
  25. <%- include('footer') %>

修改 routes/posts.js,将:

routes/posts.js

  1. // GET /posts/:postId/edit 更新文章页
  2. router.get('/:postId/edit', checkLogin, function (req, res, next) {
  3. res.send('更新文章页')
  4. })
  5. // POST /posts/:postId/edit 更新一篇文章
  6. router.post('/:postId/edit', checkLogin, function (req, res, next) {
  7. res.send('更新文章')
  8. })
  9. // GET /posts/:postId/remove 删除一篇文章
  10. router.get('/:postId/remove', checkLogin, function (req, res, next) {
  11. res.send('删除文章')
  12. })

修改为:

  1. // GET /posts/:postId/edit 更新文章页
  2. router.get('/:postId/edit', checkLogin, function (req, res, next) {
  3. const postId = req.params.postId
  4. const author = req.session.user._id
  5. PostModel.getRawPostById(postId)
  6. .then(function (post) {
  7. if (!post) {
  8. throw new Error('该文章不存在')
  9. }
  10. if (author.toString() !== post.author._id.toString()) {
  11. throw new Error('权限不足')
  12. }
  13. res.render('edit', {
  14. post: post
  15. })
  16. })
  17. .catch(next)
  18. })
  19. // POST /posts/:postId/edit 更新一篇文章
  20. router.post('/:postId/edit', checkLogin, function (req, res, next) {
  21. const postId = req.params.postId
  22. const author = req.session.user._id
  23. const title = req.fields.title
  24. const content = req.fields.content
  25. // 校验参数
  26. try {
  27. if (!title.length) {
  28. throw new Error('请填写标题')
  29. }
  30. if (!content.length) {
  31. throw new Error('请填写内容')
  32. }
  33. } catch (e) {
  34. req.flash('error', e.message)
  35. return res.redirect('back')
  36. }
  37. PostModel.getRawPostById(postId)
  38. .then(function (post) {
  39. if (!post) {
  40. throw new Error('文章不存在')
  41. }
  42. if (post.author._id.toString() !== author.toString()) {
  43. throw new Error('没有权限')
  44. }
  45. PostModel.updatePostById(postId, { title: title, content: content })
  46. .then(function () {
  47. req.flash('success', '编辑文章成功')
  48. // 编辑成功后跳转到上一页
  49. res.redirect(`/posts/${postId}`)
  50. })
  51. .catch(next)
  52. })
  53. })
  54. // GET /posts/:postId/remove 删除一篇文章
  55. router.get('/:postId/remove', checkLogin, function (req, res, next) {
  56. const postId = req.params.postId
  57. const author = req.session.user._id
  58. PostModel.getRawPostById(postId)
  59. .then(function (post) {
  60. if (!post) {
  61. throw new Error('文章不存在')
  62. }
  63. if (post.author._id.toString() !== author.toString()) {
  64. throw new Error('没有权限')
  65. }
  66. PostModel.delPostById(postId)
  67. .then(function () {
  68. req.flash('success', '删除文章成功')
  69. // 删除成功后跳转到主页
  70. res.redirect('/posts')
  71. })
  72. .catch(next)
  73. })
  74. })

现在刷新主页,点击文章右下角的小三角,编辑文章和删除文章试试吧。