“真实世界”全栈开发-3.13-博文列表
我们的后端还差最后一类功能:返回博文列表。登录用户的个人主页按时间倒序显示所有关注用户新发表;另外,后端还提供一个可接受查询参数的端点,用户可以获取某一标签下、或者某个特定作者、或者某个用户点过赞的所有博文。我们先来实现后者。
接受查询参数的端点
具体端点为GET /api/articles
,身份验证可有可无,响应体中除了有博文JSON对象的列表外,还包括列表的长度,其格式为:
{
"articles": [{
"description": "Ever wonder how?",
"slug": "how-to-train-your-dragon",
"title": "How to train your dragon",
"tagList": ["dragons", "training"],
"createdAt": "2016-02-18T03:22:56.637Z",
"updatedAt": "2016-02-18T03:48:35.824Z",
"favorited": false,
"favoritesCount": 0,
"author": {
"username": "jake",
"bio": "I work at statefarm",
"image": "https://i.stack.imgur.com/xHWG8.jpg",
"following": false
}
}, {
"description": "So toothless",
"slug": "how-to-train-your-dragon-2",
"title": "How to train your dragon 2",
"tagList": ["dragons", "training"],
"createdAt": "2016-02-18T03:22:56.637Z",
"updatedAt": "2016-02-18T03:48:35.824Z",
"favorited": false,
"favoritesCount": 0,
"author": {
"username": "jake",
"bio": "I work at statefarm",
"image": "https://i.stack.imgur.com/xHWG8.jpg",
"following": false
}
}],
"articlesCount": 2
}
这个端点可以接受如下的查询参数:
-
limit
- 响应体中博文对象的最大个数。默认为20
-
offset
- 跳过博文的个数,用于博文列表的分页。默认为0
-
tag
- 如果提供,则仅返回含该标签的博文;唯一可以重复的参数 -
author
- 用来指定作者的用户名 -
favoritedBy
- 用来指定点赞者的用户名
我们分三步完成这个任务。第一步,先实现limit
和offset
并返回正确的列表长度。
获取列表以及列表的长度,在MongoDB(及Mongoose)里是两个不同的query。我们可以用Promise.all()
来处理它们同时成功的情况。
打开routes/api/articles.js
,加入以下代码:
router.get('/', auth.optional, loadCurrentUser,
(req, res, next) => {
const conditions = {};
const options = {limit: 20, skip: 0};
['limit', 'skip'].forEach(opt => {
if (req.query.hasOwnProperty(opt))
options[opt] = Number(req.query[opt]);
});
const query = Article.find(conditions, null, options)
.sort({createdAt: 'desc'})
.populate('author');
return Promise.all([
query.exec(),
Article.count(query).exec(),
]).then(([articles, articlesCount]) => {
return res.json({
articles: articles.map(a => a.toJSONFor(res.locals.user)),
articlesCount,
});
}).catch(next);
});
第二步,实现返回某个标签下博文列表的功能。这一步非常简单,只需要加入如下的用到MongoDB的$in
操作符的代码:
router.get('/', auth.optional, loadCurrentUser,
(req, res, next) => {
// ...
// +++
if (req.query.hasOwnProperty('tag')) {
const tags = Array.isArray(req.query.tag) ? req.query.tag : [req.query.tag];
conditions.tagList = {'$in': tags};
}
// +++
const query = // ...
当tag
出现一次以上时(例如/api/articles?tag=Node&tag=React
),req.query.tag
将是一个数组(['Node', 'React']
)。此时,只要博文的标签列表里含有其中的一个标签,就满足{'$in': tags}
。
第三步,实现author
和favoritedBy
。与上面不同,我们首先需要预处理URL中所提供的用户名,因为:1. 对于作者来说,博文模型里存的是作者的ID,而这里提供的是用户名,不能直接用到conditions
里;2. 博文模型里只存储点赞的个数,而没有点赞者的信息,所以也需要用提供的点赞者的用户名来获取对应的用户对象;3. 如果所提供的作者或者点赞者的用户名不存在,后端简单地返回空列表和0
长度。我们仍然使用Promise.all()
来添加这层预处理逻辑。
将上面的代码更改如下:
router.get('/', auth.optional, loadCurrentUser,
(req, res, next) => {
const conditions = {};
const options = {limit: 20, skip: 0};
['limit', 'skip'].forEach(opt => {
if (req.query.hasOwnProperty(opt))
options[opt] = Number(req.query[opt]);
});
if (req.query.hasOwnProperty('tag')) {
const tags = Array.isArray(req.query.tag) ? req.query.tag : [req.query.tag];
conditions.tagList = {'$in': tags};
}
Promise.all([
req.query.author ? User.findOne({username: req.query.author}) : null,
req.query.favoritedBy ? User.findOne({username: req.query.favoritedBy}) : null
]).then(([author, favoriter]) => {
if (author) {
conditions.author = author._id;
} else if (req.query.author) {
conditions.author = {$in: []};
}
if (favoriter) {
conditions._id = {$in: favoriter.favorites};
} else if (req.query.favoritedBy) {
conditions._id = {$in: []};
}
const query = Article.find(conditions, null, options)
.sort({createdAt: 'desc'})
.populate('author');
return Promise.all([query.exec(), Article.count(query).exec()]);
}).then(([articles, articlesCount]) => {
return res.json({
articles: articles.map(a => a.toJSONFor(res.locals.user)),
articlesCount,
});
}).catch(next);
});
用户的Feed端点
用户的Feed端点GET /api/articles/feed
与上面的博文列表端点非常相似:返回相同格式的数据,不过Feed端点只接受limit
和skip
两个参数并且需要身份验证。
我们首先重构上一节新加的代码,把上面创建用于查询语句中的options
的逻辑抽取出来成独立的中间件,如下:
const buildQueryOptions = (req, res, next) => {
const options = {limit: 20, skip: 0};
['limit', 'skip'].forEach(opt => {
if (req.query.hasOwnProperty(opt))
options[opt] = Number(req.query[opt]);
});
res.locals.options = options;
return next();
};
const executeQuery = (req, res, next) => {
const query = Article.find(res.locals.conditions, null, res.locals.options)
.sort({createdAt: 'desc'})
.populate('author');
Promise.all([query.exec(), Article.count(query).exec()])
.then(([articles, articlesCount]) => {
return res.json({
articles: articles.map(a => a.toJSONFor(res.locals.user)),
articlesCount,
});
}).catch(next);
};
router.get('/', auth.optional, loadCurrentUser, buildQueryOptions,
(req, res, next) => {
const conditions = {};
if (req.query.hasOwnProperty('tag')) {
const tags = Array.isArray(req.query.tag) ? req.query.tag : [req.query.tag];
conditions.tagList = {$in: tags};
}
Promise.all([
req.query.author ? User.findOne({username: req.query.author}) : null,
req.query.favoritedBy ? User.findOne({username: req.query.favoritedBy}) : null
]).then(([author, favoriter]) => {
if (author) {
conditions.author = author._id;
} else if (req.query.author) {
conditions.author = {$in: []};
}
if (favoriter) {
conditions._id = {$in: favoriter.favorites};
} else if (req.query.favoritedBy) {
conditions._id = {$in: []};
}
res.locals.conditions = conditions;
return next();
}).catch(next);
}, executeQuery);
继续往routes/api/articles.js
加入以下的代码:
router.get('/feed', auth.required, loadCurrentUser, buildQueryOptions,
(req, res, next) => {
res.locals.conditions = {author: {$in: res.locals.user.following}};
return next();
}, executeQuery);
从上面的代码结构中也可以看出,上一节里查询博文的逻辑和查询Feed的逻辑只是conditions
的区别。
下一步
到此为止,整个后端就已经全部搭建好了。从下一讲开始,我们来实现应用的前端。