使用React、Node.js、MongoDB、Socket.IO開發一個角色投票應用的學習過程(三)

papersnake發表於2016-05-12

這幾篇都是我原來首發在 segmentfault 上的地址:https://segmentfault.com/a/1190000005040834 突然想起來我這個部落格冷落了好多年了,也該更新一下,呵呵

前篇

使用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我們就可以往資料庫裡新增東西了,不知道哪些使用者名稱可以用?相當簡單,反正我用的全是一名人的名字(英文名),外國人也喜歡搶註名字,嘿嘿嘿

add character ui

原文第十三步,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() 之類的方法但是,waterlinesort(order by rand())不被支援,所以我是把所有符合條件的記錄取出來,能過lodashsampleSize方法從所有記錄中獲取兩天隨機記錄。

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

這一個也只是一點mongoosewaterline的一點點小區別
原文中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))這樣的方式返回最大值。

相關文章