NodeJS+Angular+MongodbWeb開發教程

1. 項目簡介

本項目實例完成時限選項卡視圖、天氣服務視圖、可拖動的組件和交互表格數據的過程,這些組件使用不同類型的組件交互。

選項卡視圖允許你輕松地集成隱藏多個選項卡視圖,但仍然為用戶提供瞭用顯而易見的方法來快速訪問視圖的能力。

天氣空間掛接到後端遠程天氣服務,以獲取天氣信息,這使你可以看到如何實現支持前端視圖的後端服務。

可拖動視圖使你可以拖動並重新定位的具有圖像和文本元素的一個基本頁面,它演示瞭超越標準Web元素的組件交互的概念。

表格視圖存在各種控制,可以對結果進行排序、過濾和分頁瀏覽,而無需重新加載頁面,表格視圖顯示瞭表格數據可以如何互動。


2. 用到的庫

express:作為項目的主web服務器

body-parser:為post請求提供JSON正文支持

ejs:用於呈現HTML模板

mongodb:用於訪問MongoDB數據庫

mongoose:用於提供結構化的數據模型

AngularJs庫
 


3. 項目的目錄結構


4. 定義項目模型、創建應用程序服務器和實現路由

4.1 定義項目模型

代碼很簡單,如下:

//word_model.js

var mongoose=require('mongoose');
var Schema=mongoose.Schema;

var WordSchema=new Schema({
    word:{type:String,index:1,required:true,unique:true},
    first:{type:String,index:1},
    last:String,
    size:Number,
    letters:[String],
    stats:{
        vowels:Number,//元音
        consonants:Number,//輔音
    },
    charsets:[{type:String,chars:[String]}]
});

mongoose.model('Word',WordSchema);

4.2 創建應用程序服務器

//rich_ui_server.js

var express=require('express');
var bodyParser=require('body-parser');
var mongoose=require('mongoose');
var bluebird = require('bluebird');

var db=mongoose.connect('mongodb://localhost:27017/words');
// 將mongoose已經不被建議的Promise方法替換為bluebird,
//在weather.controller.js中進行查詢回調使用
mongoose.Promise=bluebird;
require('./models/word_model.js');
require('./models/weather_cache_model.js');

var app=express();
app.engine('.html',require('ejs').__express);
app.set('views',__dirname+'/views');
app.set('view engine','html');

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:false}));

require('./rich_ui_routes')(app);

app.listen(3000);
console.log('App running...');

4.3 實現路由

./rich_ui_routes.js 文件作為Express服務器的一部分配置加載,提供瞭加載選項卡視圖、訪問單詞數據庫、訪問後端天氣服務、獲得必要的靜態文件所需的路由。

/weather路由通過weather_controller.js 中的 getWeather() 處理程序獲取數據,/words路由通過word_controller.js 控制器提供與MongoDB數據庫的交互。

//rich_ui_routes.js

var express=require('express');
module.exports=function(app){
    //加載控制器
    var weather=require('./controllers/weather_controller');
    var words=require('./controllers/words_controller');

    //配置中間件
    app.use('/static',express.static('./static'));
    app.use('/images',express.static('./images'));
    app.use('/lib',express.static('./lib'));

    //配置路由
    //主頁路由
    app.get('/',function(req,res){
        res.render('rich_ui');
    });

    //天氣服務路由
    app.get('/weather',weather.getWeather);

    //單詞word路由
    app.get('/words',words.getWords);
}

5. 實現選項卡視圖和天氣服務視圖

5.1 實現選項卡視圖

(1)定義卡片模板視圖

卡片模板作為窗格中元素的容器,用來支持選項卡視圖中的多個窗格,需要支持定義多個窗格中的元素。
註意,ng-transclude 所在的元素將被新元素轉置

代碼示例如下:

//rich_tabs.html

<p class="tabbable">
    <!-- 一共存在三個選項卡 -->
  <ul class="tabs nav nav-tabs" role='tablist'>
    <li class="tab" role="presentation" ng-repeat="pane in panes" 
        ng-class="{activeTab:pane.selected}"
        ng-click="select(pane)">
        <a href="#weather-tab" role='tab' data-toggle='tab'>{{pane.title}}</a>
    </li>
  </ul>
  <!-- 選項卡的內容,隻有ng-show='selected' 為true才會顯示 -->
  <p class='tabContentDiv'>
    <p class="tabcontent" ng-transclude></p>
  </p>
</p>

(2)實現選項卡視圖

註意,body 中的DOM元素,其實是AngularJS自定義指令的實現,使用瞭駝峰命名法,必須在後臺進行相應的Angular自定義指令,才能實現。

//rich_ui.html


<!doctype html>
<html ng-app="richApp">
<head>
  <title>NMA</title>
  <link rel="stylesheet" type="text/css" href="/static/css/bootstrap.min.css">
  <link rel="stylesheet" type="text/css" 
      href="/static/css/draggable_styles.css" />
  <link rel="stylesheet" type="text/css" 
      href="/static/css/weather_styles.css" />
  <link rel="stylesheet" type="text/css" href="/static/css/table_styles.css" />

  <link rel="stylesheet" type="text/css" href="/static/css/rich_ui_styles.css" />
</head>
<body>
  <rich_tabs>
    <rich_pane title="Weather">
      <p ng-include="'/static/weather.html'"></p>
    </rich_pane>
    <rich_pane title="Draggable">
      <p ng-include="'/static/draggable.html'"></p>
    </rich_pane>
    <rich_pane title="Tables">
      <p ng-include="'/static/tables.html'"></p>
    </rich_pane>
  </rich_tabs>
  <script src="/static/js/angular.min.js"></script>
  <script src="/static/js/rich_ui_app.js"></script>
  <script src="https://code.jquery.com/jquery.min.js"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</script>

</body>
</html>

(3)實現Angular自定義指令

想要讓richTabs 和 richPane 指令模板生效,必須使用自定義指令來支持。

richTabs 指令定義瞭控制器函數,其中包含用來填充選項卡的窗格數組,其中select() 函數把數組中所有窗格的select 值都設置為false ,然後把當前窗格的select 設置為true ,這樣就隱藏瞭除被選擇的一個窗格外所有的窗格。

addPane() 函數從richPane 指令調用,並把窗格添加到richTabs 中的窗格列表,註意templateUrl 指向上面定義的rich_tabs.html 模板。

richPane 定義瞭一個指令,並制定rich_pane.html 文件作為模板源,它定義瞭作用域中的標題(title),使得其顯示在選項卡中。
註意,需要richTabs 才能通過tabsCtrl 從該指令訪問,然後在link函數中,可執行對addPane() 的調用而將面板添加到面板數組。

//rich_ui_app.js - richTabs/richPane

var app = angular.module('richApp', []);
...

//自定義指令
app.directive('richTabs', function() {
  return { restrict: 'E', transclude: true,
    scope: {},
    controller: function($scope) {
      var panes = $scope.panes = [];
      $scope.select = function(pane) {
        angular.forEach(panes, function(pane) {
          pane.selected = false;
        });
        pane.selected = true;
      };
      this.addPane = function(pane) {
        if (panes.length == 0) {
          $scope.select(pane);
        }
        panes.push(pane);
      };
    },
    templateUrl: '/static/rich_tabs.html'
  };
});
app.directive('richPane', function() {
  return { require: '^richTabs', restrict: 'E',
    templateUrl: '/static/rich_pane.html', 
    transclude: true, scope: { title: '@' },
    link: function(scope, element, attrs, tabsCtrl) {
      tabsCtrl.addPane(scope);
    }
  };
});

5.2 實現天氣服務視圖

本實例的天氣視圖部分顯示瞭使用一個後端服務連接到openweathermap.com網站並檢索特定城市的天氣。

如果瀏覽器帶有跨域問題,需要在把遠程數據發送到客戶端之前,對它進行格式化等情況,那麼使用後臺服務器訪問遠程站點可以很有用。

(1)創建後端天氣服務

此服務區導出瞭getWeather() 路由處理程序來處理Express中的/weather 路由,當這段代碼收到請求時,它從req,query.city 提取城市名,並用它執行服務器上的遠程Web請求,parserWeather() 函數處理該請求。

//weather_controller.js

var http=require('http');
var mongoose=require('mongoose');
var WeatherCache=mongoose.model('WeatherCache');


//註意,因為這裡是需要把Mongoose查詢結果放入函數返回值,所以必須使用promise方法,例如bluebird
//否則函數將接收不到值
function getWeatherCache(cityname){
    var getResult;
    //這裡使用瞭bluebird,在rich_ui_server.js 中聲明
    getResult=WeatherCache.findOne({'city.name':cityname})
        .exec()
        .then(function(promiseResult){
            //返回promise對象
            return promiseResult;
        })
        .error(function(error){
            return error;
            // ...
        });
    return getResult;
}
//如果天氣緩存數據過期,則進行清除
function removeWeatherCache(cityname){
    var removeWeather=WeatherCache.remove({'city.name':cityname})
    .exec()
    .then(function(removeResult){
        console.log('removeResult:'+removeResult);
        if(removeResult){
            return true;
        }else{
            return false;
        }
    })
    .error(function(error){
        return false;
    });

    return removeWeather;
}
//將獲取的新數據插入到數據庫
function insertWeatherCache(newWeatherData){
    var newWeatherDataObj=JSON.parse(newWeatherData);
    newWeatherDataObj.city.name=newWeatherDataObj.city.name.replace(/\s/g, "");
    var addResult='';
    // var newWeatherDB=new WeatherCache(newWeatherData);
    var newWeatherDB=new WeatherCache(newWeatherDataObj);
    newWeatherDB.save(function(err,results){
        if(err){
            throw err;
        }else{
            addResult='ok';
        }
    });
    return addResult;
}

//開氏溫度轉攝氏度
function toCelsius(Kelvin){
    return Math.round(Kelvin-273.15);
}
function parseWeather(req,res,weatherResponse){
    var weatherData='';
    var wObj;
    weatherResponse.on('data',function(chunk){
        weatherData+=chunk;
    });
    weatherResponse.on('end',function(){
        if(weatherData){
            wObj=JSON.parse(weatherData);
            insertWeatherCache(weatherData);
        }else{
            wObj={name:'Not Found'}
        }
        res.status(200).json(wObj);
    })
}

exports.getWeather=function(req,res){
    var city=req.query.city;

    //首先根據city查詢數據庫,看看是否有緩存
    var weatherCache;
    //這裡因為是使用瞭promise,所以使用then接收
    getWeatherCache(city)
    .then(function(promiseData){
        weatherCache= promiseData;
        //如果存在,則直接從數據庫獲取,無需請求api
        if(weatherCache){

            var nowTime = new Date();
            var currentTime=nowTime.toString().replace(/.+\d{4}\s(\d+).+/,"$1");
            if((weatherCache.list[0].dt_txt.toString().replace(/.+\s(\d+).+/,"$1")-9)<currentTime){
                res.status(200).json(weatherCache);
                return;
            }else{
                removeWeatherCache(city)
                .then(function(removeResult){
                    if(!removeResult){
                        res.json.status(500).json({mag:'Server error'});
                    }
                })
            }
        }
        var wData='';
        var options = {
            hostname: 'api.openweathermap.org',
            // path: '/data/2.5/forecast/city?id=524901&APPID=YOURKEYID'        //根據城市id查詢
            path: '/data/2.5/forecast/city?q='+city+'&APPID=YOURKEYID'      //根據城市名查詢(如果是中文地名,必須用拼音代替,這裡的YOURKEYID就是你從openweathermap.org申請到的key)
        };
        http.request(options, (response) => {
          response.setEncoding('utf8');

          parseWeather(req,res,response);
        }).end();
    });

};

(2)定義天氣AngularJS控制器

在NodeJs服務器端定義瞭天氣服務和路由之後,就可以實現訪問路由以獲取天氣數據的控制器。

本控制器實現瞭ANgularJS應用程序的Module對象中的weatherController ,作用域包括城市列表和獲取天氣數據時使用的位置或城市名。

getWeather() 函數執行一個$http GET 請求,傳遞來自$scope.location 的城市參數,並設置將被綁定到視圖的$scope.weather 的值。

addCity() 函數使用`locationIn來把新的城市添加到城市數組,如果城市已經存在,那麼就不會添加,位置被設置為新的城市,而getWeather()` 獲取天氣數據。

//rich_ui_app.js - weatherController

//定義天氣控制器
app.controller('weatherController', function($scope, $http) {
  $scope.cities = ['Beijing','London', 'Paris', 'New York', 'Rome'];
  $scope.location = $scope.cities[0];
  $scope.locationIn = '';
  $scope.currentCity='Beijing';
  //開氏溫度轉攝氏度
  function toCelsius(Kelvin){
    return Math.round(Kelvin-273.15);
  }
  $scope.getWeather = function(){
    //去除所有空格
    var param=$scope.location.replace(/\s/g, "");
    //將第一個字母大寫
    param=param.charAt(0).toUpperCase()+param.slice(1);

    $http({url: '/weather', method: "GET", params:{city:param}})
        .then(function successCallback(data){
          var wObj=data.data;
          var wDataAll=[];
          var wData=[];
          var wDataSecond=[];
          var wDataThird=[];
          var wDataFourth=[];
          var wDataFifth=[];

          var wDatai;
          var point;
          var currentHours=new Date().getHours();//獲取當前本地時間中的小時
          if(currentHours-7<0){
            //0:17>2  1:18>1 2:19>1 3:20>1 4:21>0 
            point=Math.ceil((7-currentHours)/3)-1;
          }else{
            point=Math.ceil((24-(currentHours-7))/3)-1;
          } 

          var len=wObj.list.length;

          for(var i=0;i<len;i++){
            wDatai={
             name: wObj.city.name,//城市名
             country:wObj.city.country,//所屬國傢
             temp: toCelsius(wObj.list[i].main.temp),//實時溫度
             tempMin: toCelsius(wObj.list[i].main.temp_min),//當前最低溫
             tempMax: toCelsius(wObj.list[i].main.temp_max),//當前最高溫
             humidity: (wObj.list[i].main.temp_max).toFixed(1),//空氣濕度
             windSpeed: (wObj.list[i].wind.speed*2.23694).toFixed(1),//風速,mph
             windDirection:(wObj.list[i].wind.deg).toFixed(1),//風向
             clouds: wObj.list[i].clouds.all,//雲量
             description: wObj.list[i].weather[0].description,//天氣狀況,晴、多雲、小雨等
             icon: wObj.list[i].weather[0].icon,//天氣圖標
             time: wObj.list[i].dt_txt//預報的時間
            };
            wDataAll.push(wDatai);

          }
          wData=wDataAll.slice(0,point);
          wDataSecond=wDataAll.slice(point,point+8);
          wDataThird=wDataAll.slice(point+8,point+16);
          wDataFourth=wDataAll.slice(point+16,point+24);
          wDataFifth=wDataAll.slice(point+24);

          //規定哪一天的天氣要顯示在頁面上,默認為當前日期
          var showData=[wData,wDataSecond,wDataThird,wDataFourth,wDataFifth];
          $scope.paginations=[
            {'nav':['0',wData[0].time.match(/\b\d{2}-\d+\b/).toString()]},
            {'nav':['1',wDataSecond[0].time.match(/\b\d{2}-\d+\b/).toString()]},
            {'nav':['2',wDataThird[0].time.match(/\b\d{2}-\d+\b/).toString()]},
            {'nav':['3',wDataFourth[0].time.match(/\b\d{2}-\d+\b/).toString()]},
            {'nav':['4',wDataFifth[0].time.match(/\b\d{2}-\d+\b/).toString()]}
          ];

            //改變要顯示的日期天氣
          $scope.changeDate=function(num){
            var numId=parseInt(num);
            $scope.weather=showData[numId]
          }
          $scope.weather = showData[0];
        },function errorCallback(data){
          console.log('weather dataError:'+JSON.stringify(data));
          $scope.weather = data.data;
        });

  };
  $scope.addCity = function(){
    if ($scope.cities.indexOf($scope.locationIn) != 0){
      $scope.cities.push($scope.locationIn);
    }
    $scope.location = $scope.locationIn;
    $scope.currentCity = $scope.locationIn;
    $scope.getWeather();
  };
  $scope.setLocation = function(city){
    $scope.location = city;
    $scope.currentCity = city;
    $scope.getWeather();
  };


  $scope.getWeather('Beijing');

});

(3)定義天氣AngularJS視圖

有瞭控制器,就可以定義天氣數據的視圖。

在視圖的頂部,有個文本框接受新的城市名稱,並將其綁定到locationIn ,還有一個按鈕。它被單擊時調用控制器中的addCity() 。

所有的天氣數據都來自$scope.weather 變量,並使用AngularJS表達式呈現(後續我使用瞭bootstrap佈局)

//weather.html

<p ng-controller="weatherController"><hr>
  <br><label class="weatherInfo">City:</label>
  <input class="weatherInput" type="text" ng-model="locationIn" />
  <input class="weatherButton" type="button" 
         ng-click="addCity()" value="Add City"/><hr>
  <p class="cities">
      <p class="city" ng-repeat="city in cities" 
           ng-click="setLocation(city)">
        {{city}}
      </p>
  </p>
  <p class="weatherData">
    <p class="weatherCity">{{weather.name}}</p>    
    <img 
    ng-src="/uploadfile/2016/1015/20161015092328186.png" />
    <span class="weatherTemp">{{weather.temp}}&deg;F</span>
    <p class="weatherDesc">{{weather.description}}</p>
    <label class="weatherInfo">Clouds:</label>
    <span class="weatherInfo">{{weather.clouds}}%</span>
    <label class="weatherInfo">Humidity:</label>
    <span class="weatherInfo">{{weather.humidity}}%</span>
    <label class="weatherInfo">Wind Speed:</label>
    <span class="weatherInfo">{{weather.wind}} mph</span>
    <label class="weatherInfo">Min Temp:</label>
    <span class="weatherInfo">{{weather.tempMin}} &deg;F</span>
    <label class="weatherInfo">Max Temp:</label>
    <span class="weatherInfo">{{weather.tempMax}} &deg;F</span>
  </p>
</p>

效果如下:

這裡寫圖片描述


6. 實現可拖動的元素

6.1 定義可拖動的自定義AngularJS指令

定義瞭一個richDraggable 自定義指令,該指令存儲在模板編譯時傳入的元素的初始位置,然後把mousedown 事件處理程序註冊到元素上。

當鼠標被按下時,處理程序增加瞭mousemove 和 mouseup 事件, mousemove 處理程序調整top和left CSS屬性,以便使元素在屏幕上移動。
mouseup 事件處理程序解除mousemove 和 mouseup 事件處理程序的綁定,並停止拖動。

//rich_ui_app.js - richDraggable

app.directive('richDraggable', function($document, $window) {
  return function(scope, element, attr) {
    var startX = 0, startY = 0;
    var x = Math.floor((Math.random()*500)+40);
    var y = Math.floor(100+Math.random()*(550-100));
    element.css({ 
      position: 'absolute', 
      cursor: 'pointer',
      top: y + 'px',
      left: x + 'px'
    });
    element.on('mousedown', function(event) {
      event.preventDefault();
      startX = event.pageX - x;
      startY = event.pageY - y;
      $document.on('mousemove', mousemove);
      $document.on('mouseup', mouseup);
    });
    function mousemove(event) {
      y = event.pageY - startY;
      x = event.pageX - startX;
      element.css({
        top: y + 'px',
        left:  x + 'px'
      });
    }
    function mouseup() {
      $document.unbind('mousemove', mousemove);
      $document.unbind('mouseup', mouseup);
    }
  };
});

6.2 在AngularJS視圖中實現可拖動的指令

定義瞭上面的自定義指令後,可以通過在HTML元素定義中包括rich-draggable 指令在你的視圖中實現它,例如:

<p rich-draggable>My draggable paragraph.</p>

<!--draggable.html-->
<!--後續我使用瞭bootsrap佈局-->

<img class="dragImage" rich-draggable src="/images/arch.jpg" />
<img class="dragImage" rich-draggable src="/images/flower.jpg" />
<img class="dragImage" rich-draggable src="/images/lake.jpg" />
<img class="dragImage" rich-draggable src="/images/volcano.jpg" />
<img class="dragImage" rich-draggable src="/images/jump.jpg" />
<img class="dragImage" rich-draggable src="/images/bison.jpg" />
<span class="dragLabel" rich-draggable>Lake</span>
<span class="dragLabel" rich-draggable>Volcano</span>
<span class="dragLabel" rich-draggable>Sunset</span>
<span class="dragLabel" rich-draggable>Bison</span>
<span class="dragLabel" rich-draggable>Flower</span>
<span class="dragLabel" rich-draggable>Arch</span>

效果如下:

這裡寫圖片描述

7. 單詞查詢實現動態數據訪問

若要以便於閱讀的格式顯示大量來自服務器的數據,使用表格是一種不錯的方式。

7.1 創建/word 路由的Express路由控制器

getWords() :該路由處理程序從查詢讀取 limit、skip、sort 和 direction參數,並使用它們來查詢MongoDB數據庫。

getSortObj() :將sort字段和direction轉換為在Query對象的.sort()發放中被使用的sort對象。

//words.controller.js

var mongoose = require('mongoose'),
    Word = mongoose.model('Word');

exports.getWords = function(req, res) {
  var sort = getSortObj(req);//例如:{"size":1} {'word':1}
  var query = Word.find();
  var limit=parseInt(req.query.limit,10);
  var skip=parseInt(req.query.skip,10);
  //判斷是直接查詢所有還是根據值查詢
  if(req.query.contains.length > 0){
    query.find({'word' : new RegExp(req.query.contains, 'i')});
  }

  //註意,這裡有個坑,limit和skip字段必須首先轉成number才能正常查詢
  query.sort(sort)
  .limit(limit)
  .skip(skip)
  .exec(function(err, word) {
    if(err){
      var errs=err;
    }
    if (!word){
      // res.json(404, {msg: 'Word Not Found.'});
      res.status(404).json({msg: errs+',Word Not Found.'+word})
    } else {
      // res.json(word);
      res.status(200).json(word);
    }
  });
};

//判斷排序的依據字段是什麼
function getSortObj(req){
  var field = "word";
  if(req.query.sort == 'Vowels'){
    field = 'stats.vowels';
  } else if(req.query.sort == 'Consonants'){
    field = 'stats.consonants';
  } else if(req.query.sort == 'Length'){
    field = 'size';
  }else{
    field = req.query.sort.toLowerCase();
  }
  var sort = new Object();
  //正序或者逆序查詢
  sort[field] = req.query.direction==='asc'?1:-1;
  return sort;
};

7.2 定義表格AngularJS控制器

完成瞭上面/word 路由,就可以實現訪問列表中顯示的單詞列表的ANgularJS控制器瞭,該控制器實現瞭tableController

前幾行定義瞭words 數組,其中包含表中的數據,以及用在$http GET 請求中來檢索改組單詞的contains limit skip direction 的值。

sortFields 數組提供用來選擇進行排序字段的數據。

getWords() 函數對/words 路由發出一個$http GET 請求,並用請求中的結果來綁定到表中數據的$scope.words 數組,在此過程中,contains limit skip direction 字段隨同請求被發送,以支持排序、分頁和過濾。

find() 方法重新出赤化skip() 值,然後調用getWords() 來執行新的搜索。
next() 和 prev() 調整skip() 值來對單詞數據庫的結果進行分頁。

//rich_ui_app.js - tabelController

//表格單詞數據控制器
//表格單詞數據控制器
app.controller('tableController', function($scope, $http) {
  $scope.words = [];
  $scope.contains = '';
  $scope.limit = 5;
  $scope.skip = 0;
  $scope.skipEnd = 0;
  //選擇通過何種方式查詢
  $scope.sortFields = ['Word', 'First', 'Last', 'Length', 
                       'Vowels', 'Consonants'];

  //默認通過"Word"查詢
  $scope.sortField ="Word";
  //默認正序查詢
  $scope.direction = "asc";
  //請求查詢單詞函數
  $scope.getWords = function(){
    $http({url: '/words', method: "GET", 
           params:{ limit:$scope.limit, 
                    skip:$scope.skip,
                    sort:$scope.sortField, 
                    direction:$scope.direction,
                    contains:$scope.contains }})
    .success(function(data, status, headers, config) {
      //獲取查詢到的所有數據,並通過Angular的雙向綁定,呈現在視圖表格內
       $scope.words = data;
       //將下次查詢的單詞坐標,調整到當前
       $scope.skipEnd = $scope.skip + $scope.words.length;
     })
     .error(function(data, status, headers, config) {
       $scope.words = [];
       //$scope.skipEnd 呈現在視圖上的單詞查詢區間之一
       $scope.skipEnd = $scope.skip + $scope.words.length;
     });
  };
  $scope.find = function(){
    $scope.skip = 0;
    $scope.getWords();
  };
  $scope.next = function(){
    if($scope.words.length == $scope.limit){
      $scope.skip += parseInt($scope.limit);
      $scope.getWords();
    }
  };
  $scope.prev = function(){
    if($scope.skip > 0){
      if($scope.skip >= parseInt($scope.limit)){
        $scope.skip -= parseInt($scope.limit);
      } else{
        //如果已經到頭瞭,查找索引就置為0
        $scope.skip = 0;
      }
      $scope.getWords();
    }
  };
  //剛打開頁面的時候,調用函數初始化一下
  $scope.getWords();
});

7.3 實現表格AngularJS視圖

完成瞭 tableController 控制器後,就可以使用AngularJS視圖呈現,提供對單詞的過濾、排序和分頁,並進行顯示(後續我使用瞭bootstrap進行佈局)。

//table.html

<p ng-controller="tableController"><hr>
<input class="findButton" type="button" 
       value="Find Words" ng-click="find()" />
<p id="sortOptions">
  <label class="tableLabel">Page Limit</label>
  <input class="tableInput" type="text" ng-model="limit" /><br>
  <label class="tableLabel">Contains</label>
  <input class="tableInput" type="text" ng-model="contains" /><br>
  <label class="tableLabel">Sort By</label>
  <select class="tableInput" ng-model="sortField" 
          ng-options="field for field in sortFields"></select>
  <input type="radio" ng-model="direction" value="asc"> Ascending
  <input type="radio" ng-model="direction" value="desc"> Descending
</p>
<hr>
<p>
  <input class="pageButton" type="button" value="Prev" 
         ng-click="prev()" />
  <input class="pageButton" type="button" value="Next" 
         ng-click="next()" />
  <label class="tableLabel">Words {{skip+1}} to {{skipEnd}}</label>
  <hr>
  <p id="tableContainer">
    <table>
      <tr><th>Word</th><th>First</th><th>Last</th><th>Length</th>
      <th>Vowels</th><th>Consonants</th></tr>
      <tr ng-repeat="word in words">
        <td>{{word.word}}</td>
        <td>{{word.first}}</td>
        <td>{{word.last}}</td>
        <td>{{word.size}}</td>
        <td>{{word.stats.vowels}}</td>
        <td>{{word.stats.consonants}}</td>
      </tr>
    </table>
  </p>
</p>
</p>

效果如下:

這裡寫圖片描述

8. 初始化應用程序

主要就是為數據庫中的words文檔進行初始化。

//word_init.js

var vowelArr = "aeiou";
var consenantArr = "bcdfghjklmnpqrstvwxyz";
//words中就是你想要插入到數據庫中的單詞,數量由你自己決定,在我的文件中,一共是包含瞭4000多個單詞,這裡我就不詳細貼出來瞭。
var words = "the,be,and,of,a,in,to,have,it,I,that...";
var wordArr = words.split(",");
var wordObjArr = new Array();
for (var i=0; i<wordArr.length; i++){
  try{
    var word = wordArr[i].toLowerCase();
    //使用正則分割掉元音,隻剩下輔音字段,使用'|',是為瞭避免分割後的數組中存在空字符
    var vowelCnt = ("|"+word+"|").split(/[aeiou]/i).length-1;
    //使用正則分割出輔音,隻剩下元音字段,使用'|',是為瞭避免分割後的數組中存在空字符
    var consonantCnt = 
      ("|"+word+"|").split(/[bcdfghjklmnpqrstvwxyz]/i).length-1;
    var letters = [];
    var vowels = [];
    var consonants = [];
    var other = [];
    for (var j=0; j<word.length; j++){
      var ch = word[j];
      if (letters.indexOf(ch) === -1){
        letters.push(ch);
      }
      if (vowelArr.indexOf(ch) !== -1){
        if(vowels.indexOf(ch) === -1){
          vowels.push(ch);
        }
      }else if (consenantArr.indexOf(ch) !== -1){
        if(consonants.indexOf(ch) === -1){
          consonants.push(ch);
        }
      }else{
        if(other.indexOf(ch) === -1){
          other.push(ch);
        }
      }
    }
    var charsets = [];
    if(consonants.length){
      charsets.push({type:"consonants", chars:consonants});
    }
    if(vowels.length){
      charsets.push({type:"vowels", chars:vowels});
    }
    if(other.length){
      charsets.push({type:"other", chars:other});
    }
    var wordObj = {
      word: word,
      first: word[0],
      last: word[word.length-1],
      size: word.length,
      letters: letters,
      stats: { vowels: vowelCnt, consonants: consonantCnt },
      charsets: charsets
    };
    if(other.length){
      wordObj.otherChars = other;
    }
    wordObjArr.push(wordObj);
  } catch (e){
    console.log(e);
    console.log(word);
  }
}
var MongoClient = require('mongodb').MongoClient;
MongoClient.connect("mongodb://localhost/27017", function(err, db) {  
  var myDB = db.db("words");
  myDB.dropCollection("words");
  myDB.createCollection("words", function(err, wordCollection){
    wordCollection.insert(wordObjArr, function(err, result){
      console.log(result);
      db.close();
    });
  });
});

9. 額外的工作

至此,應用程序就算是結束瞭,你首先啟動word_init.js 文件初始化數據庫後,就可以啟動項目文件rich_ui_server.js 瞭,然後就可以看到項目的真實效果。

我這裡的天氣api因為調用的是openweathermap.org網站,速度很慢,所以我另外設置瞭緩存,將查詢到的天氣狀況緩存到數據庫,方便下次查詢,並且設置瞭如果天氣日期過期,則重新獲取新的天氣情況數據。

數據庫模型如下:
天氣狀況模型中包括城市名稱(name)、所屬國傢(country)、氣溫(temp)、風速(speed)、風向(deg)、時間(dt_txt)等基本天氣信息,如下:

//weather_cache_model.js

var mongoose=require('mongoose');
var Schema=mongoose.Schema;

var WeatherCacheSchema=new Schema({
    city:{
        id:Number,
        name:String,
        coord:{
            lon:Number,
            lat:Number,
        },
        country:String,
        population:Number,
        sys:{
            population:Number
        }
    },

    cod: String,
    message: Number,
    cnt: Number,
    list:[{
        "dt": Number,
        "main": {
            "temp": Number,
            "temp_min": Number,
            "temp_max": Number,
            "pressure": Number,
            "sea_level": Number,
            "grnd_level": Number,
            "humidity": Number,
            "temp_kf": Number
        },
        "weather": [{
            "id": Number,
            "main": String,
            "description": String,
            "icon": String,
        }],
        "clouds": {
            "all": Number
        },
        "wind": {
            "speed": Number,
            "deg": Number
        },
        "sys": {
            "pod": String,
        },
        "dt_txt": String,
    }]
});

mongoose.model('WeatherCache',WeatherCacheSchema);

10. 程序完成

到這裡,一個基本的NodeJS+Angular+MongoDB Web程序就算是完成瞭。

發佈留言