“真实世界”全栈开发-3.8-博文的增查改删

2018-02-10  本文已影响26人  桥头堡2015

对数据做增查改删(CRUD)是交互式网络应用的核心功能。对于用户,我们只提供了前三项操作;而对博文,四项操作我们的应用都要提供。

创建博文的模型

如本系列第二部分所说的,博文应该有如下数据:

我们先来为上面的数据新建Mongoose模式,并将其注册为可用的模型。新建models/Article.js文件,写入:

const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const slug = require('slug');

const ArticleSchema = new mongoose.Schema({
  slug: {type: String, lowercase: true, unique: true},
  title: String,
  description: String,
  body: String,
  favoritesCount: {type: Number, default: 0},
  tagList: [{type: String}],
  author: {type: mongoose.Schema.Types.ObjectId, ref: 'User'}
}, {timestamps: true});

ArticleSchema.plugin(uniqueValidator, {message: 'is already taken'});

mongoose.model('Article', ArticleSchema);

如同用户模式里一样,我们这里用到了mongoose-unique-validator来验证slug是唯一的。

上面的代码已经导入了slug包。接下来,我们要定义一个新方法,使用这个包来生成博文slug。为了保证slug的唯一性,我们会在原标题生成的字符串后再加上六个随机字符。

ArticleSchema.plugin(uniqueValidator, {message: 'is already taken'});

// +++
ArticleSchema.methods.slugify = function() {
  this.slug = slug(this.title) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36);
};
// +++

mongoose.model('Article', ArticleSchema);

什么时候调用这个方法呢?每当一篇新博文创建后或者是一篇已有博文的标题发生改变后,往数据库保存的时候,我们需要调用这个方法生成新的slug。

我们可以借助Mongoose中间件来在上述的场合自动调用slugify方法。

models/Article.js加入以下代码:

// +++
ArticleSchema.pre('validate', function (next) {
  if (!this.slug || this.isModified('title'))
    this.slugify();
  return next();
});
// +++

mongoose.model('Article', ArticleSchema);

slug的生成应该发生在Mongoose检验数据之前,否则没有slug字段,检验必然失败。这也是上面代码中pre'validate'表达的意思。

另外注意,这里回调函数中的this指向的是当前的博文对象;因此,定义Mongoose中间件时也不能用箭头函数。

最后,我们还需要添加一个方法,返回博文的JSON对象。不要忘了加入Mongoose自动创建的createAtupdateAt这两条数据。

// +++
ArticleSchema.methods.toJSONFor = function (user) {
  return {
    slug: this.slug,
    title: this.title,
    description: this.description,
    body: this.body,
    createdAt: this.createdAt,
    updatedAt: this.updatedAt,
    tagList: this.tagList,
    favoritesCount: this.favoritesCount,
    author: this.author.toProfileJSONFor(user)
  };
};
// +++

ArticleSchema.pre('validate', ...);

值得指出的是,author一行,我们用到了之前为用户模型定义的toProfileJSONFor(user)方法。

最后,我们需要在后端应用中运行上面的脚本,否则之后的中间件无法使用Article模型。

app.js文件里加入一行代码:

require('./models/User');
// +++
require('./models/Article');
// +++
require('./config/passport');

到此博文模型就创建完成了。我们接下来要做的就是添加增查改删博文的路由及相应的中间件。

为博文操作新建路由对象

跟之前的步骤一样,我们首先要为博文的所有操作(其跟URL都将是/api/articles)新建一个Router对象。

新建文件/routes/api/articles.js,写入:

const router = require('express').Router();
const passport = require('passport');
const mongoose = require('mongoose');
const Article = mongoose.model('Article');
const User = mongoose.model('User');
const auth = require('../auth');

module.exports = router;

同样地,这个路由需要注册到API的主路由上。打开routes/api/index.js,加入:

router.use('/profiles', require('./profiles'));
// +++
router.use('/articles', require('./articles'));
// +++

路由对象建好了,也注册到主路由上了,接下来就该实现具体的中间件了。

新建博文

新建博文的端点为POST /api/articles,而且只有登录的用户才能执行,所以需要身份验证。

routes/api/articles.js加入如下代码:

// +++
const loadCurrentUser = require('./user').loadCurrentUser;

const respondArticle = (req, res, next) => {
  return res.json({article: res.locals.article.otJSONFor(res.locals.res)});
}

router.post('/', auth.required, loadCurrentUser, (req, res, next) => {
  const article = new Article(req.body.article);
  article.author = res.locals.user;
  return article.save().then(() => {
    res.locals.article = article;
    return next();
  }).catch(next);
}, respondArticle);
// +++

module.exports = router;

从URL中获取博文slug

博文的查、改、删操作,都需要从数据库中通过其slug读取该博文的数据。如同上一讲获取用户名一样,我们可以在routes/api/articles.js中用router.param为路由定义一个获取博文slug的参数中间件,如下:

// +++
router.param('slug', (req, res, next, slug) => {
  Article.findOne({slug})
    //.populate('author')
    .then(article => {
      if (!article)
        return res.status(404).json({errors: {slug: `no such slug: ${slug}`}});
      res.locals.article = article;
      return next();
    })
    .catch(next);
});
// +++

router.post('/', auth.required, loadCurrentUser, ...

每当该路由遇到的URL中有和:slug对应的部分时,Express就会调用上述中间件,把截取到的数据传给第四个参数slug,然后从数据库中读取相应的博文,存给res.locals.article或者返回404。

查阅博文

端点为GET /api/articles/:slug,身份验证可有可无。

routes/api/articles.js中写入:

// +++
router.get('/:slug', auth.optional, loadCurrentUser, (req, res, next) => {
  res.locals.article.populate('author')
    .execPopulate()
    .then(() => next())
    .catch(next);
}, respondArticle);
// +++

Mongoose提供的populate()方法,允许一个文档(这里是个Article)读取关联的另一个文档(这里属于User)。

更改博文

端点是PUT /api/articles/:slug,身份验证是必需的,请求体的数据会覆盖相应字段。另外,我们还需确保当前登录的用户必须是该博文的作者,否则返回403

routes/api/articles.js中写入:

// +++
const checkAuthor = (req, res, next) => {
  if (!res.locals.user.equals(res.locals.article.author))
    return res.status(403).json({errors: {user: 'not the author'}});
  return next();
};

router.put('/:slug', auth.required, loadCurrentUser, checkAuthor,
  (req, res, next) => {
    ['title', 'description', 'body'].forEach(propName => {
      if (!req.body.article.hasOwnProperty(propName)) return;
      res.locals.article[propName] = req.body.article[propName];
    });
    res.locals.article.save()
      .then(() => next())
      .catch(next);
  }, respondArticle);
// +++

module.exports = router;

删除博文

端点为DELETE /api/articles/:slug,需要身份验证,不需要请求体。同上,我们需要检查当前用户是否该博文的作者。如果删除成功,我们返回状态码为204且无响应体的响应。

routes/api/articles.js中写入:

// +++
router.delete('/:slug', auth.required, loadCurrentUser, checkAuthor,
  (req, res, next) => {
    res.locals.article.remove()
      .then(() => res.sendStatus(204))
      .catch(next);
  });
// +++

module.exports = router;

到此,博文增查改删就全部实现了。

上一篇下一篇

猜你喜欢

热点阅读