Nodejs之MEAN棧開發(六)---- 用Angular創建單頁應用(上)


在上一節中我們學會了如何在頁面中添加一個組件以及一些基本的Angular知識,而這一節將用Angular來創建一個單頁應用(SPA)。這意味着,取代我們之前用Express在服務端運行整個網站邏輯的方式(jade、路由都需要在服務端編譯),我們將用Angular在客戶端瀏覽器上跑起來。PS:在正常的開發流程上,我們可能不會在服務器端創建了一個網站,然后又用SPA重建它。但從學習的角度來說這還不錯,這樣掌握了兩種構建方式。

上一節所有Angular相關的代碼都在一個js里面,這不便管理和維護,這一節在根目錄下新建一個app_client,用來專門放單頁相關的代碼。不要忘記設置為靜態:

app.use(express.static(path.join(__dirname, 'app_client')))

Angular路由

 在SPA應用中,頁面間的切換並不會每次都向后台發送求請求。這一節將路由移到客戶端,但保留母版頁(layout.jade),其他視圖用Angular實現。為此先在控制器中新建一個angularApp方法。

module.exports.angularApp = function (req, res) {
    res.render('layout', { title: 'ReadingClub' });
};

設置路由

router.get('/', ctrlOthers.angularApp);

剩下的Express路由是多余的了,你可以刪掉或者注釋掉。為避免頁面重新加載,Angular的默認做法就是在url中加一個#號。#號一般是用來作為錨,來定位頁面上的點,Angular用來訪問應用中的點。比如在Express中,訪問about頁面:

/about

在Angular中,url會變成

/#/about

不過這個#號也是可以拿掉的,畢竟看起來不是那么直觀,這個在下一節講。

老版本的Angular庫是包含路由模塊的,但是現在是作為一個外部依賴文件,可以自己維護。所以先需要下載並添加到項目中。https://code.angularjs.org/1.2.19/

下載angular-route.min.js和angular-route.min.js.map,並在app_client下創建一個app.js

在layout.jade 中添加

    script(src='/angular/angular.min.js')
    script(src='/lib/angular-route.min.js')
    script(src='/app.js')

使用路由前需要設置模塊依賴,要注意的是路由的文件名是angular-route,但實際模塊名稱是ngRoute。在app_client/app.js 下:

angular.module('readApp', ['ngRoute']);

ngRoute模塊會生成一個$routeProvider對象 ,可以用來傳遞配置函數,也就是我們定義路由的地方:

function config($routeProvider) {
    $routeProvider
    .when('/', {})
    .otherwise({ redirectTo: '/' });
}
angular
.module('readApp')
.config(['$routeProvider', config]);
回顧以前的$http,$scope,service 以及現在的$routeProvider 出現在函數參數的時候,Angular會自動為我們獲取實例,這就是Angular的依賴注入機制;config方法定義了路由。而目前這個路由沒有做多少活,但語法很直觀,當URL是'/'時,也就是訪問主頁時什么也不做。而當是別的URL訪問時就跳轉到首頁。接下來我們讓這個路由干點活。

Angular 視圖

 先在app_client文件夾下創建一個home文件夾,用來放置主頁的一些文件。但是目前首頁都還是jade視圖,我們需要將其轉換為html,因此先創建一個home.view.html:

<div class="row"  >
    <div class="col-md-9 page" >
        <div class="row topictype"><a href="/" class="label label-info">全部</a><a href="/">讀書</a><a href="/">書評</a><a href="/">求書</a><a href="/">求索</a></div>
        <div class="row topiclist" data-ng-repeat='topic in data'>
            <img data-ng-src='{{topic.img}}'><span class="count"><i class="coment">{{topic.commentCount}}</i><i>/</i><i>{{topic.visitedCount}}</i></span>
            <span class="label label-info">{{topic.type}}</span><a href="/">{{topic.title}}</a>
            <span class="pull-right">{{topic.createdOn}}</span><a href="/" class="pull-right author">{{topic.author}}</a>
        </div>
    </div>
    <div class="col-md-3">
        <div class="userinfo">
            <p>{{user.userName}}</p>
        </div>
    </div>
</div>

因為還沒有數據,所以這個html片段什么也不會做。而接下來就是告訴Angular模塊,訪問主頁的時候加載這個視圖,這通過templateUrl 實現,修改路由:

function config($routeProvider) {
    $routeProvider
    .when('/', {
        templateUrl: 'home/home.view.html'
    })
    .otherwise({ redirectTo: '/' });
}

但這只是提供了一個模板地址,Angular從哪兒開始替換呢,像Asp.Net MVC中有一個@RenderBody的標記,在jade中是block content。這就需要用到ngRoute模塊中的一個指令:ng-view。被標記的元素會被Angular當成一個容器來切換視圖。我們不妨就加在block content的上方:

    #bodycontent.container
      div(ng-view)
        block content

控制器

 有了路由和視圖,還需要控制器.同樣在home文件夾下創建一個home.controller.js文件,先還是使用靜態數據。經過了上一節,這個部分是輕車熟路。

angular
.module('readApp')
.controller('homeCtrl', homeCtrl);
function homeCtrl($scope) {
    $scope.data = topics;
    $scope.user = {
        userName: "stoneniqiu",
    };
}

再修改路由:

function config($routeProvider) {
    $routeProvider
    .when('/', {
        templateUrl: 'home/home.view.html',
        controller: 'homeCtrl',
    })
    .otherwise({ redirectTo: '/' });
}

這個時候訪問頁面,出來數據了。 所以不管是Asp.net MVC,Express還是Angular,MVC模式的思路是一致的,請求先到達路由,路由負責轉發給控制器,控制器拿到數據然后渲染視圖。

 

 和上一節不同的是,沒有在頁面上使用ng-controller 指令了,而是在路由里面指定。

 controllerAs 

 Angular提供了一個創建視圖模型的方法來綁定數據,這樣就不用每次直接修改$scope 對象,保持$scope 干凈。 

function config($routeProvider) {
    $routeProvider
    .when('/', {
        templateUrl: 'home/home.view.html',
        controller: 'homeCtrl',
        controllerAs: 'vm'
    })
    .otherwise({ redirectTo: '/' });
}

紅色代碼表示啟用controllerAs語法,對應的視圖模型名稱是vm。這個時候Angular會將控制器中的this綁定到$scope上,而this又是一個上下文敏感的對象,所以先定義一個變量指向this。controller方法修改如下

function homeCtrl() {
    var vm = this;
    vm.data = topics;
    vm.user = {
        userName: "stoneniqiu",
    };
}

注意我們已經拿掉了$scope參數。然后再修改下視圖,加上前綴vm

<div class="row"  >
    <div class="col-md-9 page" >
        <div class="row topictype"><a href="/" class="label label-info">全部</a><a href="/">讀書</a><a href="/">書評</a><a href="/">求書</a><a href="/">求索</a></div>
          <div class="error">{{ vm.message }}</div>
          <div class="row topiclist" data-ng-repeat='topic in vm.data'>
            <img data-ng-src='{{topic.img}}'><span class="count"><i class="coment">{{topic.commentCount}}</i><i>/</i><i>{{topic.visitedCount}}</i></span>
            <span class="label label-info">{{topic.type}}</span><a href="/">{{topic.title}}</a>
            <span class="pull-right">{{topic.createdOn}}</span><a href="/" class="pull-right author">{{topic.author}}</a>
        </div>
    </div>
    <div class="col-md-3">
        <div class="userinfo">
            <p>{{vm.user.userName}}</p>
        </div>
    </div>
</div>

service:

 因為服務是給全局調用的,而不是只服務於home,所以再在app_clinet下新建一個目錄:common/services文件夾,並創建一個ReadData.service.js :

angular
.module('readApp')
.service('topicData', topicData);

function topicData ($http) {
    return $http.get('/api/topics');
};

直接拿來上一節的代碼。注意function寫法, 最好用function fool()的方式,而不要var fool=function() 前者和后者的區別是前者的聲明會置頂。而后者必須寫在調用語句的前面,不然就是undefined。修改layout

    script(src='/app.js')
    script(src='/home/home.controller.js')
    script(src='/common/services/ReadData.service.js')

相應的home.controller.js 改動:

function homeCtrl(topicData) {
    var vm = this;
    vm.message = "loading...";
    topicData.success(function (data) {
        console.log(data);
        vm.message = data.length > 0 ? "" : "暫無數據";
        vm.data = data;
    }).error(function (e) {
        console.log(e);
        vm.message = "Sorry, something's gone wrong ";
    });
    vm.user = {
        userName: "stoneniqiu",
    };
}

這個時候頁面已經出來了,但是日期格式不友好。接下來添加過濾器和指令

filter&directive

 在common文件夾創建一個filters目錄,並創建一個formatDate.filter.js文件,同上一節一樣

angular
.module('readApp')
.filter('formatDate', formatDate);

function formatDate() {
    return function (dateStr) {
        var date = new Date(dateStr);
        var d = date.getDate();
        var monthNames = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"];
        var m = monthNames[date.getMonth()];
        var y = date.getFullYear();
        var output = y + '/' + m + '/' + d;
        return output;
    };
};

然后在common文件夾下新建一個directive文件夾,再在directive目錄下新建一個ratingStars目錄。ratingStars指令會在多個地方使用,它包含一個js文件和一個html文件,將上一節的模板文件復制過來,並命名為:ratingStars.template.html。然后新建一個ratingStars.directive.js文件,拷貝之前的指令代碼,並改造兩處。

angular
.module('readApp')
.directive('ratingStars', ratingStars);

function ratingStars () {
return {
restrict: 'EA',
scope: {
thisRating : '=rating'
},
templateUrl: '/common/directive/ratingStars/ratingStars.template.html'
};
}

EA表示指令作用的范圍,E表示元素(element),A表示屬性(attribute),A是默認值。還C表示樣式名(class),M表示注釋(comment), 最佳實踐還是EA。更多知識可以參考這篇博客 Angular指令詳解

因為還沒有創建booksController,先用topic.commentCount來測試ratingStars指令,並記得在layout下添加引用

  <div class="row topiclist" data-ng-repeat='topic in vm.data'>
            <img data-ng-src='{{topic.img}}'><span class="count"><i class="coment">{{topic.commentCount}}</i><i>/</i><i>{{topic.visitedCount}}</i></span>
               <small rating-stars rating="topic.commentCount"></small>
               <span class="label label-info">{{topic.type}}</span><a href="/">{{topic.title}}</a>
            <span class="pull-right">{{topic.createdOn | formatDate}}</span><a href="/" class="pull-right author">{{topic.author}}</a>
        </div>

 這個時候效果已經出來了。

有哪些優化?

這一節和上一節相比,展現的內容基本沒有變化,但組織代碼的結構變得更清晰好維護了,但還是不夠好,比如layout里面我們增加了過多的js引用。這也是很煩的事情。所以我們可以做一些優化:

1.減少全局變量  

第一點,在團隊開發的時候要盡量減少全局變量,不然容易混淆和替換,最簡單的辦法就是用匿名函數包裹起來:
(function() {
 //....
})();

被包裹的內容會在全局作用域下隱藏起來。而且在這個Angular應用也不需要通過全局作用域關聯,因為模塊之間都是通過angular.module('readApp', ['ngRoute'])連接的。controller、service、directive這些js都可以處理一下。

2.減少JavaScript的尺寸

我們可以讓js最小化,但有一個問題,在controller中的依賴注入會受影響。因為JavaScript在最小化的時候,會將一些變量替換成a,b,c

function homeCtrl ($scope, topicData, otherData)

會變成:

function homeCtrl(a,b,c){

這樣依賴注入就會失效。這個時候怎么辦呢,就要用到$inject ,$inject作用在方法名稱后面,等於是聲明當前方法有哪些依賴項。

homeCtrl.$inject = ['$scope', 'topicData', 'otherData']; function homeCtrl ($scope, topicData, otherData) {

$inject數組中的名字是不會在最小化的時候被替換掉的。但記住順序要和方法的調用順序一致。

topicData.$inject = ['$http'];
function topicData ($http) {
    return $http.get('/api/topics');
};

做好了這個准備,接下來就可以最小化了

3.減少文件下載

在layout中我們引用了好幾個js,這樣很煩,可以使用UglifyJS 去最小化JavaScript文件。 UglifyJS 能將Angular應用的源文件合並成一個文件然后壓縮,而我們只需在layout中引用它的輸出文件即可。 

 安裝:
 

 然后在根目錄/app.js中引用

var uglifyJs = require("uglifyjs");
var fs = require('fs');
接下來有三步
1.列出需要合並的文件
2.調用uglifyJs 來合並並壓縮文件。
3.然后保存在Public目錄下。
在/app.js下var一個appClientFiles數組,包含要壓縮的對象。然后調用uglifyjs.minify方法壓縮,然后寫入public/angular/readApp.min.js
var appClientFiles = [
    'app_client/app.js',
    'app_client/home/home.controller.js',
    'app_client/common/services/ReadData.service.js',
    'app_client/common/filters/formatDate.filter.js',
    'app_client/common/directive/ratingStars/ratingStars.directive.js'
];

var uglified = uglifyJs.minify(appClientFiles, { compress : false });

fs.writeFile('public/angular/readApp.min.js', uglified.code, function (err) {
    if (err) {
        console.log(err);
    } else {
        console.log('腳本生產並保存成功: readApp.min.js');
    }
});

最后修改layout:

    script(src='/angular/readApp.min.js')
    //script(src='/app.js')
    //script(src='/home/home.controller.js')
    //script(src='/common/services/ReadData.service.js')
    //script(src='/common/filters/formatDate.filter.js')
    //script(src='/common/directive/ratingStars/ratingStars.directive.js')

這里選擇注釋而不是刪掉,為了便於后面的調試。但如果用nodemon啟動,它會一直在重啟。因為生產文件的時候觸發了nodemon重啟,如此循環。所以這里需要一個配置文件告訴nodemon忽略掉這個文件的改變。在根目錄下新增一個文件nodemon.json

{
  "verbose": true,
  "ignore": ["public//angular/readApp.min.js"]
}

這樣就得到了一個min.js 。原本5個文件是5kb,換成min之后是2kb。所以這個優化還是很明顯的。

源碼:https://github.com/stoneniqiu/ReadingClub (注意不同分支)

小結:這一節主要是構建SPA的基礎環境,和以前不同的是我們將視圖、路由、一部分的邏輯從服務端的Express移到了前端的Angular,學習了Angular路由、視圖,結構上更加清楚,最后對整體的JavaScript進行了優化。下一節再更深入的講解基於Angular的SPA。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM