我爱编程

“真实世界”全栈开发-3.13-博文列表

2018-02-12  本文已影响16人  桥头堡2015

我们的后端还差最后一类功能:返回博文列表。登录用户的个人主页按时间倒序显示所有关注用户新发表;另外,后端还提供一个可接受查询参数的端点,用户可以获取某一标签下、或者某个特定作者、或者某个用户点过赞的所有博文。我们先来实现后者。

接受查询参数的端点

具体端点为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
}

这个端点可以接受如下的查询参数:

我们分三步完成这个任务。第一步,先实现limitoffset并返回正确的列表长度。

获取列表以及列表的长度,在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}

第三步,实现authorfavoritedBy。与上面不同,我们首先需要预处理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端点只接受limitskip两个参数并且需要身份验证。

我们首先重构上一节新加的代码,把上面创建用于查询语句中的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的区别。

下一步

到此为止,整个后端就已经全部搭建好了。从下一讲开始,我们来实现应用的前端。

上一篇 下一篇

猜你喜欢

热点阅读