对数据做增查改删(CRUD)是交互式网络应用的核心功能。对于用户,我们只提供了前三项操作;而对博文,四项操作我们的应用都要提供。
创建博文的模型
如本系列第二部分所说的,博文应该有如下数据:
-
slug
:为后端为每篇博文自动生成的独一字符串,用于数据库里的查询。如果你对这个概念不熟悉,请阅读这一维基百科页面 -
title
:标题 -
body
:我们的博客平台采用Markdown编辑器,所以body
里存储的是本篇博文的Markdown文本 -
description
:对本篇博文的一小段介绍 -
favoritesCount
:获得的点赞数 -
tagList
:标签列表 -
author
:作者,为其公共信息对象
我们先来为上面的数据新建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自动创建的createAt
和updateAt
这两条数据。
// +++
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;
到此,博文增查改删就全部实现了。
网友评论