9

上一章节,给大家回顾了一下AngularJS指令参数的基础使用。如果有纰漏,欢迎大家给我留言,相互探讨探讨。

指令化,其实本质就是代码的通用化与模块化,AngularJS的指令化工作,将逻辑与DOM都结合在一起,能够做到即插即用,与Asp的Component是相似的概念。

要做到模块化,必要的要求就是通用代码与业务代码的解耦。而解耦并不代表完全的隔绝,解耦要做的是,通用模块与业务模块的隔离,同时也保留接口提供两者通讯

啰嗦,赶紧实例吧!

某天,产品老大压下来需求,要做个学生信息填写卡,说白了就是一个表单编辑器,so easy,前端单身狗拍拍脑袋马上开工。

<body>
    <div ng-controller='demoCtrl'>
        <div class="panel">
            <h3>student card</h3>
            <p>
                <span>name:</span>
                <input type="text" ng-model="stu.name" />
            </p>
            <p>
                <span>sexy :</span>
                <select ng-model="stu.sexy">
                    <option value="1">male</option>
                    <option value="2">female</option>
                </select>
            </p>
            <p>......</p>
            <button class="btn btn-info" type="button" ng-click="saveEditing()">save</button>
        </div>
    </div>

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

        app.controller('demoCtrl', function ($scope) {
            $scope.stu = {
                name: "mark",
                sexy: 1
            };

            $scope.saveEditing = function () {
                //$http.post("...", { stu: $scope.guy });
                console.log($scope.guy.name + " is saving!");
                console.log($scope.guy);
            };
            
            //这里省略信息卡交互逻辑…… 
        });               
    </script>
</body>

出来结果,还行。

clipboard.png

隔了一天,产品老大想了想,要增强体验,在选择性别的时候,名称要根据男女颜色变化;输入名称的时候,要自动匹配近似名称,并且首字母大写。行,改呗。demoCtrl加上以下逻辑

$scope.nameChange = function () {
    //处理名称变更
};

$scope.sexyChange = function () {
    //处理性别变更
};

又隔了一天,产品老大终于定稿,这个学生信息填写卡,要应用到demo页、demo1页、demo2页上,而3个页面保存功能指向的后端接口都不一样。

前端单身狗:虽然不至于问候您大爷,但至少要好好考虑如何实现才省功夫吧。难道要把demoCtrl的相关代码都copy到另外两个页面吗?这个时候,只要是写过代码的同志都会say no吧!

赶紧指令化。

隔离

首先,把信息卡的DOM结构独立开来,创建指令student。指令scope参数要设为{},如果设为falsetrue达不到完全隔离的效果哦,不理解原因的童鞋请回顾上一章节。

<div ng-controller='demoCtrl'>
    <student></student>
</div>

<div ng-controller='demo1Ctrl'>
    <student></student>
</div>

<div ng-controller='demo2Ctrl'>
    <student></student>
</div>

<!--信息卡DOM模板->
<script type="text/html" id="t1">
    <div class="panel">
        <p>
            <span>name:</span>
            <input type="text" ng-model="stu.name" />
        </p>
        <p>
            <span>sexy :</span>
            <select ng-model="stu.sexy">
                <option value="1">male</option>
                <option value="2">female</option>
            </select>
        </p>
        <button class="btn btn-info" type="button" ng-click="onSave()">save</button>
    </div>
</script>

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

   app.directive('student', function () {
        return {
            restrict: 'E',
            scope: {},
            template: function (elem, attr) {
                return document.getElementById('t1').innerHTML;
            },
            controller:function($scope){
                //这里省略信息卡交互逻辑……                    
            }                
        };
    });
</script>

虽然独立了信息卡代码,但有两个问题是显而易见的

  1. 隔离以后,指令内部如何获得3个demoCtrl的stu?

  2. 保存按钮又是如何调用外部3个demoCtrl不同的save方法?

问题其实指明了解决思路,student需要两个接口与外部通讯:scope.stu & scope.save()

通讯

为大家介绍3种通讯方案。

scope {}

指令scope {}参数,可以完全隔离作用域,但是也预留了3种绑定策略,实现子域与父域通讯。

为指令建立两个通讯接口,stu采取=双向绑定父域对象的策略;而onSave则采取$反向调用父域函数策略。

scope: {
    stu: '=',      //=为双向绑定策略
    onSave: '&'    //$反向调用父域函数策略
},

优点:简单、简洁
缺点:

  1. 通讯接口要求比较多、复杂的情况下,指令scope {}要配置的绑定策略也比较多;

  2. 造成指令与指令之间的通讯容易混乱;

  3. 指令内部好像没有办法,往onSave()函数里面传参,这个不确定,求助大家。

DOM写法:

<student stu="guy" on-save="saveEditing()"></student>

完整例子:

<body>
    <div ng-controller='demoCtrl'>
        <!--指令scope.stu双向绑定demoCtrl scope.guy-->
        <!--指令scope.onSave函数指向demoCtrl scope.saveEditing()-->
        <student stu="guy" on-save="saveEditing()"></student>
    </div>

    <script type="text/html" id="t1">
        <div class="panel">
            <p>
                <span>name:</span>
                <input type="text" ng-model="stu.name" />
            </p>
            <p>
                <span>sexy :</span>
                <select ng-model="stu.sexy">
                    <option value="1">male</option>
                    <option value="2">female</option>
                </select>
            </p>
            <button class="btn btn-info" type="button" ng-click="onSave()">save</button>
        </div>
    </script>

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

        app.controller('demoCtrl', function ($scope) {
            $scope.guy = {
                name: "mark",
                sexy: 1
            };

            $scope.saveEditing = function () {
                //$http.post("...", { stu: $scope.guy });
                console.log($scope.guy.name + " is saving!");
                console.log($scope.guy);
            };
        });

        app.directive('student', function () {
            return {
                restrict: 'E',
                scope: {
                    stu: '=',      //=为双向绑定策略
                    onSave: '&'    //$反向调用父域函数策略
                },
                template: function (elem, attr) {
                    return document.getElementById('t1').innerHTML;
                },
                controller: function ($scope, $element, $attrs, $transclude) {
                    //这里省略信息卡交互逻辑……
                }
            };
        });
    </script>
</body>

效果:

clipboard.png

ngModel

ngModel,这个内置指令相信大家都不会陌生。自定义指令引用其自身的ngModel指令,其原理就是:

  • 父域对象 绑定 ngModel

  • ngModel 绑定 指令子域对象

优点:可以充分利用ngModel的特性,例如commit、rollback等特性,也可以搭配ng-model-options进行使用。

缺点:

  1. 相对于方案1,父子对象绑定中间还要多一层ngModel的绑定,性能必然降低;

  2. 相对于方案1,使用比较麻烦;

  3. 自身不能实现反向调用父域函数,需要借助$parse转换表达式方案实现

DOM写法:

<student ng-model="guy" on-save="saveEditing()"></student>

完整例子:

<body>
    <div ng-controller='demoCtrl'>
        <student ng-model="guy" on-save="saveEditing()"></student>
    </div>

    <script type="text/html" id="t1">
        <div class="panel">
            <p>
                <span>name:</span>
                <input type="text" ng-model="ngModel.$modelValue.name" />
            </p>
            <p>
                <span>sexy :</span>
                <select ng-model="ngModel.$modelValue.sexy">
                    <option value="1">male</option>
                    <option value="2">female</option>
                </select>
            </p>
            <button class="btn btn-info" type="button" ng-click="onSave()">save</button>
        </div>
    </script>

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

        app.controller('demoCtrl', function ($scope) {
            $scope.guy = {
                name: "mark",
                sexy: 1
            };

            $scope.saveEditing = function () {
                //$http.post("...", { stu: $scope.guy });
                console.log($scope.guy.name + " is saving!");
                console.log($scope.guy);
            };
        });

        app.directive('student', ["$parse", function ($parse) {
            return {
                restrict: 'E',
                require: ['student', 'ngModel'],
                scope: {},
                template: function (elem, attr) {
                    return document.getElementById('t1').innerHTML;
                },
                link: function (scope, element, attr, ctrls) {
                    //link阶段,通过require获取指令自身的控制器,及ngModel指令的控制器
                    var stCtrl = ctrls[0];
                    var ngModelCtrl = ctrls[1];

                    //并将ngModel指令的控制器,通过自身控制器的init()方法传入到其中
                    stCtrl.init(ngModelCtrl);
                },
                controller: function ($scope, $element, $attrs, $transclude) {
                    //创建controller的对外初始化方法,并将外部ngModel的控制器设置本地作用域对象
                    this.init = function (ngModelCtrl) {
                        $scope.ngModel = ngModelCtrl;
                    };

                    //获得on-save属性指向的表达式{{saveEditing()}}
                    var saveInvoker = $parse($attrs.onSave);

                    $scope.onSave = function () {
                        //在父域中,执行表达式{{saveEditing()}}——执行父域saveEditing()
                        saveInvoker($scope.$parent, null);
                    };
                }
            };
        }]);


    </script>
</body>

$parse

在ngModel的解决方案中,已经有过通过$parse获取执行表达式操作父域函数的例子:$parse($attrs.onSave)。这个方案不仅能够执行父域函数表达式,同时也能够执行对象的get/set表达式。但是本方案本质其实是对于父域$scope.$parent的直接操作,只是通过$parse服务实现解耦。

关于$parse服务,想了解更多,请移步——
http://segmentfault.com/a/1190000002749571

<body>
    <div ng-controller='demoCtrl'>
        <student stu="guy" on-save="saveEditing()"></student>
    </div>

    <script type="text/html" id="t1">
        <div class="panel">
            <p>
                <span>name:</span>
                <input type="text" ng-model="stu.name" />
            </p>
            <p>
                <span>sexy :</span>
                <select ng-model="stu.sexy">
                    <option value="1">male</option>
                    <option value="2">female</option>
                </select>
            </p>
            <button class="btn btn-info" type="button" ng-click="onSave()">save</button>
        </div>
    </script>

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

        app.controller('demoCtrl', function ($scope) {
            $scope.guy = {
                name: "mark",
                sexy: 1
            };

            $scope.saveEditing = function () {
                //$http.post("...", { stu: $scope.guy });
                console.log($scope.guy.name + " is saving!");
                console.log($scope.guy);
            };
        });

        app.directive('student', ["$parse", function ($parse) {
            return {
                restrict: 'E',
                scope: {},
                template: function (elem, attr) {
                    return document.getElementById('t1').innerHTML;
                },
                controller: function ($scope, $element, $attrs, $transclude) {
                    var getClassName,
                        setClassName,
                        saveInvoker = angular.noop;

                    //注意:建议查阅一下$parse内置表达式转换函数的使用方法。

                    //获得stu属性指向的表达式{{guy}}
                    getStudent = $parse($attrs.stu);
                    setStudent = getStudent.assign;

                    //获得on-save属性指向的表达式{{saveEditing()}}
                    saveInvoker = $parse($attrs.onSave);

                    //监听父域的{{guy}}
                    $scope.$parent.$watch(getStudent, function (stu) {
                        $scope.stu = stu;
                    });

                    $scope.onSave = function () {
                        //在父域中,执行表达式{{guy}} assign——将本地对象stu设置到父域guy
                        setStudent($scope.$parent, $scope.stu);

                        //在父域中,执行表达式{{saveEditing()}}——执行父域saveEditing()
                        saveInvoker($scope.$parent, null);
                    };
                }
            };
        }]);


    </script>
</body>

效果:

clipboard.png

End

说了一大堆,大家还是动手试试哪个方案更合适你的项目,或者有更好的开发思路。

下一章节,为大家介绍一下关于指令的自动化测试。欢迎继续捧场。


Tgor
1.3k 声望57 粉丝

井底之蛙


引用和评论

0 条评论