文章的增删改查

这节我们来实现一个文章相关功能:

发表文章 GET posts/new POST posts/new

文章详情 GET posts/:id

修改文章 GET posts/:id/edit POST posts/:id/edit

删除文章 GET /posts/:id/detele

文章列表直接就在 GET /GET /posts显示

  1. // routes/index.js
  2. ...
  3. router.get('/', require('./posts').index)
  4. ...
  5. router.get('/posts', require('./posts').index)
  6. router.get('/posts/new', require('./posts').create)
  7. router.post('/posts/new', require('./posts').create)
  8. router.get('/posts/:id', require('./posts').show)
  9. router.get('/posts/:id/edit', require('./posts').edit)
  10. router.post('/posts/:id/edit', require('./posts').edit)
  11. router.get('/posts/:id/delete', require('./posts').destroy)
  12. ...

文章模型设计

  1. // models/post.js
  2. const mongoose = require('mongoose')
  3. const Schema = mongoose.Schema
  4. const PostSchema = new Schema({
  5. author: {
  6. type: Schema.Types.ObjectId,
  7. ref: 'User',
  8. require: true
  9. },
  10. title: {
  11. type: String,
  12. required: true
  13. },
  14. content: {
  15. type: String,
  16. required: true
  17. },
  18. pv: {
  19. type: Number,
  20. default: 0
  21. },
  22. meta: {
  23. createdAt: {
  24. type: Date,
  25. default: Date.now()
  26. },
  27. updatedAt: {
  28. type: Date,
  29. default: Date.now()
  30. }
  31. }
  32. })
  33. PostSchema.pre('save', function (next) {
  34. if (this.isNew) {
  35. this.meta.createdAt = this.meta.updatedAt = Date.now()
  36. } else {
  37. this.meta.updatedAt = Date.now()
  38. }
  39. next()
  40. })
  41. module.exports = mongoose.model('Post', PostSchema)

这个文章模型,有作者、标题、内容、pv、创建时间、修改时间等。当然还应该有分类,额,我们之后再加。

上面我们用到了pre() 前置钩子来更新文章修改时间。

文章发表

先来实现创建文章的功能。新建个创建文章页views/create.html

create

  1. {% extends 'views/base.html' %}
  2. {% block body %}
  3. <form action="/posts/new" method="POST">
  4. <header class="editor-header">
  5. <input name="title" class="input is-shadowless is-radiusless" autofocus="autofocus" type="text" placeholder="输入文章标题...">
  6. <div class="right-box">
  7. <button type="submit" class="button is-small is-primary">发布</button>
  8. </div>
  9. </header>
  10. <div id="editor">
  11. <textarea name="content" class="input" name="content"></textarea>
  12. <div class="show content markdown-body"></div>
  13. </div>
  14. </form>
  15. <nav class="navbar has-shadow">
  16. <div class="navbar-brand">
  17. <a class="navbar-item" href="/">
  18. JS之禅
  19. </a>
  20. </div>
  21. </nav>
  22. {% block script %}
  23. <script src="https://cdn.bootcss.com/marked/0.3.19/marked.min.js"></script>
  24. <script>
  25. var input = $('#editor .input')
  26. $('#editor .show').html(marked(input.val()))
  27. input.on('input', function() {
  28. $('#editor .show').html(marked($(this).val()))
  29. })
  30. </script>
  31. {% endblock %}
  32. {% endblock %}

这儿我们实现了一个最简陋的Markdown编辑器(函数去抖都懒得加)

Markdown: Basics (快速入门)

新建控制器routes/posts.js,并把create方法挂到路由

  1. module.exports = {
  2. async create (ctx, next) {
  3. await ctx.render('create', {
  4. title: '新建文章'
  5. })
  6. }
  7. }

访问http://localhost:3000/posts/new 试试。

接下来,我们在routes/posts.js 引入文章Model

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

修改create 方法,在GET时显示页面,POST时接收表单数据并操作数据库

  1. ...
  2. async create (ctx, next) {
  3. if (ctx.method === 'GET') {
  4. await ctx.render('create', {
  5. title: '新建文章'
  6. })
  7. return
  8. }
  9. const post = Object.assign(ctx.request.body, {
  10. author: ctx.session.user._id
  11. })
  12. const res = await PostModel.create(post)
  13. ctx.flash = { success: '发表文章成功' }
  14. ctx.redirect(`/posts/${res._id}`)
  15. }
  16. ...

发表一篇文章试试!到数据库看看刚刚新建的这条数据。注意:这儿我们并没有做任何校验

文章列表与详情

上面,在发表文章后将跳转到文章详情页,但是先什么都没有,现在就来实现它,在posts.js 新建show方法用来显示文章

  1. async show (ctx, next) {
  2. const post = await PostModel.findById(ctx.params.id)
  3. .populate({ path: 'author', select: 'name' })
  4. await ctx.render('post', {
  5. title: post.title,
  6. post,
  7. comments
  8. })
  9. }

这儿用到了populate 方法,MongoDB是非关联数据库,它没有关系型数据库joins特性,但是有时候我们还是想引用其它的文档,为了决这个问题,Mongoose封装了一个Population功能。使用Population可以实现在一个 document 中填充其他 collection(s)document(s)

文章详情模板

  1. {% extends 'views/base.html' %}
  2. {% block body %}
  3. {% include "views/components/header.html" %}
  4. <div class="container margin-top">
  5. <div class="columns">
  6. <div class="column is-8 is-offset-2">
  7. <div class="box markdown-body">
  8. <h1>{{post.title}}</h1>
  9. <div>
  10. 作者:<a href="/user/{{post.author._id}}">{{post.author.name}}</a>
  11. {% if post.author.toString() == ctx.session.user._id %}
  12. <div class="is-pulled-right">
  13. <a class="button is-small is-primary" href="/posts/{{post._id}}/edit">编辑</a>
  14. <a class="button is-small is-danger" href="/posts/{{post._id}}/delete">删除</a>
  15. </div>
  16. {% endif %}
  17. </div>
  18. {{marked(post.content) | safe}}
  19. </div>
  20. </div>
  21. </div>
  22. </div>
  23. {% endblock %}

在模板里我们用到marked,我们需要将marked挂到ctx.state上

  1. ...
  2. const marked = require('marked')
  3. ...
  4. marked.setOptions({
  5. renderer: new marked.Renderer(),
  6. gfm: true,
  7. tables: true,
  8. breaks: false,
  9. pedantic: false,
  10. sanitize: false,
  11. smartLists: true,
  12. smartypants: false
  13. })
  14. ...
  15. app.use(async (ctx, next) => {
  16. ctx.state.ctx = ctx
  17. ctx.state.marked = marked
  18. await next()
  19. })
  20. ...

接下来实现文章列表页

  1. const PostModel = require('../models/post')
  2. module.exports = {
  3. async index (ctx, next) {
  4. const posts = await PostModel.find({})
  5. await ctx.render('index', {
  6. title: 'JS之禅',
  7. desc: '欢迎关注公众号 JavaScript之禅',
  8. posts
  9. })
  10. }
  11. }

修改我们的主页模板

  1. {% extends 'views/base.html' %}
  2. {% block body %}
  3. {% include "views/components/header.html" %}
  4. <section class="hero is-primary is-bold">
  5. <div class="hero-body">
  6. <div class="container">
  7. <h1 class="title">
  8. JS之禅
  9. </h1>
  10. <h2 class="subtitle">
  11. 起于JS,而不止于JS
  12. </h2>
  13. </div>
  14. </div>
  15. </section>
  16. <div class="container margin-top">
  17. <div class="columns">
  18. <div class="column is-8 is-offset-2">
  19. {% for post in posts %}
  20. <div class="card">
  21. <div class="card-content">
  22. <div class="content">
  23. <a href="/posts/{{post._id}}">{{post.title}}</a>
  24. </div>
  25. </div>
  26. </div>
  27. {% endfor %}
  28. </div>
  29. </div>
  30. </div>
  31. {% endblock %}

现在访问下http://localhost:3000 你将看到文章列表,点击文章将打开文章详情页

indexshow

文章编辑与删除

现在来实现文章的编辑修改,在posts.js 新建edit方法

  1. async edit (ctx, next) {
  2. if (ctx.method === 'GET') {
  3. const post = await PostModel.findById(ctx.params.id)
  4. if (!post) {
  5. throw new Error('文章不存在')
  6. }
  7. if (post.author.toString() !== ctx.session.user._id.toString()) {
  8. throw new Error('没有权限')
  9. }
  10. await ctx.render('edit', {
  11. title: '更新文章',
  12. post
  13. })
  14. return
  15. }
  16. const { title, content } = ctx.request.body
  17. await PostModel.findByIdAndUpdate(ctx.params.id, {
  18. title,
  19. content
  20. })
  21. ctx.flash = { success: '更新文章成功' }
  22. ctx.redirect(`/posts/${ctx.params.id}`)
  23. }

edit.htmlcreate.html 基本一致。不过有了文章的数据

  1. {% extends 'views/base.html' %}
  2. {% block body %}
  3. <form action="/posts/{{post._id}}/edit" method="POST">
  4. <header class="editor-header">
  5. <input name="title" value={{post.title}} class="input is-shadowless is-radiusless" type="text" placeholder="输入文章标题...">
  6. <div class="right-box">
  7. <button type="submit" class="button is-small is-primary">更新</button>
  8. </div>
  9. </header>
  10. <div id="editor">
  11. <textarea autofocus="autofocus" name="content" class="input" name="content">{{post.content}}</textarea>
  12. <div class="show content markdown-body"></div>
  13. </div>
  14. </form>
  15. <nav class="navbar has-shadow">
  16. <div class="navbar-brand">
  17. <a class="navbar-item" href="/">
  18. JS之禅
  19. </a>
  20. </div>
  21. </nav>
  22. {% block script %}
  23. <script src="https://cdn.bootcss.com/marked/0.3.19/marked.min.js"></script>
  24. <script>
  25. var input = $('#editor .input')
  26. $('#editor .show').html(marked(input.val()))
  27. input.on('input', function() {
  28. $('#editor .show').html(marked($(this).val()))
  29. })
  30. </script>
  31. {% endblock %}
  32. {% endblock %}

删除功能很简单,找到文章、判断用户是否有权限删除,然后删除即可

  1. // routes/posts.js
  2. async destroy (ctx, next) {
  3. const post = await PostModel.findById(ctx.params.id)
  4. if (!post) {
  5. throw new Error('文章不存在')
  6. }
  7. console.log(post.author, ctx.session.user._id)
  8. if (post.author.toString() !== ctx.session.user._id.toString()) {
  9. throw new Error('没有权限')
  10. }
  11. await PostModel.findByIdAndRemove(ctx.params.id)
  12. ctx.flash = { success: '删除文章成功' }
  13. ctx.redirect('/')
  14. }

动手试试,并思考思考还有那些问题?