前篇
使用React、Node.js、MongoDB、Socket.IO開發一個角色投票應用的學習過程(一)
使用React、Node.js、MongoDB、Socket.IO開發一個角色投票應用的學習過程(二)
原文第十三步,Express API路由
第一個路由是用來建立角色的
app.post(`/api/characters`,(req,res,next) => {
let gender = req.body.gender;
let characterName = req.body.name;
let characterIdLookupUrl = `https://api.eveonline.com/eve/CharacterId.xml.aspx?names=` + characterName;
const parser = new xml2js.Parser();
async.waterfall([
function(callback) {
request.get(characterIdLookupUrl,(err,request,xml) => {
if(err) return next(err);
parser.parseString(xml,(err,parsedXml) => {
try {
let characterId = parsedXml.eveapi.result[0].rowset[0].row[0].$.characterID;
app.models.character.findOne({ characterId: characterId},(err,model) => {
if(err) return next(err);
if(model) {
return res.status(400).send({ message: model.name + ` is alread in the database`});
}
callback(err,characterId);
});
} catch(e) {
return res.status(400).send({ message: ` xml Parse Error`});
}
});
});
},
function(characterId) {
let characterInfoUrl = `https://api.eveonline.com/eve/CharacterInfo.xml.aspx?characterID=` + characterId;
console.log(characterInfoUrl);
request.get({ url: characterInfoUrl },(err,request,xml) => {
if(err) return next(err);
parser.parseString(xml, (err,parsedXml) => {
if (err) return res.send(err);
try{
let name = parsedXml.eveapi.result[0].characterName[0];
let race = parsedXml.eveapi.result[0].race[0];
let bloodline = parsedXml.eveapi.result[0].bloodline[0];
app.models.character.create({
characterId: characterId,
name: name,
race: race,
bloodline: bloodline,
gender: gender
},(err,model) => {
if(err) return next(err);
res.send({ message: characterName + ` has been added successfully!`});
});
} catch (e) {
res.status(404).send({ message: characterName + ` is not a registered citizen of New Eden`,error: e.message });
}
});
});
}
]);
});
是不是看起來和原文的基本一模一樣,只不過把var 變成了let 匿名函式變成了ES6的`=>`箭頭函式,雖然我用的是warterline而原文中用的是mongoose但是包括方法名基本都一樣,所以我感覺waterline是在API上最接近mongoose
順便說一下,我為什麼不喜歡mongodb,僅僅是因為有一次我安裝了,只往裡面寫了幾條測試資料,按文字算最多幾kb,但第二天重啟機器的時候,系統提示我,我的/home分割槽空間不足了(雙系統分割槽分給linux分小了本來就不大),結果一查mongodb 的data檔案 有2G多,我不知道什麼原因,可能是配置不對還是別的什麼原因,反正,當天我就把它刪除了,
完成了這個API我們就可以往資料庫裡新增東西了,不知道哪些使用者名稱可以用?相當簡單,反正我用的全是一名人的名字(英文名),外國人也喜歡搶註名字,嘿嘿嘿
原文第十三步,Home元件
基本保持和原文一樣,只是用lodash 替換了 underscore
一開始我看到網上介紹lodash是可以無縫替換underscore,中要修改引用就可以,但是我用的版本是4.11.2已經有很多方法不一樣了,還去掉了不少方法(沒有去關注underscore是不是也在最新版本中有同樣的改動)
原文中:
......
import {first, without, findWhere} from `underscore`;
......
var loser = first(without(this.state.characters, findWhere(this.state.characters, { characterId: winner }))).characterId;
......
修改為:
......
import {first, filter} from `lodash`;
......
let loser = first(filter(this.state.characters,item => item.characterId != winner )).characterId;
findWhere 在最新版本的lodash中已經不存正,我用了filter來實現相同功能。
第十四步:Express API 路由(2/2)
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([]);
});
});
});
});
可以看到原文中用{ random: { $near: [Math.random(), 0] } }做為查詢條件從而在資料庫裡取出兩條隨機的記錄返回給頁面進行PK,前文說過random的型別在mysql沒有類似的,所以我把這個欄位刪除了。本來mysql,可以用order by rand() 之類的方法但是,waterline的sort(order by rand())不被支援,所以我是把所有符合條件的記錄取出來,能過lodash的sampleSize方法從所有記錄中獲取兩天隨機記錄。
app.get(`/api/characters`, (req,res,next) => {
let choice = [`Female`, `Male`];
let randomGender = _.sample(choice);
//原文中是通過nearby欄位來實現隨機取值,waterline沒有實現mysql order by rand()返回隨機記錄,所以返回所有結果,用lodash來處理
app.models.character.find()
.where({`voted`: false})
.exec((err,characters) => {
if(err) return next(err);
//用lodash來取兩個隨機值
let randomCharacters = _.sampleSize(_.filter(characters,{`gender`: randomGender}),2);
if(randomCharacters.length === 2){
//console.log(randomCharacters);
return res.send(randomCharacters);
}
//換個性別再試試
let oppsiteGender = _.first(_.without(choice, randomGender));
let oppsiteCharacters = _.sampleSize(_.filter(characters,{`gender`: oppsiteGender}),2);
if(oppsiteCharacters === 2) {
return res.send(oppsiteCharacters);
}
//沒有符合條件的character,就更新voted欄位,開始新一輪PK
app.models.character.update({},{`voted`: false}).exec((err,characters) => {
if(err) return next(err);
return res.send([]);
});
});
});
在資料量大的情況下,這個的方法效能上肯定會有問題,好在我們只是學習過程,資料量也不大。將就用一下,能實現相同的功能就可以了。
GET /api/characters/search
這個API之前還有兩個API,和原文基本一樣,所做的修改只是用了ES6的語法,就不浪費篇幅了,可以去我的github看
這一個也只是一點mongoose和waterline的一點點小區別
原文中mongoose的模糊查詢是用正則來做的,mysql好像也可以,但是warterline中沒有找到相關方法(它的文件太簡陋了)
所以原文中
app.get(`/api/characters/search`, function(req, res, next) {
var characterName = new RegExp(req.query.name, `i`);
Character.findOne({ name: characterName }, function(err, character) {
......
我改成了
app.get(`/api/characters/search`, (req,res,next) => {
app.models.character.findOne({name:{`contains`:req.query.name}}, (err,character) => {
.....
通過contains來查詢,其實就是like %sometext%的方法來實現
下面還有兩個方法修改的地方也大同小異,就不仔細講了,看程式碼吧
GET /api/stats
這個是原文最後一個路由了,
原文中用了一串的函式來獲取各種統計資訊,原作者也講了可以優化,哪我們就把它優化一下吧
app.get(`/api/stats`, (req,res,next) => {
let asyncTask = [];
let countColumn = [
{},
{race: `Amarr`},
{race: `Caldari`},
{race: `Gallente`},
{race: `Minmatar`},
{gender: `Male`},
{gender: `Female`}
];
countColumn.forEach(column => {
asyncTask.push( callback => {
app.models.character.count(column,(err,count) => {
callback(err,count);
});
})
});
asyncTask.push(callback =>{
app.models.character.find()
.sum(`wins`)
.then(results => {
callback(null,results[0].wins);
});
} );
asyncTask.push(callback => {
app.models.character.find()
.sort(`wins desc`)
.limit(100)
.select(`race`)
.exec((err,characters) => {
if(err) return next(err);
let raceCount = _.countBy(characters,character => character.race);
console.log(raceCount);
let max = _.max(_.values(raceCount));
console.log(max);
let inverted = _.invert(raceCount);
let topRace = inverted[max];
let topCount = raceCount[topRace];
callback(err,{race: topRace, count: topCount});
});
});
asyncTask.push(callback => {
app.models.character.find()
.sort(`wins desc`)
.limit(100)
.select(`bloodline`)
.exec((err,characters) => {
if(err) return next(err);
let bloodlineCount = _.countBy(characters,character => character.bloodline);
let max = _.max(_.values(bloodlineCount));
let inverted = _.invert(bloodlineCount);
let topBloodline = inverted[max];
let topCount = bloodlineCount[topBloodline];
callback(err,{bloodline: topBloodline, count: topCount});
});
});
async.parallel(asyncTask,(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]
});
})
});
我把要統計資料的欄位放入一個陣列countColumn通過forEach把push到asyncTask,最後兩個統計方法不一樣的函式,單獨push,最後用async.parallel方法執行並獲得結果。
underscore的max方法可以從{a:1,b:6,d:2,e:3}返回最大值,但是lodash新版中的不行,只能通過_.max(_.values(bloodlineCount))這樣的方式返回最大值。