天天看點

Mastering Web Application Development with AngularJS-Packt 2013 讀書筆記(不錯的好書!) Angular Zen 建構和測試 與後端伺服器通信 顯示和格式化資料 建立進階表單 組織導航(前端路由) App安全 定制指令 進階指令 國際化和本地化 編寫健壯的Web應用 打包和部署

Angular Zen

  • Batarang

    Batarang is a Chrome developer tool extension for inspecting the AngularJS web applications. Batarang is very handy for visualizing and examining the runtime characteristics of AngularJS applications. We are going to use it extensively in this book to peek under the hood of a running application. Batarang can be installed from the Chrome's Web Store (AngularJS Batarang) as any other Chrome extension.

  • Sublime 2插件: https://github.com/angular-ui/AngularJS-sublime-package
  • ng-app ng-init
  • 2-way binding:

    <input type="text" ng-model="name">

控制器

<div ng-controller="HelloCtrl">
...
var HelloCtrl = function ($scope) {
   ... //初始化scope對象
}
           

scope

<li ng-repeat="country in countries">
           

注意這裡ng-repeat指令為每次疊代的country變量建立一個單獨的scope!(仔細體會這裡的精髓~)

scope的讀通路類似于JS的prototype,相當直覺(但是前提是沒有名字shadowing)。寫通路就不是了:

<input type="text" ng-model="$parent.name">
           

避免使用$parent,這使得表達式依賴于DOM樹結構。

scope上的事件傳播:

  • $emit 向上
  • $broadcast
$scope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl){
    ...
})
           

3個$emit事件:

  1. $includeContentRequested
  2. $includeContentLoaded
  3. $viewContentLoaded

7個$broadcast事件:

  1. $locationChangeStart
  2. $locationChangeSuccess
  3. $routeUpdate
  4. $routeChangeStart
  5. $routeChangeSuccess
  6. $routeChangeError
  7. $destroy

DI

注冊服務

  • $provide
  • $injector

DI管理的對象類型

  • value
  • myModule.service('notificationsService', NotificationsService);
  • factory
    • 對象可以引用内部私有狀态
  • constant 常量
  • 最一般的:provider
    • $get
    • 對象可以擁有額外的屬性/方法

子產品生命周期

  • 配置階段
myMod.config(function(notificationsServiceProvider){ //<-- DI風格;
    notificationsServiceProvider.setMaxLen(5);
});
           
  • 運作階段
angular.module('upTimeApp', []).run(function($rootScope) {
    $rootScope.appStarted = new Date();
});
           
  • 子產品依賴于其他子產品
angular.module('application', ['notifications', 'archive'])
           

A service defined in one of the application's modules is visible to all the other modules. (平面的名字空間)

** redefine to override(更靠近根的優先)

Testacular(spectacular test runner)

A sneak peek into the future

  • Object.observe 就為了把Angular的性能提升20%?幸好後來被廢棄了(核心開發者發現此API容易被濫用)

建構和測試

  • Sample應用:https://github.com/angular-app/angular-app
  • 項目模闆:https://github.com/angular/angular-seed
  • Anatomy of a Jasmine test
    • describe
    • beforeEach
      beforeEach(module('archive'));
      beforeEach(*inject*(function (_notificationsArchive_) { //inject傳回了一個callback函數?
          notificationsArchive = _notificationsArchive_;
      }));
                 
    • it
    • expect
  • Mock objects and asynchronous code testing
    • $timeout.flush()
  • 端到端測試
    • Karma runner tips and tricks(有點意思)
      • xdescribe xit
      • ddescribe
      • iit

與後端伺服器通信

XHR & JSONP by $http

  • MongoLab
    • https://api.mongolab.com/api/1/databases/[DB-name]/collections/[collection-name]/[item-id]?apiKey=[secret-key]
  • JSONP:angular.callbacks._k
  • CORS
    • OPTIONS
  • Server-side proxies

promise with $q

  • $q.defer():
    • ~.promise.then(succCb, errCb);
      • then可注冊多次
    • ~.resolve(value)
  • 最後需要 $rootScope.$digest()
  • 異步調用鍊 We can return a new promise from an error callback. The returned promise will be part of the resolution chain, and the final consumer won't even notice that something went wrong. (但是如果這個error callback是在一個很長的異步調用鍊中間時?)
    • return ... .then(...) 或 $q.reject(...)
  • $q.all
    • 怎麼沒有race方法?
  • $q.when:包裝普通value為promise
  • $q在AngularJS中內建
    • promise可作為普通value在model中使用:
    $scope.name = $timeout(function () {
            return "World";
        }, 2000);
               
    • 但是傳回promise的函數調用不能直接用在{{...}}模闆中

REST通信with $resource

顯示和格式化資料

  • 嵌入html代碼:

    <p ng-bind-html-unsafe="msg"></p>

    • 或 ng-bind-html ,需引入ngSanitize
  • 條件顯示: ng-show / ng-hide , ng-switch-* , ng-if and ng-include
    • ng-switch/if 會添加/删除DOM元素,并建立新scope
  • ng-include:動态包含内容,

    <div ng-include="'header.tpl.html'"></div>

  • ng-repeat
    <li ng-repeat="(name, value) in user">
        Property {{$index}} with {{name}} has value {{value}}
    </li>
               
    ng-repeat預設會對name屬性在輸出前進行排序?
    <tbody ng-repeat="user in users" ng-click="selectUser(user)" ng-switch on="isSelected(user)">
               
    注意這裡user的scope
    • 可在ng-repeat作用的元素上定義controller,以顯式使用ng-repeat建立的scope
  • 事件
    • <li ... ng-click="logPosition(item, $event)"

  • AngularJS (1.2.x):ng-repeat不再需要應用到單獨的容器元素(而可以是一組元素,fragment)
    <li ng-repeat-start="item in items">
        <strong>{{item.name}}</strong>
    </li>
    <li ng-repeat-end>{{item.description}}</li>
               
  • IE不允許動态修改input元素的type屬性
    • <input type="{{myinput.type}}" ng-model="myobject[myinput.model]">

      • 繞過:

        <ng-include src="'input'+myinput.type+'.html'"></ng-include>

  • 過濾器 略(就一個日期格式化、及數字編号的寬度對齊可能項目中會用到)
    • $
    ng-repeat="item in filteredBacklog = (backlog | filter:{$: criteria, done: false})"
               
    • DI中通路
      • var limitToFilter = $filter('limitTo');

建立進階表單

暫略(表單不是目前關注的内容)

組織導航(前端路由)

$location服務

  • $anchorScroll 

    $anchorScrollProvider.disableAutoScrolling();

  • 不需要# 

    $locationProvider.html5Mode(true);

     但還是需要伺服器端配置重定向?
  • Structuring pages around routes
    • navbar: 

      <a href="#/admin/users/list" target="_blank" rel="external nofollow" >List users</a>

    • ng-include:

      <div class="container-fluid" ng-include="selectedRoute.templateUrl"> ... </div>

    $scope.$watch(function () {
        return $location.path();
    }, function (newPath) {
        $scope.selectedRoute = routes[newPath] || defaultRoute; //ng-include指令将響應此model的變化
    });
               

$route服務

1.2:ngRoute服務被分離到單獨的angular-route.js檔案。

angular.module('routing_basics', [])
    .config(function($routeProvider) {
        $routeProvider
            .when('/admin/users/list', {templateUrl: 'tpls/users/list.html'}) //可以指定controller屬性;
            .when('/admin/users/new', {templateUrl: 'tpls/users/new.html'})
            .when('/admin/users/:id', {templateUrl: 'tpls/users/edit.html'})
            .otherwise({redirectTo: '/admin/users/list'});
    })
           

疑問:

  1. 動态url path看起來可以做到,注意到url path支援:id占位符這種形式
  2. templateUrl可以改用template嗎?其内容是替換整個body還是可以指定容器根元素?ng-view指令指定??
    <div class="container-fluid" ng-view>
        <!-- Route-dependent content goes here -->
    </div>
               
  3. config隻能執行一次?
  • 避免route改變時UI閃爍
    • 盡快顯示markup html,并在有資料後再次update —— 這回造成閃爍
    • 確定route改變前,所有後端請求已完成
      • resolve:枚舉controller的所有異步依賴項
        • key是注入到controller的變量,對應的value是擷取它的函數,可以傳回一個promise
  • 阻止route改變
    • resolve:function傳回一個rejected promise。缺陷:位址欄的新url無法被重置回之前的?
    If the route's navigation is canceled the browser's address bar won't be reverted and will still read  /users/edit/1234 , even if UI will be still reflecting, content of the  /users/list route.
               
  • 預設route的局限
    • 更強大的

      ui-router

       https://github.com/angular-ui/ui-router
    • ng-view隻能定義一個矩形區域(hole)—— 但是可以改變hole的位置吧?
    • 不支援route嵌套(url path)
      • 對于使用了iframe tree的Web應用可能比較重要(比如feedly?不對不對,feedly沒有使用iframe元素)
  • Routing-specific patterns, tips, and tricks
    • I see:

      <a ng-href="/admin/users/{{user.$id()}}" target="_blank" rel="external nofollow" >Edit user</a>

    • 連結到外部資源:target="_self"
    • 每個module可config自己的$routeProvider(那麼假如module可以動态添加的話,route也可以了??)
    • 封裝 $routeProvider 服務,提供定制的provider,以減少代碼備援
      • TODO

App安全

  • $templateCache
  • 防止XSS
    • 注意對html格式的動态嵌入内容進行轉義即可
  • JSON注入(防止JSON代碼可執行)
    • $http:有意在response之前注入 ")]}',\n"
  • 防止XSRF
    • $http要求伺服器端在session cookie中設定一個XSRF-TOKEN
  • Adding client-side security support 略
  • 用定制的securityInterceptor服務來處理401響應:略
    • 不過,值得借鑒:可用于實作在會話逾時失效的情況下自動重新登入
  • route resolve中使用定制的authorization服務 略

定制指令

  • 内建指令:https://github.com/angular/angular.js/tree/master/src/ng/directive/
The compile stage is mostly an optimization. It is possible to do almost all the work in the linking function (
  except for a few advanced things like access to the transclusion function). If you consider the case of a 
  repeated directive (inside ng-repeat), the compile function of the directive is called only once, but the 
  linking function is called on every iteration of the repeater, every time the data changes.
           
  • 測試
element = linkingFn(scope); //實際上,原來模闆中對應的DOM元素也一直挂載在document中?
           
scope.$digest(); //當測試中使用了$watch、$observe、$q時需要;
           
  • A directive definition is an object which ...
  • 設定button元素樣式(但是button不已經是HTML标準元素嗎)
myModule.directive('button', function() {
    return {
        restrict: 'E',
        compile: function(element, attributes) {
            element.addClass('btn');
            if ( attributes.size ) {
                element.addClass('btn-' + attributes.size);
            }
        }
    };
});
           

指令不依賴于scope資料:隻在compile中處理即可。

  • 編寫一個分頁指令
    • Isolated scope:child scope的prototype斷開與parent scope的連結,但是$parent仍然可以引用到?
    • There are three types of interface we can specify between the element's attributes and the isolated scope: interpolate (@), data bind (=), and expression (&).
    • @等價于:
    attrs.$observe('attribute1', function(value) {
        isolatedScope.isolated1 = value;
    });
    attrs.$$observers['attribute1'].$$scope = parentScope;
               
    • =相當于2個$watch:(實際實作要更複雜點)
    var parentGet = $parse(attrs['attribute2']);
    var parentSet = parentGet.assign;
    parentScope.$watch(parentGet, function(value) {
        isolatedScope.isolated2 = value;
    });
    isolatedScope.$watch('isolated2', function(value) {
        parentSet(parentScope, value);
    });
               
    • &提供了一個回調表達式:
    parentGet = $parse(attrs['attribute3']);
    scope.isolated3 = function(locals) {
        return parentGet(parentScope, locals);
    };
               
    • &的用法示例:
    scope: { ...,
        onSelectPage: '&'
    },
    ...
    scope.onSelectPage({ page: page }); //map傳參就相當于scope上的屬性一樣;
        //==> 使用:on-select-page="selectPageHandler(page)"
               
  • 編寫定制校驗指令(需要引用同一級别DOM元素上的scope關聯資料)
    • require的用法:
    require: '^?ngModel',
    link: function(scope, element, attrs, ngModelController) { ... }
               
    • ngModelController暴露了下列屬性:
      • $parsers
      • $formatters
      • $setValidity(validationErrorKey, isValid)
      • $valid
      • $error
    • link實作:(注意這裡參數被重命名了,前後不太一緻!)
    function validateEqual(myValue) {
        var valid = (myValue === scope.$eval(attrs.validateEquals));
        ngModelCtrl.$setValidity('equal', valid);
        return valid ? myValue : undefined;
    }
    ngModelCtrl.$parsers.push(validateEqual);
    ngModelCtrl.$formatters.push(validateEqual);
    scope.$watch(attrs.validateEquals, function() {
        ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue);
    });
               
  • 建立一個異步校驗指令
    • TDD:

      <input ng-model="user.email" unique-email>

    • ngModelCtrl.$parsers.push(function (viewValue) {...略}

  • 封裝jQuery datepicker:(這個例子有點複雜)
    • link實作:
    ...
    var updateModel = function () {
        scope.$apply(function () {
             var date = element.datepicker("getDate");
             element.datepicker("setDate", element.val());
             ngModelCtrl.$setViewValue(date);
        });
    };
    ...
    ngModelCtrl.$render = function () {
        element.datepicker("setDate", ngModelCtrl.$viewValue);
    };
               

進階指令

Using transclusion

  • 當元素在DOM樹上移動位置到新的scope時,仍然能夠"bring the original scope with us"
    Transclusion is necessary whenever a directive is replacing its original contents with new elements but wants to use the original contents somewhere in the new elements.
               
  • ng-repeat不同尋常,因為它clone自身元素;更常見的是用于

    templated widget

    1. 建立一個定制的alert指令:
      • 看起來它就是一個scope資料的xml封裝,這讓人想到最新的W3C規範 template 元素,以及Web Components...
      <alert type="alert.type" close="closeAlert($index)" ng-repeat="alert in alerts">
          {{alert.msg}}
      </alert>
                 
      • 這裡的type屬性映射到指令template的isolate scope?
        • 但transcluded scope(即alert.msg)仍然從原來的parent scope繼承...
    2. alert指令的實作
    myModule.directive('alert', function () {
        return {
            restrict:'E',
            replace: true,
            transclude: true,
            template:
                '<div class="alert alert-{{type}}">' +
                    '<button type="button" class="close" ng-click="close()">&times;</button>' +
                    '<div ng-transclude></div>' +
                '</div>',
            scope: { type:'=', close:'&' }
        };
    });
               
    1. 了解

      replace: true

      • 屬性将從拷貝到template的div元素上
      • 如果指定了template但未指定replace,則template内容會append到指令元素()
    2. 了解

      transclude

      屬性
      • true:transclude的是指令元素的children
      • element:transclude的是整個指令元素(對應于ng-repeat的例子)
    3. 了解transclusion的scope
      • 指令template中的表達式無法通路指令元素所在的parent scope???

Creating and working with transclusion functions

  • transclude: true

var elementsToTransclude = directiveElement.contents();
    directiveElement.html('');
    var transcludeFunction = $compile(elementsToTransclude);
           
  • clone when transcluding:
var clone = linkingFn(scope, function callback(clone) {
    element.append(clone);
});
           
  • 在指令中通路transclusion函數:
myModule.directive('myDirective', function() {
    return {
        transclude: true,
        compile: function(element, attrs, transcludeFn) { ... },
        controller: function($scope, $transclude) { ... },
    };
});
           
  • 通路transclusion函數所在的scope:
compile: function(element, attrs, transcludeFn) {
    return function postLink(scope, element, attrs, controller) {
        var newScope = scope.$parent.$new();
        element.find('p').first().append(transcludeFn(newScope));
    };
}
           
  • 改在controller中通路:注入的$transclude已綁定scope!
controller: function($scope, $element, $transclude) {
    $element.find('p').first().append($transclude());
}
           
  • 建立一個if指令,使用transclusion函數(而不是ng-transclude) 略
  • transclude: 'element'

    情況下priority屬性的處理
    • The 

      ng-repeat

       directive has 

      transclude: 'element'

       and 

      priority: 1000

      ,
      • so generally all attributes that appear on the ng-repeat element are transcluded to appear on the cloned repeated elements.

Understanding directive controllers

  • 指令控制器 vs ng-controller
    • TODO
  • 注入特殊依賴:
    1. $element
    2. $attrs
    3. $transclude
  • 建立一個基于指令控制器的分頁指令
controller: ['$scope, '$element', '$attrs',
    function($scope, $element, $attrs) {
        ...
    }]
           
  • vs link函數
If an element contains multiple directives then for that element:
    • A scope is created, if necessary
    • Each directive's directive controller is instantiated
    • Each directive's pre-link function is called
    • Any child elements are linked
    • Each directive's post-link function is called
           

link函數可通過require注入控制器參數,而指令控制器不能注入其他的指令控制器。(并非實作上不可能吧?)

  • 用法示例:accordion
myModule.controller('AccordionController', ['$scope', '$attrs',
    function ($scope, $attrs) {
        ...
    }
    ]);

myModule.directive('accordion', function () {
    return {
        restrict:'E',
        controller:'AccordionController',
            //accordion指令引入的controller可被其子指令通過require注入到子指令的link;
        link: function(scope, element, attrs) {
            element.addClass('accordion');
        }
    };
})

myModule.directive('accordionGroup', function() {
    return {
        require:'^accordion',
        //下略
           

手工控制compile過程

  • TDD:
<field type="email" ng-model="user.email" required >
    <label>Email</label>
    <validator key="required">$fieldLabel is required</validator>
    <validator key="email">Please enter a valid email</validator>
</field>
           
  • 定制compile實作:
priority: 100, //在ng-model之前執行;
terminal: true,
compile: function(element, attrs) {
  ...
  var validationMgs = getValidationValidationMessages(element);
  var labelContent = getLabelContent(element);
  element.html('');
  return function postLink(scope, element, attrs) {
    var template = attrs.template || 'input.html';
    loadTemplate(template).then(function(templateElement) {
    ... 
    });
  };
}
           
  • 需要自己處理

    {{}}

    插值:$interpolate服務 略
  • 動态加載template
function loadTemplate(template) {
    return $http.get(template, {cache:$templateCache})
        .then(function(response) {
            return angular.element(response.data);
        }, function(response) {
            throw new Error('Template not found: ' + template);
        }
    );
}
           

... faint,下面還有一堆代碼 ... skip

國際化和本地化

  • <span>{{'greetings.hello' | i18n}}, {{name}}!</span>

     用了一個過濾器?
  • i18n key='greetings.hello'></i18n>

    • 不再需要附加的$watch(提高了性能?)
  • Translating partials during the build-time
    • Grunt.js模版?

      <%= greeting.hello %>

編寫健壯的Web應用

  • 了解AngularJS的inner workings
    • scope.$watch(watchExpression, modelChangeCallback)

    • scope.$apply
      • Enter the $digest loop
      AngularJS makes sure that all the model values are calculated and "stable" before giving control back to the DOM rendering context. This way UI is repainted in one single batch, instead of being constantly redrawn in response to individual model value changes.
                 
      • Model stability
        • dirty-checking算法:watchExpression至少會被求值2次!
        • 每次都必須從$rootScope開始,求值所有$watch
  • 性能優化(邊界)
AngularJS, as any other well-engineered library, was constructed within a frame of certain boundary conditions, and those are best described by Misko Hevery, father of AngularJS (http://stackoverflow.com/a/9693933/1418796):
Humans are:
    slow:小于50ms但延遲感覺不到
    limited(資訊過載的問題)
So the real question is this: can you do 2000 comparisons in 50 ms even on slow browsers?
           
  • 性能測量 Batarang allows us to easily pinpoint the slowest watch expressions(這個要試一試)
  • Avoid DOM access in the watch-expression
    • CSS渲染屬性的通路會觸發reflow?
    The entire AngularJS philosophy is based on the fact that the source of truth is the model. It is the model that drives declarative UI. But by observing DOM properties, we are turning things upside-down!
               
  • 不要寫 

    {{myComplexComputation()}}

  • Don't watch for invisible
  • call scope.$digest if 确切知道哪個scope受到model修改的影響
  • Remove unused watches
    var watchUnregisterFn = $scope.$watch(...)
    watchUnregisterFn(); //remove this watch;
               
  • Entering the $digest loop less frequently
    • $timeout(update, 1000, false);

    • 每次mouse移動(travels over)都會觸發$digest loop:
      • <div ng-class='{active: isActive}' ng-mouseenter ='isActive=true' ng- mouseleave='isActive=false'>Some content</div>

      • ? 考慮使用定制指令,以響應事件,修改DOM... how?
  • Avoid deep-watching whenever possible(可能導緻大的記憶體消耗)
    • $scope.$watch(expr, cb, true); //<-- 把這裡的expr改成function類型,傳回序列化後的string

打包和部署

  • Preloading templates
    1. <script type="text/ng-template" id="tpls/users/list.html"> ... </scriopt>

    2. 使用$templateCache服務
  • Optimizing the landing page
    • ng-cloak 将元素設定為display:none; 直到

      {{}}

      裡的model資料準備好
    • {{name}}

      改寫成

      <span ng-bind="name"></span>

  • 與AMD
    • ng-app

       => 

      angular.bootstrap

      , This way you can control the timing of AngularJS kickstarting the application.

繼續閱讀