一、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>
字符串里,它是安全显示的:
<p>This is <b onmouseover=alert('Attack!')>dangerous</b> data</p>
每一个字符都被浏览器当做普通字符对待,因为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>
程序运行结果为:
如果不使用编译函数,即注销加粗的两行,结果是