针对全局对象、错误和表达式的服务

一、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>

程序运行结果为:

 

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

原文地址:https://www.cnblogs.com/YangqinCao/p/6019624.html