第十四步:Express API 路由(2/2)

第十四步:Express API 路由(2/2)

让我们回到server.js。我希望现在你已经明白下面这些路由该放在哪里——在Express中间件后面和React中间件前面。

注意:请理解我们这里将所有的路由都放在server.js,是为了这个教程的方便。在我工作期间所构建的仪表盘项目里,所有的路由都被拆开分散到不同的文件,并放在routes目录下面,并且,所有的路由处理程序也都被打散,分成不同的文件放到controllers目录下。

让我们以获取Home组件中两个角色的路由作为开始。

GET /api/characters

/**
 * GET /api/characters
 * Returns 2 random characters of the same gender that have not been voted yet.
 */
app.get('/api/characters', function(req, res, next) {
  var choices = ['Female', 'Male'];
  var randomGender = _.sample(choices);

  Character.find({ random: { $near: [Math.random(), 0] } })
    .where('voted', false)
    .where('gender', randomGender)
    .limit(2)
    .exec(function(err, characters) {
      if (err) return next(err);

      if (characters.length === 2) {
        return res.send(characters);
      }

      var oppositeGender = _.first(_.without(choices, randomGender));

      Character
        .find({ random: { $near: [Math.random(), 0] } })
        .where('voted', false)
        .where('gender', oppositeGender)
        .limit(2)
        .exec(function(err, characters) {
          if (err) return next(err);

          if (characters.length === 2) {
            return res.send(characters);
          }

          Character.update({}, { $set: { voted: false } }, { multi: true }, function(err) {
            if (err) return next(err);
            res.send([]);
          });
        });
    });
});

别忘了在最顶部添加Underscore.js模块,因为我们需要使用它的几个函数_.sample()_.first()_.without()

var _ = require('underscore');

我已经尽力让这段代码易于理解,所以你应该很清楚如何获取两个随机角色。它将随机选择Male或Female性别并查询数据库以获取两个角色,如果获得的角色少于2个,它将尝试用另一个性别进行查询。比如,如果我们有10个男性角色但其中9个已经被投票过了,只显示一个角色没有意义。如果无论是男性还是女性角色查询返回都不足两个角色,说明我们已经耗尽了所有未投票的角色,应该重置投票计数,通过设置所有角色的voted:false即可办到。

PUT /api/characters

这个路由和前一个相关,它会分别更新获胜的wins字段和失败角色的losses字段。

/**
 * PUT /api/characters
 * Update winning and losing count for both characters.
 */
app.put('/api/characters', function(req, res, next) {
  var winner = req.body.winner;
  var loser = req.body.loser;

  if (!winner || !loser) {
    return res.status(400).send({ message: 'Voting requires two characters.' });
  }

  if (winner === loser) {
    return res.status(400).send({ message: 'Cannot vote for and against the same character.' });
  }

  async.parallel([
      function(callback) {
        Character.findOne({ characterId: winner }, function(err, winner) {
          callback(err, winner);
        });
      },
      function(callback) {
        Character.findOne({ characterId: loser }, function(err, loser) {
          callback(err, loser);
        });
      }
    ],
    function(err, results) {
      if (err) return next(err);

      var winner = results[0];
      var loser = results[1];

      if (!winner || !loser) {
        return res.status(404).send({ message: 'One of the characters no longer exists.' });
      }

      if (winner.voted || loser.voted) {
        return res.status(200).end();
      }

      async.parallel([
        function(callback) {
          winner.wins++;
          winner.voted = true;
          winner.random = [Math.random(), 0];
          winner.save(function(err) {
            callback(err);
          });
        },
        function(callback) {
          loser.losses++;
          loser.voted = true;
          loser.random = [Math.random(), 0];
          loser.save(function(err) {
            callback(err);
          });
        }
      ], function(err) {
        if (err) return next(err);
        res.status(200).end();
      });
    });
});

这里我们使用async.parallel来同时进行两个数据库查询,因为这两个查询并不相互依赖。不过,因为我们有两个独立的MongoDB文档,还要进行两个独立的异步操作,因此我们还需要另一个async.parallel。一般来说,我们仅在两个角色都完成更新并没有错误后给出一个success的响应。

GET /api/characters/count

MOngoDB有一个内建的count()方法,可以返回所匹配的查询结果的数量。

/**
 * GET /api/characters/count
 * Returns the total number of characters.
 */
app.get('/api/characters/count', function(req, res, next) {
  Character.count({}, function(err, count) {
    if (err) return next(err);
    res.send({ count: count });
  });
});

注意:从这个返回总数量的一次性路由上,你可能注意到我们开始与RESTful API设计模式背道而驰。很不幸这就是现实。我还没有在一个能完美实现RESTful API的项目中工作过,你可以参看Apigee写的这篇文章来进一步了解为什么会这样。

GET /api/characters/search

我上次检查时MongoDB还不支持大小写不敏感的查询,所以这里我们需要使用正则表达式,不过还好MongoDB提供了$regex操作符。

/**
 * GET /api/characters/search
 * Looks up a character by name. (case-insensitive)
 */
app.get('/api/characters/search', function(req, res, next) {
  var characterName = new RegExp(req.query.name, 'i');

  Character.findOne({ name: characterName }, function(err, character) {
    if (err) return next(err);

    if (!character) {
      return res.status(404).send({ message: 'Character not found.' });
    }

    res.send(character);
  });
});

GET /api/characters/:id

这个路由是供角色资料页面使用的(我们将在下一节创建角色组件),教程最开始的图片就是这个页面。

/**
 * GET /api/characters/:id
 * Returns detailed character information.
 */
app.get('/api/characters/:id', function(req, res, next) {
  var id = req.params.id;

  Character.findOne({ characterId: id }, function(err, character) {
    if (err) return next(err);

    if (!character) {
      return res.status(404).send({ message: 'Character not found.' });
    }

    res.send(character);
  });
});

当我开始构建这个项目时,我大概有7-9个几乎相同的路由来检索Top 100的角色。在经过一些代码重构后我仅留下了下面这一个:

/**
 * GET /api/characters/top
 * Return 100 highest ranked characters. Filter by gender, race and bloodline.
 */
app.get('/api/characters/top', function(req, res, next) {
  var params = req.query;
  var conditions = {};

  _.each(params, function(value, key) {
    conditions[key] = new RegExp('^' + value + '$', 'i');
  });

  Character
    .find(conditions)
    .sort('-wins') // Sort in descending order (highest wins on top)
    .limit(100)
    .exec(function(err, characters) {
      if (err) return next(err);

      // Sort by winning percentage
      characters.sort(function(a, b) {
        if (a.wins / (a.wins + a.losses) < b.wins / (b.wins + b.losses)) { return 1; }         if (a.wins / (a.wins + a.losses) > b.wins / (b.wins + b.losses)) { return -1; }
        return 0;
      });

      res.send(characters);
    });
});

比如,如果我们对男性、种族为Caldari、血统为Civire的Top 100角色感兴趣,你可以构造这样的URL路径:

GET /api/characters/top?race=caldari&bloodline=civire&gender=male

如果你还不清楚如何构造conditions对象,这段经过注释的代码应该可以解释:

// Query params object
req.query = {
  race: 'caldari',
  bloodline: 'civire',
  gender: 'male'
};

var params = req.query;
var conditions = {};

// This each loop is equivalent...
_.each(params, function(value, key) {
  conditions[key] = new RegExp('^' + value + '$', 'i');
});

// To this code
conditions.race = new RegExp('^' + params.race + '$', 'i'); // /caldari$/i
conditions.bloodline = new RegExp('^' + params.bloodline + '$', 'i'); // /civire$/i
conditions.gender = new RegExp('^' + params.gender + '$', 'i'); // /male$/i

// Which ultimately becomes this...
Character
    .find({ race: /caldari$/i, bloodline: /civire$/i, gender: /male$/i })

在我们取回获胜数最多的角色后,我们会对胜率进行一个排序,不让最老的角色始终显示在前面。

GET /api/characters/shame

和前一个路由差不多,这个路由会取回失败最多的100个角色:

/**
 * GET /api/characters/shame
 * Returns 100 lowest ranked characters.
 */
app.get('/api/characters/shame', function(req, res, next) {
  Character
    .find()
    .sort('-losses')
    .limit(100)
    .exec(function(err, characters) {
      if (err) return next(err);
      res.send(characters);
    });
});

POST /api/report

有些角色没有一个有效的avatar(一般是灰色轮廓),另有些角色的avatar是漆黑一片,它们在一开始就不应该添加到数据库中。但因为任何人都能添加任何角色,因此有些时候你需要从数据库移除一些异常角色。这里设置当一个角色被访问者举报4次后将被删除。

/**
 * POST /api/report
 * Reports a character. Character is removed after 4 reports.
 */
app.post('/api/report', function(req, res, next) {
  var characterId = req.body.characterId;

  Character.findOne({ characterId: characterId }, function(err, character) {
    if (err) return next(err);

    if (!character) {
      return res.status(404).send({ message: 'Character not found.' });
    }

    character.reports++;

    if (character.reports > 4) {
      character.remove();
      return res.send({ message: character.name + ' has been deleted.' });
    }

    character.save(function(err) {
      if (err) return next(err);
      res.send({ message: character.name + ' has been reported.' });
    });
  });
});

GET /api/stats

最后,为角色的统计创建一个路由。是的,下面的代码可以用async.eachpromises来简化,不过记住,我在两年前开始创建New Eden Faces时对这些方案还不熟悉,到现在绝大部分的后端代码没怎么动过。不过即使这样,这些代码还是足够鲁棒,最少它很明确并且易读。

/**
 * GET /api/stats
 * Returns characters statistics.
 */
app.get('/api/stats', function(req, res, next) {
  async.parallel([
      function(callback) {
        Character.count({}, function(err, count) {
          callback(err, count);
        });
      },
      function(callback) {
        Character.count({ race: 'Amarr' }, function(err, amarrCount) {
          callback(err, amarrCount);
        });
      },
      function(callback) {
        Character.count({ race: 'Caldari' }, function(err, caldariCount) {
          callback(err, caldariCount);
        });
      },
      function(callback) {
        Character.count({ race: 'Gallente' }, function(err, gallenteCount) {
          callback(err, gallenteCount);
        });
      },
      function(callback) {
        Character.count({ race: 'Minmatar' }, function(err, minmatarCount) {
          callback(err, minmatarCount);
        });
      },
      function(callback) {
        Character.count({ gender: 'Male' }, function(err, maleCount) {
          callback(err, maleCount);
        });
      },
      function(callback) {
        Character.count({ gender: 'Female' }, function(err, femaleCount) {
          callback(err, femaleCount);
        });
      },
      function(callback) {
        Character.aggregate({ $group: { _id: null, total: { $sum: '$wins' } } }, function(err, totalVotes) {
            var total = totalVotes.length ? totalVotes[0].total : 0;
            callback(err, total);
          }
        );
      },
      function(callback) {
        Character
          .find()
          .sort('-wins')
          .limit(100)
          .select('race')
          .exec(function(err, characters) {
            if (err) return next(err);

            var raceCount = _.countBy(characters, function(character) { return character.race; });
            var max = _.max(raceCount, function(race) { return race });
            var inverted = _.invert(raceCount);
            var topRace = inverted[max];
            var topCount = raceCount[topRace];

            callback(err, { race: topRace, count: topCount });
          });
      },
      function(callback) {
        Character
          .find()
          .sort('-wins')
          .limit(100)
          .select('bloodline')
          .exec(function(err, characters) {
            if (err) return next(err);

            var bloodlineCount = _.countBy(characters, function(character) { return character.bloodline; });
            var max = _.max(bloodlineCount, function(bloodline) { return bloodline });
            var inverted = _.invert(bloodlineCount);
            var topBloodline = inverted[max];
            var topCount = bloodlineCount[topBloodline];

            callback(err, { bloodline: topBloodline, count: topCount });
          });
      }
    ],
    function(err, results) {
      if (err) return next(err);

      res.send({
        totalCount: results[0],
        amarrCount: results[1],
        caldariCount: results[2],
        gallenteCount: results[3],
        minmatarCount: results[4],
        maleCount: results[5],
        femaleCount: results[6],
        totalVotes: results[7],
        leadingRace: results[8],
        leadingBloodline: results[9]
      });
    });
});

最后使用aggregate()方法的操作比较令人费解。必须承认,到这一步我也曾去寻求过帮助。在MongoDB里,聚合(aggregation)操作处理数据记录并且返回计算后的结果。在这里它通过将所有wins数量相加,来计算所有投票的总数。因为投票是一个零和游戏,获胜总数总是和失败总数相同,所以我们同样也可以使用losses数量来计算。

项目到这里基本就完成了。在教程的最后我还将给项目添加更多特性,给它稍稍扩展一下。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号

意见反馈
返回顶部