zoukankan      html  css  js  c++  java
  • 针对全局对象、错误和表达式的服务

    一、DOM API全局对象

    暴露DOM API特性的服务

    $anchorScroll 滚动浏览器窗口到指定的锚点

    $document 提供jqLite对象包括DOM window.document对象

    $location 提供URL入口

    $log 提供围绕console对象的封装

    $timeout 提供围绕window.setTimeout函数的增强封装

    $window 提供DOM window对象的引用

    1.1 为什么使用以及何时使用全局对象服务

    AngularJS包含这些服务的主因是使测试更简单。单元测试的一个重要方面是隔离一小段代码,并且测试它的行为而无需测试它所依赖的组件。DOM API通过全局对象暴露接口,比如document和window。这些对象使其难以为了单元测试分离代码,还没有测试浏览器实现它的全局对象的方法。使用诸如$document这样的服务,使得不直接使用DOM API全局对象也可以写AngularJS的代码,也允许使用AngularJS的代码,也允许使用AngularJS测试服务来配置指定的测试场景。

    1.2 访问window对象

    $window服务使用起来很简单,声明依赖于它给你的对象,它是围绕全局window对象的封装。AngularJS不增强或改变由全局对象提供的API,加入你直接使用过DOM API,你能像你会的那样访问window对象定义的方法。

    angular.module("exampleApp", [])
            .controller("defaultCtrl", function ($scope, $window) {
                $scope.displayAlert = function(msg) {
                    $window.alert(msg);
                }
            });
    // 这里不是用$window服务,直接使用alert(msg)也可以实现同样的效果,但是使用$window服务可以方便测试

    1.3 访问document对象

    $document服务是一个包含DOM API全局window.document对象的jqLite对象。由于该服务通过jqLite呈现,你可以使用它来查询DOM。

    angular.module("exampleApp", [])
            .controller("defaultCtrl", function ($scope, $window, $document) {
                $document.find("button").on("click", function (event) {
                    $window.alert(event.target.innerText);
                });
            });

    1.4 使用interval和timeout

    $interval和$timeout服务提供访问window.setInterval和window.setTimeout函数的入口,以及一些增强功能,使其更好地与AngularJS协作。

    $interval $timeout服务使用的参数

    fn 定时执行的函数

    delay fn被执行前的毫秒数

    count 定时/执行循环将重复的次数(仅$interval)。默认是0,意为没有限制。

    invokeApply 当设置默认值true时,fn将与scope.$apply方法一同执行

    angular.module("exampleApp", [])
            .controller("defaultCtrl", function ($scope, $interval) {
                $interval(function () {
                    $scope.time = new Date().toTimeString();
                }, 2000);
            });

    1.5 访问URL

    $location服务是围绕全局window对象的Location属性的封装,提供了访问当前URL的入口。$location服务操作第一个#号后面的URL部分,这意味着它可以用于当前文档的导航,而不导航到新文件中。

    URL: http://mydomain.com/app.html#/cities/london?select=hotels#north

    path: cities/london

    主机Host: mydomain.com

    search: select=hotels

    hash: north

     

    $location服务所定义的方法:

    absUrl()  返回当前文档的完整URL,包括第一个#号之前的部分(http://mydomain.com/app.html#/cities/london?select=hotels#north)

    hash() hash(target) 获取或设置URL的散列部分

    host() 返回完整的URL的主机名称

    path() path(target) 获取或设置完整的URL路径

    port() 返回端口号

    protocol() 返回完整的URL的协议(http)

    replace() 当在HTML5浏览器中被调用时,URL的变化代替了浏览器历史记录中的最新条目,而不是创建一条新的记录

    search() search(term,params) 获取或设置搜索项

    url() url(target) 获取或设置路径、查询字符串和散列集

     

    此外。$location服务还定义了两个事件,当URL改变时或者由于用于交互或编程方式改变,你可以使用它们接受通知

    $locationChangeStart URL被改变前触发。你可以在Event对象中调用preventDefault方法来阻止URL改变。

    $locationChangeSuccess URL被改变后触发

    $scope.setUrl = function (component) {
                    switch (component) {
                        case "reset":
                            $location.path("");
                            $location.hash("");
                            $location.search("");
                            break;
                        case "path":
                            $location.path("/cities/london");
                            break;
                        case "hash":
                            $location.hash("north");
                            break;
                        case "search":
                            $location.search("select", "hotels");
                            break;
                        case "url":
                            $location.url("/cities/london?select=hotels#north");
                            break;
                    }
                }

     

    使用HTML5 MRUL

    前面展示的是标准的URL,格式是杂乱的,因为应用程序本质上是视图复制#号之后的URL部分,使得浏览器不载入新HTML文档。

    HTML5的History API提供了更优雅的方式来处理这点,并且能改变URL,而不导致文档重载。所有主流浏览器的最新版本都支持History API,而且它的支持可以在AngularJS应用程序中通过$location服务的提供器,$locationProvider启用。

    angular.module("exampleApp", [])
            .config(function($locationProvider) {
                $locationProvider.html5Mode(true);
            })

     

    启用后,仍然用上面的setUrl函数改变URL http://mydomain.com/app.html#/cities/london?select=hotels#north

    URL: http://localhost:5050/cities/london?select=hotels#north

    这是一个更清晰的URL结构,不过它当然依靠的是HTML5特性,在旧浏览器中是不可用的,而且如果使用$location 的HTML5模式,那么你的应用程序将在不支持History API的浏览器中无法工作。

    测试History API的存在

    angular.module("exampleApp", [])
            .config(function ($locationProvider) {
                if (window.history && history.pushState) {
                    $locationProvider.html5Mode(true);
                }
            })

     

     我不得不直接使用两个全局对象,因为只能将常量和提供器注入到config函数中,也就是说我无法使用$window服务。如果浏览器有定义window.history和history.pushState方法,那我就可以使用$location服务的HTML5模式,并且从改良的URL结构中获益。

     

    1.6 滚动到$location散列的位置

    $anchorScroll服务滚动浏览器窗口到显示id与$location.hash方法返回值一致的元素处。

    angular.module("exampleApp", [])
            .controller("defaultCtrl", function ($scope, $location, $anchorScroll) {
                $scope.itemCount = 50;
                $scope.items = [];
    
                for (var i = 0; i < $scope.itemCount; i++) {  // 生成较长的文字
                    $scope.items[i] = "Item " + i;
                }
                
                $scope.show = function(id) {  // 主体
                    $location.hash(id);
                }
            });

    $anchorScroll服务非同寻常,因为你并非一定要使用服务对象,你仅需要声明依赖。当创建服务对象时,他就开始监听$location.hash值,然后在其改变时自动滚动。

    通过服务提供器禁用自动滚动,它允许你调用$anchorScroll服务作为函数来选择性地滚动。

    angular.module("exampleApp", [])
            .config(function ($anchorScrollProvider) {
                $anchorScrollProvider.disableAutoScrolling(); // 在config方法中调用禁用自动滚动,改变$location.hash的值将不再触发自动滚动
            })
            .controller("defaultCtrl", function ($scope, $location, $anchorScroll) {
    
                $scope.itemCount = 50;
                $scope.items = [];
    
                for (var i = 0; i < $scope.itemCount; i++) {
                    $scope.items[i] = "Item " + i;
                }
                    
                $scope.show = function(id) {
                    $location.hash(id);
                    if (id == "bottom") {
                        $anchorScroll(); // 要明确地触发滚动,当传到行为show的参数为bottom时,我调用$anchorScroll服务函数,实现滚动
                    }
                }
            });

    程序的效果是:两个按钮,页面上部的按钮id为top,ng-click=show(bottom),页面下部按钮id为bottom,ng-click=show(top)

    如果单击id为top的按钮,触发show(bottom),滚动到页面下方按钮

    单击id为bottom的按钮,触发show(top),不能实现滚动

     

    1.7 日志服务

    $log服务,围绕全局console对象封装。$log服务定义了debug,error,info,log和warn方法,与console对象定义的那些一致。

    angular.module("customServices", [])
        .factory("logService", function ($log) {
            var messageCount = 0;
            return {
                log: function (msg) {
                    $log.log("(LOG + " + this.messageCount++ + ") " + msg);
                }
            };
        });

    $log服务的默认行为不是调用debug方法到控制台。你可以通过设置$logProvider.debugEnabled属性为true启用调试。

    二、异常处理

    $exceptionHandler服务处理任何在应用程序执行时出现的异常。默认实现是调用$log服务定义的error方法,其中调用了全局的console.error方法。$exceptionHandler服务仅处理未捕获的异常。你可以使用js的try...catch块来捕获异常,它将不被服务处理。

    尽管AngularJS会自动传入异常到$exceptionHandler服务,你可以提供更多上下文,在你的代码中使用该服务。

    angular.module("exampleApp", [])
            .controller("defaultCtrl", function ($scope, $exceptionHandler) {
                $scope.throwEx = function () {
                    try {
                        throw new Error("Triggered Exception");
                    } catch (ex) {
                        $exceptionHandler(ex.message, "Button Click");
                    }
                }
            });

     

    $exceptionHandler服务对象是有两个参数的函数:异常和可选字符串,用于描述异常的原因。

    程序运行结果:Triggered Exception Button Click

     

    实现自定义异常处理器

    angular.module("exampleApp", [])
            .controller("defaultCtrl", function ($scope, $exceptionHandler) {
                $scope.throwEx = function () {
                    try {
                        throw new Error("Triggered Exception");
                    } catch (ex) {
                        $exceptionHandler(ex, "Button Click");
                    }
                }
            })
            .factory("$exceptionHandler", function ($log) {
                return function (exception, cause) {
                    $log.error("Message: " + exception.message + " (Cause: " + cause + ")");
                }
            });

     

    运行结果:Message:Triggered Exception(Cause:Button Click);

     

    三、处理危险数据

    操作危险数据的服务:

    $sce 从HTML中移除危险元素和属性

    $sanitize 将HTML字符串中的危险字符替换为与之对应的转义字符

    3.1 显示危险数据

    AngularJS使用叫做严格上下文转义SCE的特性,预防不安全的值通过数据绑定被展现出来。

    <script>
            angular.module("exampleApp", [])
            .controller("defaultCtrl", function ($scope) {
                $scope.htmlData 
                    = "<p>This is <b onmouseover=alert('Attack!')>dangerous</b> data</p>";            
            });
    </script>
    
    <body ng-controller="defaultCtrl">
        <div class="well">
            <p><input class="form-control" ng-model="htmlData" /></p>
            <p>{{htmlData}}</p>
        </div>
    </body>

    我已将属性设置为危险的HTML字符串,这样你就不需要手动输入文本。AngularJS自动将危险符号(像HTML内容里的<和>)替换为安全显示的对应转义字符。

    AngularJS从input元素中转换了该HTML字符串:

    <p>This is <b onmouseover=alert('Attack!')>dangerous</b> data</p>

    字符串里,它是安全显示的:

    &lt;p&gt;This is &lt;b onmouseover=alert('Attack!')&gt;dangerous&lt;/b&gt; data&lt;/p&gt;

    每一个字符都被浏览器当做普通字符对待,因为HTML已经被替换为安全的代替字符

    3.2 使用不安全的绑定

     第一个技术是使用ng-bind-html指令,它允许你指定某个数据的值是可信的,并应该不被转义的呈现出来。ng-bind-html指令依赖于ngSanitize模块,主AngularJS库没有包含它。需要自己下载,并用script引用。

    <head>
        <title>SCE</title>
        <script src="angular.js"></script>
        <script src="angular-sanitize.js"></script>
        <link href="bootstrap.css" rel="stylesheet" />
        <link href="bootstrap-theme.css" rel="stylesheet" />
        <script>
            angular.module("exampleApp", ["ngSanitize"])
            .controller("defaultCtrl", function ($scope) {
                $scope.htmlData
                    = "<p>This is <b onmouseover=alert('Attack!')>dangerous</b> data</p>";
            });
        </script>
    </head>
    <body ng-controller="defaultCtrl">
        <div class="well">
            <p><input class="form-control" ng-model="htmlData" /></p>
            <p ng-bind-html="htmlData"></p>
        </div>
    </body>

     

    虽然内容显示了HTML,但我在b元素上使用的onmouseover事件处理器不工作了,那是因为在这里还有一个安全措施,从HTML字符串中剔除了危险元素和属性。该过程删除script和css元素、内联JS事件处理器和样式属性以及可能造成问题的任何东西。这种处理被称为净化(sanitization),由ngSanitize模块的$sanitize服务提供。$sanitize服务自动被用于ng-bind-html指令,这就是我在例子中添加该模块的原因。

     

    立即净化

    依靠AngularJS,你可以为其呈现的值使用$sanitize服务。

    angular.module("exampleApp", ["ngSanitize"])
            .controller("defaultCtrl", function ($scope, $sanitize) {
                $scope.dangerousData
                    = "<p>This is <b onmouseover=alert('Attack!')>dangerous</b> data</p>";
    
                $scope.$watch("dangerousData", function (newValue) {
                    $scope.htmlData = $sanitize(newValue);
                });
            });

    $sanitize对象是个函数,它能去除潜在危险的值并返回洁净的结果。

    跟第一个程序相比,你会看到净化处理从我输入到Input元素的字符串中删除了JS事件处理器,该值没有作为HTML显示,因为Angular仍然转义了危险字符。

     

    3.3 明确信任的数据

    在极其少的情况下,你可能需要显示没有转义或净化的潜在危险内容。你可以使用$sce服务声明内容时可信的。$sce服务对象定义了trustAsHtml方法,它返回一个值,将伴随着被使用的SCE(上下文转义)过程一起显示。

    <script>
            angular.module("exampleApp", ["ngSanitize"])
            .controller("defaultCtrl", function ($scope, $sce) {
                $scope.htmlData
                    = "<p>This is <b onmouseover=alert('Attack!')>dangerous</b> data</p>";
    
                $scope.$watch("htmlData", function (newValue) {
                    $scope.trustedData = $sce.trustAsHtml(newValue);
                });
            });
    </script>
    
    <body ng-controller="defaultCtrl">
        <div class="well">
            <p><input class="form-control" ng-model="htmlData" /></p>
            <p ng-bind-html="trustedData"></p>
        </div>
    </body>

     我使用监听器函数来设置trustedData属性为$sce.trustAsHtml方法返回的结果。我仍旧必须使用ng-bind-html指令作为HTML显示该值,而不是被转义的问了。信任的数据值由于删除而阻止了JS事件的处理器,并使用ng-bind-html指令阻止了字符的转义。其结果是浏览器显示了来自input元素并被JS处理的内容。如果你移动鼠标触碰加粗文字,会看到警告窗口显示。

     

    如果把ng-bind-html改成ng-bind结果为:

     

     

    总结:

     ng-bind和{{}}不使用服务或者使用了$sce.trustAsHtml方法,AngularJS都会将字符串转义显示,一些JS绑定,CSS样式都是按字符串显示

     ng-bind和{{}}如果使用了$sanitize净化服务,AngularJS仍然会将字符串转义,但是JS绑定,CSS样式那些都会被过滤掉

     ng-bind-html如果不使用服务,AngularJS不会转义字符串,但是JS绑定和CSS样式会被过滤掉

     ng-bind-html使用了$sce.trustAsHtml方法,则不会转义字符串,并且JS绑定和CSS样式都会生效。

     

    四、使用AngularJS表达式和指令

    操作AngularJS表达式的服务

    $compile 将包含绑定和指令的HTML片段转换为被调用的函数生成内容

    $interpolate 将包含内联绑定的字符串转换为能被调用的函数生成内容

    $parse 将AngularJS表达式转换为能被调用的函数生成内容

    4.1 转换表达式为函数

    $parse服务传入AngularJS表达式,并转换它为函数,你可以使用该函数求得使用作用域对对象的表达式的值。在自定义指令中这是可用的,它允许表达式由属性和计算提供,而这些指令不需要知道表达式的细节。

    angular.module("exampleApp", [])
            .controller("defaultCtrl", function ($scope) {
                $scope.price = "'hello'+100.23";
            })
            .directive("evalExpression", function ($parse) {
                return function(scope, element, attrs) {
                    scope.$watch(attrs["evalExpression"], function (newValue) {
                        try {
                            var expressionFn = $parse(scope.expr);
                            var result = expressionFn(scope);
                            if (result == undefined) {
                                result = "No result";
                            }
                        } catch (err) {
                            result = "Cannot evaluate expression";
                        }
                        element.text(result);
                    });
                }
            });
    <body ng-controller="defaultCtrl">
        <div class="well">
            <p><input class="form-control" ng-model="expr" /></p>
            <div>
                Result: <span eval-expression="expr"></span>
            </div>
        </div>
    </body>

     

    如果在Input框输入price|currency,显示result: $100.23

    使用$parse服务的过程很简单,服务对象是个函数,它唯一的参数是个将被求值的表达式,并且返回当你准备执行求值时所使用的函数。换言之,$parse服务不计算表达式本身,他是做实际工作的函数的工厂。 

    $parse(expression)

    returnsFunc(context,locals)

    context:对象,针对你需要解析的语句,这个对象中含有你需要解析的语句中的表达式,通常是一个scope object

    locals:对象,关于context中变量的本地变量,对于覆盖context中的变量值很有用

    angular.module("myApp",[])
        .controller("MyCtrl",function($scope,$parse){
            $scope.context={
                  add:function(a,b){return a+b;}
                  mul:function(a,b){return a*b;}
             };
             $scope.expression="mul(a,add(b,c))";
             $scope.data={
                  a:3,b:6,c:9
               };
             var parseFunc=$parse($scope.expression);
             $scope.parseValue=parseFunc($scope.context,$scope.data);
        })    
    结果为45

     

    4.2 插入字符串

    $interpolate服务和他的提供器$interpolateProvider,用于配置AngularJS执行内插方式,将表达式插入字符串的过程。$interpolate服务比$parse更灵活,因为它能和包含表达式的字符串一起工作,而不仅仅是表达式自身。

    angular.module("exampleApp", [])
            .controller("defaultCtrl", function ($scope) {
                $scope.dataValue = "100.23";
            })
            .directive("evalExpression", function ($interpolate) {
                var interpolationFn= $interpolate("The total is: {{amount | currency}} (including tax)");
                return {
                    scope: {
                        amount: "=amount",
                        tax: "=tax"
                    },
                    link: function (scope, element, attrs) {
                        scope.$watch("amount", function (newValue) {
                            element.text(interpolationFn(localData));
                        });
                    }
                }            
            });

     

    使用$interpolate服务比使用$parse简单,虽然有一些很重要的差异。第一个(最明显的)不同是$interpolate服务能操作包含非AngularJS内容与内联绑定混合的字符串。实际上,{{和}}字符表示内联绑定被称为内插字符。第二个不同是你无法提供作用域和本地数据给$interpolate服务创建的内插函数。反而,你必须确保你的表达式所需数值被包含在你传入内插函数的对象中。

    配置内插

    AngularJS并不是唯一使用{{和}}字符的库,而如果你试图混合AngularJS与另外的包,这可是个问题。不过你可以改变AngularJS用于内插的字符,通过$interpolate服务的提供器$interpolateProvider,方法如下:

    startSymbol 替换起始符号,{{是默认的

    endSymbol 替换结束符号,}}是默认的

    使用这些方法时必须小心,因为他们会影响所有AngularJS的内插,包括在HTML标签中的内联数据绑定。

    angular.module("exampleApp", [])
            .config(function($interpolateProvider) {
                $interpolateProvider.startSymbol("!!");
                $interpolateProvider.endSymbol("!!");
            })
            .controller("defaultCtrl", function ($scope) {
                $scope.dataValue = "100.23";
            })
            .directive("evalExpression", function ($interpolate) {
                var interpolationFn
                    = $interpolate("The total is: !!amount | currency!! (including tax)");
                return {
                    scope: {
                        amount: "=amount",
                        tax: "=tax"
                    },
                    link: function (scope, element, attrs) {
                        scope.$watch("amount", function (newValue) {
                            element.text(interpolationFn(scope));
                        });
                    }
                }
            });
    <div class="well">
            <p><input class="form-control" ng-model="dataValue" /></p>
            <div>
                <span eval-expression amount="dataValue" tax="10"></span>
                <p>Original amount: !!dataValue!!</p>
            </div>
    </div>
    <p>Hello world !!dataValue!!</p>

     

    正规内联绑定是被AngularJS使用$interpolate服务处理的,由于服务对象是单例的,任何配置的改变都将覆盖整个模块。

     

    4.3 编译内容

    $compile服务处理包含绑定与表达式的HTML片段,他将创建可利用作用域生成内容的函数。这相当于$parse和$interpolate服务,但不支持指令。调用编译函数是没有返回值的。

    angular.module("exampleApp", [])
            .controller("defaultCtrl", function ($scope) {
                $scope.cities = ["London", "Paris", "New York"];
            })
            .directive("evalExpression", function($compile) {
                return function (scope, element, attrs) {
                    var content = "<ul><li ng-repeat='city in cities'>{{city}}</li></ul>"
                    var listElem = angular.element(content);
                    var compileFn = $compile(listElem);
                    compileFn(scope);
                    element.append(listElem);
                }
            });
    <body ng-controller="defaultCtrl">
        <div class="well">
            <span eval-expression></span>
        </div>
    </body>

    程序运行结果为:

     

    如果不使用编译函数,即注销加粗的两行,结果是

  • 相关阅读:
    React Native 实现MQTT 推送调研 (1)
    bpmn的依赖注入
    vdom diff
    浏览器渲染与event loop
    uni-app 通过后缀名区分不同渠道版本
    网页定宽 栅格布局
    网页顶部菜单导航和左侧菜单导航的区别?
    Android利用tcpdump和wireshark抓取网络数据包
    就算做了
    我眼中的Serverless
  • 原文地址:https://www.cnblogs.com/YangqinCao/p/6019624.html
Copyright © 2011-2022 走看看