在上一節中咱們學會了如何在頁面中添加一個組件以及一些基本的Angular知識,而這一節將用Angular來建立一個單頁應用(SPA)。這意味着,取代咱們以前用Express在服務端運行整個網站邏輯的方式(jade、路由都須要在服務端編譯),咱們將用Angular在客戶端瀏覽器上跑起來。PS:在正常的開發流程上,咱們可能不會在服務器端建立了一個網站,而後又用SPA重建它。但從學習的角度來講這還不錯,這樣掌握了兩種構建方式。html
上一節全部Angular相關的代碼都在一個js裏面,這不便管理和維護,這一節在根目錄下新建一個app_client,用來專門放單頁相關的代碼。不要忘記設置爲靜態:前端
app.use(express.static(path.join(__dirname, 'app_client')))
Angular路由node
在SPA應用中,頁面間的切換並不會每次都向後臺發送求請求。這一節將路由移到客戶端,但保留母版頁(layout.jade),其餘視圖用Angular實現。爲此先在控制器中新建一個angularApp方法。git
module.exports.angularApp = function (req, res) { res.render('layout', { title: 'ReadingClub' }); };
設置路由angularjs
router.get('/', ctrlOthers.angularApp);
剩下的Express路由是多餘的了,你能夠刪掉或者註釋掉。爲避免頁面從新加載,Angular的默認作法就是在url中加一個#號。#號通常是用來做爲錨,來定位頁面上的點,Angular用來訪問應用中的點。好比在Express中,訪問about頁面:github
/about
在Angular中,url會變成express
/#/about
不過這個#號也是能夠拿掉的,畢竟看起來不是那麼直觀,這個在下一節講。json
老版本的Angular庫是包含路由模塊的,可是如今是做爲一個外部依賴文件,能夠本身維護。因此先須要下載並添加到項目中。https://code.angularjs.org/1.2.19/api
下載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]);
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 指令了,而是在路由裏面指定。
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引用。這也是很煩的事情。因此咱們能夠作一些優化:
(function() { //.... })();
被包裹的內容會在全局做用域下隱藏起來。並且在這個Angular應用也不須要經過全局做用域關聯,由於模塊之間都是經過angular.module('readApp', ['ngRoute'])鏈接的。controller、service、directive這些js均可以處理一下。
咱們可讓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'); };
作好了這個準備,接下來就能夠最小化了
在layout中咱們引用了好幾個js,這樣很煩,可使用UglifyJS 去最小化JavaScript文件。 UglifyJS 能將Angular應用的源文件合併成一個文件而後壓縮,而咱們只需在layout中引用它的輸出文件便可。
而後在根目錄/app.js中引用
var uglifyJs = require("uglifyjs"); var fs = require('fs');
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。