1. 程式人生 > >AngularJS指令開發 詳解一

AngularJS指令開發 詳解一

費了不少時間泡在自己的Angular UI for mobile計劃,但看來是要擱置一下。一個是因為最近公司專案緊,自己還要忙著考駕照,心力交瘁啊。當然,最重要的原因還是,在Angular的使用上,鑽得越深,發現水越深,不得不停下來,重新整理一下知識點。

趕緊開始。

指令,很重要

AngularJS與JQuery最大的區別在哪裡?我認為,表現在資料雙向繫結,實質就是DOM的操作形式不一樣。

  • JQuery通過選擇器找到DOM元素,再賦予元素的行為;

  • 而AngularJS則是,將指令與DOM繫結在一起,再擴充套件指令的行為。

所以AngularJS開發最理想的結果就是,在頁面HTML與CSS的設計時,設計工程師只需要關注指令的使用;而在背後的邏輯開發上,架構工程師則是不需要知道如何操作DOM,只需要關注指令背後的行為要如何實現就行;測試工程師也可以開發針對指令的單元測試。

指令就是DOM與邏輯行為的媒介,本質就是DOM繫結的獨立邏輯行為函式。

指令難點在於引數

來看看都有哪些

angular.module('app', [])
.directive('myDirective', function() {
    return {
    restrict: String,                
    priority: Number,
    terminal: Boolean,
    template: String or Template Function:
    function(tElement, tAttrs) {...},
    templateUrl: String
, replace: Boolean or String, scope: Boolean or Object, transclude: Boolean, controller: String or function(scope, element, attrs, transclude, otherInjectables) { ... }, controllerAs: String, require: String, link: function(scope, iElement, iAttrs) { ... }, compile: // 返回一個物件或連線函式,如下所示:
function(tElement, tAttrs, transclude) { return { pre: function(scope, iElement, iAttrs, controller) { ... }, post: function(scope, iElement, iAttrs, controller) { ... } } return function postLink(...) { ... } } }; });

剛開始接觸指令的時候,我簡直就是蒙了,這堆引數究竟怎麼用怎麼理解啊。告訴大家我的一個理解方法。
把它們分成三類:

  1. 描述指令或DOM本身特性的內部引數

  2. 連線指令外界、與其他指令或控制器溝通的對外引數

  3. 描述指令本身行為的行為引數

內部引數

  • restrict:String,E(元素)<my-directive></my-directive> A(屬性,預設值)<div my-directive="expression"></div> C(類名)<div class="my-directive:expression;"></div> M(註釋)<--directive:my-directive expression-->

  • priority: Number,指令執行優先順序

  • template: String,指令連結DOM模板,例如“<h1>{{head}}</h1>”

  • templateUrl:String,DOM模板路徑

  • replace: Boolean,指令連結模板是否替換原有元素,

對外引數——scope

scope引數非常重要,本應該是放到最後說明的,但是scope卻是理解其他引數的關鍵,所以務必先跟大家說清楚。

scope引數的作用是,隔離指令與所在控制器間的作用域、隔離指令與指令間的作用域。

scope引數是可選的,預設值為false,可選true、物件{};

  • false:共享父域

  • true:繼承父域,且新建獨立作用域

  • 物件{}:不繼承父域,且新建獨立作用域

false、true、{}三者對比

來看個例子

<body>
    <div ng-controller='parentCtrl'>
        <h3>指令scope引數——false、true、{}對比測試</h3>
        parent:
        <div>
            <span> {{parentName}}</span>
            <input type="text" ng-model="parentName" />
        </div>
        <br />
        <child-a></child-a>
        <br />
        <child-b></child-b>
        <br />
        <child-c parent-name="parentName"></child-c>
    </div>
    <!--t1指令模板-->
    <script type="text/html" id="t1">
        <div>
            <span>{{parentName}}</span>
            <input type="text" ng-model="parentName" />
        </div>
    </script>
    <script>
        var app = angular.module("app", []);

        app.controller('parentCtrl', function ($scope) {
            $scope.parentName = "parent";
        })

        //false:共享作用域
        app.directive('childA', function () {
            return {
                restrict: 'E',
                scope: false,
                template: function (elem, attr) {
                    return "false:" + document.getElementById('t1').innerHTML;
                }
            };
        });

        //true:繼承父域,並建立獨立作用域
        app.directive('childB', function () {
            return {
                restrict: 'E',
                scope: true,
                template: function (elem, attr) {
                    return "true:" + document.getElementById('t1').innerHTML;
                },
                controller: function ($scope) {
                    $scope.parentName = "parent";

                    //已宣告的情況下,$scope.$watch監聽的是自己的parentName
                    $scope.$watch('parentName', function (n, o) {
                        console.log("child watch" + n);
                    });

                    //$scope.$parent.$watch監聽的是父域的parentName
                    $scope.$parent.$watch('parentName', function (n, o) {
                        console.log("parent watch" + n);
                    });
                }
            };
        });

        //{}:不繼承父域,建立獨立作用域
        app.directive('childC', function () {
            return {
                restrict: 'E',
                scope: {},
                template: function (elem, attr) {
                    return "{}:" + document.getElementById('t1').innerHTML;
                },
                controller: function ($scope) {
                    console.log($scope);
                }
            };
        });

    </script>
</body>

false引數

本質:子域與父域共享作用域。
特點:父域修改parentName的同時,指令繫結的parentName的元素會被重新整理。

反之,指令內部parentName被修改時,父域的parentName同樣會被重新整理。

true引數

本質:子域繼承父域,並建立獨立作用域。
特點:

1、在指令已宣告parentName的情況下,父域parentName變更,指令中parentName不會發生變化。
指令在true引數下,建立了的scope,獨立並隔離與父控制器的scope。

controller: function ($scope) {
    $scope.parentName = "parent";
}

反之,指令中parentName變更,父域也不會發生變化。

2、在指令未宣告parentName的情況下,父域的parentName變更,指令中parentName也會重新整理
這種情況很多時候會被忽略,指令的scope沒有宣告物件時,其元素繫結的仍然是父域的物件。但,一旦指令中Input變更了,對應的獨立scope也會自動宣告該繫結物件,這就回到了第1種情況。

controller: function ($scope) {
    //$scope.parentName = "parent";
}

然而,指令中parentName變更,父域是不會變化的;

3、在指令已宣告parentName的情況下 ,在指令中監聽父域parentName 的變化無效。但監聽子域parentName的變化有效
獨立子域scope,只能監聽自己的,不能監聽父域的。但通過 $scope.$parent可以監聽父域。

controller: function ($scope) {
    $scope.parentName = "parent" ;

    //已宣告的情況下,$scope.$watch監聽的是自己的parentName
    $scope.$watch( 'parentName' , function (n, o) {
        console.log("child watch" + n);
    });

    //$scope.$parent.$watch監聽的是父域的parentName
    $scope.$parent.$watch( 'parentName' , function (n, o) {
        console.log("parent watch" + n);
    });
}

4、在指令未宣告parentName的情況下 ,在指令中監聽父域parentName的變化有效。
這裡就不解釋了,參考第2點,大家可以動手試一下。

controller: function ($scope) {
    //$scope.parentName = "parent";

    //未宣告的情況下,$scope.$watch監聽的是父域的parentName
    $scope.$watch('parentName' , function (n, o) {
        console.log("child watch" + n);
    });
}

物件{}引數

本質:子域不繼承父域,並建立獨立作用域。
特點:

1、當scope物件為空物件時,無論是父域parentName,還是指令子域parentName發生變更,都不會影響到對方。
原理很清楚,就是指令建立的獨立作用域,與父域是完全隔離的。

scope: {}

2、當scope物件為非空物件時,指令會將該物件處理成子域scope的擴充套件屬性。而父域與子域之間傳遞資料的任務,就是可以通過這塊擴充套件屬性完成。

<div ng-controller='parentCtrl'>
    parent:
    <p><span>{{name}}</span><input type="text" ng-model="name" /></p>
    <p><span>{{sexy}}</span><input type="text" ng-model="sexy" /></p>
    <p><span>{{age}}</span><input type="text" ng-model="age" /></p>
    <br />

    <!--特別注意:@與=對應的attr,@是單向繫結父域的機制,記得加上{{}};&對應的attrName必須以on-開頭-->
    <child-c my-name="name" my-sexy-attr="sexy" my-age="{{age}}" on-say="say('i m ' + name)"></child-c>
</div>

<!--t1指令模板-->
<script type="text/html" id="t1">
    <div>
        <span>{{myName}}</span>
        <input type="text" ng-model="myName" />
    </div>
    <div>
        <span>{{mySexy}}</span>
        <input type="text" ng-model="mySexy" />
    </div>
    <div>
        <span>{{myAge}}</span>
        <input type="text" ng-model="myAge" />
    </div>
</script>

<script>
    var app = angular.module("app", []);

    app.controller('parentCtrl', function ($scope) {
        $scope.name = "mark";
        $scope.sexy = "male";
        $scope.age = "30";
        $scope.say = function (sth) {
            alert(sth);
        };
    })

    app.directive('childC', function () {
        return {
            restrict: 'E',
            scope: {
                myName: '=',
                mySexy: '=mySexyAttr',
                myAge: '@',
                onSay: '&'
            },
            template: function (elem, attr) {
                return "{}:" + document.getElementById('t1').innerHTML;
            },
            controller: function ($scope) {
                console.log($scope.myName);
                console.log($scope.mySexy);
                console.log($scope.myAge);
                $scope.onSay();
            }
        };
    });

</script>

@(or @Attr)繫結策略——本地作用域屬性,使用@符號將本地作用域同DOM屬性的值進行繫結。指令內部作用域可以使用外部作用域的變數。(單向引用父域物件)

<child-c my-age="{{age}}"></child-c>

ps:@ 是單向繫結本地作用域,記得加上{{}}

scope: {          
     myAge: '@',
}

= (or =Attr)繫結策略——雙向繫結:通過=可以將本地作用域上的屬性同父級作用域上的屬性進行雙向的資料繫結。就像普通的資料繫結一樣,本地屬性會反映出父資料模型中所發生的改變。(雙向引用父域物件)

<child-c onSay="name"></child-c>

ps:=策略不需要加上{{}}進行繫結

scope: {          
     myName: '=',
}

& (or &Attr)繫結策略——通過&符號可以對父級作用域進行繫結,以便在其中執行函式。(呼叫父域函式)

<child-c on-say="say('i m ' + name)"></child-c>

ps:&對應的attrName必須以on-開頭

scope: {          
     onSay: '&',
}

父域繫結呼叫函式及傳參


app.controller('parentCtrl', function ($scope) {
     $scope.say = function (sth) {
         alert(sth);
     };
})

ps特別注意:@=對應的attr,;

<child-c my-name="name" my-sexy-attr="sexy" my-age="{{age}}" on-say="say('i m ' + name)"></child-c>


總結下來,scope擴充套件物件,既能夠解耦父域與子域共域的問題,也能夠實現指令與外界通訊的問題,是Angular開發指令化模組化的重要基礎。在往後的章節,我會向大家介紹指令化開發的更多例項。

對外引數——require

scope是指令與外界作用域通訊的橋樑,而require是指令與指令之間通訊的橋樑。這個引數最大的作用在於,當要開發單指令無法完成,需要一些組合型指令的控制元件或功能,例如日期控制元件,通過require引數,指令可以獲得外部其他指令的控制器,從而達到交換資料、事件分發的目的。

使用方法:require: String or Array——String值為引入指令名稱,並且有兩個尋找指令策略符號‘’與‘^’;Array陣列則為多個外部指令名稱。

在link函式第4個引數ctrl中獲取注入外部指令的控制器,如果require為String,ctrl為物件,如果require是陣列,ctrl為陣列。

require: '^teacher1',
link: function ($scope, $element, $attrs, ctrl) {
    //ctrl指向teacher1指令的控制器
}

策略——尋找指令名稱,如果沒有找到,link函式第4個引數為null;如果沒有?,則報錯。

策略——在自身指令尋找指令名稱的同時,向上父元素尋找;如果沒有^,則僅在自身尋找。
如下例子,指令studentA向上可以找到指令teacher及自身,但是不能找到相鄰兄弟的student-b

<div teacher>
    <student-a></student-a>
    <student-b></student-b>
</div>

完整例子

<body>
    <div teacher>
        {{name}}
        <student-a></student-a>
        <student-b></student-b>
    </div>
    <script>
        var app = angular.module("app", []);

        //studentA——require指向父級指令teacher
        app.directive('studentA', function () {
            return {
                require: '?^teacher',
                scope: {},
                template: '<div>A`s teacher name: <span>{{teacherName}}</span></div>',
                link: function ($scope, $element, $attrs, ctrl) {
                    //獲取teacher指令控制器,並呼叫其方法sayName()
                    $scope.teacherName = ctrl.sayName();
                }
            };
        });

        //studentB——require指向父級指令teacher,及指令studentA
        //但是,由於不能獲得兄弟,也沒有采取?策略,導致報錯
        app.directive('studentB', function () {
            return {
                require: ['?^teacher', 'studentA'],
                scope: {},
                template: '<div>B`s teacher name: <span>{{teacherName}}</span></div>',
                link: function ($scope, $element, $attrs, ctrl) {
                    $scope.teacherName = ctrl.sayName();
                }
            };
        });

        app.directive('teacher', function () {
            return {
                restrict: 'A',
                controller: function ($scope) {
                    $scope.name = "Miss wang";

                    //擴充套件控制器的方法sayName,目的是讓外部內獲取控制器內部資料
                    this.sayName = function () {
                        return $scope.name;
                    };
                }
            };
        });
    </