讓我們回到server.js。我希望現(xiàn)在你已經(jīng)明白下面這些路由該放在哪里——在Express中間件后面和React中間件前面。
注意:請(qǐng)理解我們這里將所有的路由都放在server.js,是為了這個(gè)教程的方便。在我工作期間所構(gòu)建的儀表盤項(xiàng)目里,所有的路由都被拆開(kāi)分散到不同的文件,并放在routes目錄下面,并且,所有的路由處理程序也都被打散,分成不同的文件放到controllers目錄下。
讓我們以獲取Home組件中兩個(gè)角色的路由作為開(kāi)始。
/**
* 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模塊,因?yàn)槲覀冃枰褂盟膸讉€(gè)函數(shù)_.sample()
、_.first()
和_.without()
。
var _ = require('underscore');
我已經(jīng)盡力讓這段代碼易于理解,所以你應(yīng)該很清楚如何獲取兩個(gè)隨機(jī)角色。它將隨機(jī)選擇Male或Female性別并查詢數(shù)據(jù)庫(kù)以獲取兩個(gè)角色,如果獲得的角色少于2個(gè),它將嘗試用另一個(gè)性別進(jìn)行查詢。比如,如果我們有10個(gè)男性角色但其中9個(gè)已經(jīng)被投票過(guò)了,只顯示一個(gè)角色沒(méi)有意義。如果無(wú)論是男性還是女性角色查詢返回都不足兩個(gè)角色,說(shuō)明我們已經(jīng)耗盡了所有未投票的角色,應(yīng)該重置投票計(jì)數(shù),通過(guò)設(shè)置所有角色的voted:false
即可辦到。
這個(gè)路由和前一個(gè)相關(guān),它會(huì)分別更新獲勝的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
來(lái)同時(shí)進(jìn)行兩個(gè)數(shù)據(jù)庫(kù)查詢,因?yàn)檫@兩個(gè)查詢并不相互依賴。不過(guò),因?yàn)槲覀冇袃蓚€(gè)獨(dú)立的MongoDB文檔,還要進(jìn)行兩個(gè)獨(dú)立的異步操作,因此我們還需要另一個(gè)async.parallel
。一般來(lái)說(shuō),我們僅在兩個(gè)角色都完成更新并沒(méi)有錯(cuò)誤后給出一個(gè)success的響應(yīng)。
MOngoDB有一個(gè)內(nèi)建的count()
方法,可以返回所匹配的查詢結(jié)果的數(shù)量。
/**
* 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 });
});
});
注意:從這個(gè)返回總數(shù)量的一次性路由上,你可能注意到我們開(kāi)始與RESTful API設(shè)計(jì)模式背道而馳。很不幸這就是現(xiàn)實(shí)。我還沒(méi)有在一個(gè)能完美實(shí)現(xiàn)RESTful API的項(xiàng)目中工作過(guò),你可以參看Apigee寫的這篇文章來(lái)進(jìn)一步了解為什么會(huì)這樣。
我上次檢查時(shí)MongoDB還不支持大小寫不敏感的查詢,所以這里我們需要使用正則表達(dá)式,不過(guò)還好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);
});
});
這個(gè)路由是供角色資料頁(yè)面使用的(我們將在下一節(jié)創(chuàng)建角色組件),教程最開(kāi)始的圖片就是這個(gè)頁(yè)面。
/**
* 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);
});
});
當(dāng)我開(kāi)始構(gòu)建這個(gè)項(xiàng)目時(shí),我大概有7-9個(gè)幾乎相同的路由來(lái)檢索Top 100的角色。在經(jīng)過(guò)一些代碼重構(gòu)后我僅留下了下面這一個(gè):
/**
* 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);
});
});
比如,如果我們對(duì)男性、種族為Caldari、血統(tǒng)為Civire的Top 100角色感興趣,你可以構(gòu)造這樣的URL路徑:
GET /api/characters/top?race=caldari&bloodline=civire&gender=male
如果你還不清楚如何構(gòu)造conditions
對(duì)象,這段經(jīng)過(guò)注釋的代碼應(yīng)該可以解釋:
// 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 })
在我們?nèi)』孬@勝數(shù)最多的角色后,我們會(huì)對(duì)勝率進(jìn)行一個(gè)排序,不讓最老的角色始終顯示在前面。
和前一個(gè)路由差不多,這個(gè)路由會(huì)取回失敗最多的100個(gè)角色:
/**
* 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);
});
});
有些角色沒(méi)有一個(gè)有效的avatar(一般是灰色輪廓),另有些角色的avatar是漆黑一片,它們?cè)谝婚_(kāi)始就不應(yīng)該添加到數(shù)據(jù)庫(kù)中。但因?yàn)槿魏稳硕寄芴砑尤魏谓巧?,因此有些時(shí)候你需要從數(shù)據(jù)庫(kù)移除一些異常角色。這里設(shè)置當(dāng)一個(gè)角色被訪問(wèn)者舉報(bào)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.' });
});
});
});
最后,為角色的統(tǒng)計(jì)創(chuàng)建一個(gè)路由。是的,下面的代碼可以用async.each
或promises
來(lái)簡(jiǎn)化,不過(guò)記住,我在兩年前開(kāi)始創(chuàng)建New Eden Faces時(shí)對(duì)這些方案還不熟悉,到現(xiàn)在絕大部分的后端代碼沒(méi)怎么動(dòng)過(guò)。不過(guò)即使這樣,這些代碼還是足夠魯棒,最少它很明確并且易讀。
/**
* 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()
方法的操作比較令人費(fèi)解。必須承認(rèn),到這一步我也曾去尋求過(guò)幫助。在MongoDB里,聚合(aggregation)操作處理數(shù)據(jù)記錄并且返回計(jì)算后的結(jié)果。在這里它通過(guò)將所有wins
數(shù)量相加,來(lái)計(jì)算所有投票的總數(shù)。因?yàn)橥镀笔且粋€(gè)零和游戲,獲勝總數(shù)總是和失敗總數(shù)相同,所以我們同樣也可以使用losses
數(shù)量來(lái)計(jì)算。
項(xiàng)目到這里基本就完成了。在教程的最后我還將給項(xiàng)目添加更多特性,給它稍稍擴(kuò)展一下。
更多建議: