【AngularJS的概念及其单元测试】之过滤器

一、过滤器的基本概念

AngularJS的过滤器用于处理数据,以及将数据格式化后呈现给用户。一般用于HTML文档的表达式中,或直接用于控制器与服务中的数据。使用过滤器的好处是可以将常见的格式化操作和转换逻辑封装在单独的可重用组件中。

在HTML中使用过滤器的语法是管道式语法(pipe syntax):{ {expression | filter} }
也可以链式使用多个过滤器,将过滤的结果传递给下一个过滤器:{ {expression | filter1 | filter2} }
如将obj.name变量的值作以下处理,先转换成小写,并且只显示前五个字符:

{ {obj.name | lowercase | limitTo: 5} }

其中,obj.name的值是不会改变的。

常用的内置过滤器

1. currency

函数源码:

currencyFilter.$inject = ['$locale'];
function currencyFilter($locale) {
 	var formats = $locale.NUMBER_FORMATS;
	return function(amount, currencySymbol){
		if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM;		// 其中默认的formats.CURRENCY_SYM 为 '$'
	    return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2).replace(/\u00A4/g, currencySymbol);
	};
}

第二个参数currencySymbol是可选的,代表货币符号,没有指定则使用默认的'$'。

2. number

函数源码:

numberFilter.$inject = ['$locale'];
function numberFilter($locale) {
	var formats = $locale.NUMBER_FORMATS;
	return function(number, fractionSize) {
		return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP,fractionSize);
	};
}

通过添加分割符来将数字转换成易读的格式。也可接受一个参数fractionSize来决定保留小数点后几位。(fraction,分数)

3. lowercase/uppercase

函数源码:

function valueFn(value) {return function() {return value;};}
var lowercase = function(string){return isString(string) ? string.toLowerCase() : string;};
var uppercase = function(string){return isString(string) ? string.toUpperCase() : string;};

var lowercaseFilter = valueFn(lowercase);
var uppercaseFilter = valueFn(uppercase);

最终的实际结果是 isString(string) ? string.toLowerCase() : string;
其中isString是AngularJS自定义的一个公共API,源码很简单,只是做个类型判断:

function isString(value){return typeof value === 'string';}

4. json

函数源码:

function jsonFilter() {
	return function(object) {
		return toJson(object, true);
	};
}

其中,toJson也是AngularJS自定义的一个公共API,官方介绍是:

Serializes input into a JSON-formatted string. Properties with leading $ characters will be stripped since angular uses this notation internally.
将输入序列化为JSON格式的字符串。(将对象解析成json)因为Angular内部使用了\(符号,所以以\)开头的属性将会被剥离掉(即不保留这个属性)。

实现源码:

function toJson(obj, pretty) {
	if (typeof obj === 'undefined') return undefined;
	return JSON.stringify(obj, toJsonReplacer, pretty ? '  ' : null);
}

使用方式:

angular.toJson(obj, [pretty]);

参数:
obj Object | Array | Date | String | Number
pretty(optional) : Boolean
-- If set to true, the JSON output will contain newlines and whitespace.
如果将第二个参数pretty设置为true,那么就会保留对象中应有的换行和空格(不一定按照原始对象书写的格式)。

var obj = {
	$name: 'lau',
	age: 18
};

angular.toJson(obj);    // '{"age": 18}'	不保留以$开头的属
angular.toJson(obj, true);
// 结果如下
'{
	"age": 18
}'

5. date

日期格式过滤器是高度自定义的,是一个功能非常强大的过滤器,可以接收一个日期对象或代表日期的长整型,然后将数据转换成可读的字符串显示在视图中。
函数接口:

function(date, format) {}

6. limitTo

函数接口:

function limitToFilter(){
  	return function(input, limit) {}
}

接受字符串或数组,然后根据开始索引或结束索引返回输入值的子集。
当接受的参数是一个数字时,则当输入值是数组时,它返回相应的元素个数,当输入值是字符串,返回相应的字符个数。如果参数是负数,那么会从后往前数。

{ {'greeting' | limitTo: 3} }	// 'gre'
{ {[1, 2, 3, 4, 5] | 3} }		// [1, 2, 3]

7. orderBy

函数接口:

function orderByFilter($parse){
	return function(array, sortPredicate, reverseOrder) {}
}

这是一个比较复杂的过滤器,可以根据事先定义好的比较大小表达式 [或一组表达式] 将数组进行排序。第二个参数是一个可选的布尔型,表示数组是否需要进行反序。

<ul>
	<li ng-repeat="note in notes | orderBy: sortOrder">
		{ {note.name} } - { {note.location} }
	</li>
</ul>
$scope.notes = [
		{name: 'zhangjiang', location: 'shanghai'},
		{name: 'huangshan', location: 'anhui'},
		{name: 'dali', location: 'yunnan'},
		{name: 'dali', location: 'china'},
		{name: 'dali', location: 'earth'}
	];
$scope.sortOrder = ['+name', '-location'];

最简单的比较大小表达式是一个字符串(它是一个对象的键),根据这个字段进行排名。也可以在字段名之前添加+-符号表示按照升序还是降序排列。
还可以是函数,根据函数的返回值判定比较结果(通过简单的<>=进行比较)

8. filter

函数接口:

function filterFilter() {
	return function(array, expression, comparator) {}
}

filter是Angular中最复杂的过滤器。通过断言或函数来决定数组中哪些元素是符合要求的,将添加到结果集中,而哪些是将会被过滤掉的 —— 通常与ng-repeat配合过滤过滤数组元素。

过滤表达式:

  • string
    AngularJS会扫描数组中的每个对象的键值,如果其中包含指定的字符串,则这个元素就符合要求。如果要取相反的结果集,可以再表达式前加 前缀。

  • object
    AngularJS会扫描数组中的每个对象的键值,对于比如{size: 'M'},AngularJS会查找每个对象中是否包含了size这个键名,而它的值中是否包含了M这个字符(不一定正好是M)。

  • function
    使用函数制定过滤规则是功能最强大、最灵活的选项。function过滤器具有高度的扩展性,能够根据业务逻辑处理许多复杂的情况。
    数组中的每一个元素都会调用一次这个过滤函数,返回false的结果即该元素将会被过滤掉。

<button ng-click="currentFilter = 'string'">Filter with String</button>
<button ng-click="currentFilter = 'object'">Filter with Object</button>
<button ng-click="currentFilter = 'function'">Filter with Function</button>
<ul>
	<li ng-repeat="note in notes | filter: filterOptions[currentFilter]">
		{ {note.name} } - { {note.location} }
	</li>
</ul>
$scope.notes = [
	{name: 'zhangjiang', location: 'shanghai'},
	{name: 'huangshan', location: 'anhui'},
	{name: 'dali', location: 'yunnan'},
	{name: 'dali', location: 'china'},
	{name: 'dali', location: 'earth'}
];

$scope.currentFilter = 'string';
$scope.filterOptions = {
	'string': 'zhang',
	'object': {name: 'dali', location: 'n'},
	'function': function(note) {
		return note.name === 'dali';
	}
};

不同按钮的显示结果是:

在控制器和服务中使用过滤器

AngularJS能够通过依赖注入在任何地方使用过滤器。这样,我们不需要访问DOM节点和UI就可以根据业务逻辑需求在Javascript代码中使用过滤器了。

使用方式:任何过滤器(无论是内置的还是自定义的)都来可以通过在名称中添加"Filter"后缀并请求注入到控制器或服务中,如下:

angular.module('myModule', [])
.controller('myController', ['filterFilter', function(filterFilter){
	this.filterArray = filterFilter(this.notes, 'ch');
}]);

参数:

  1. 第一个参数是需要过滤的值。

  2. 其余参数是过滤器所需要的参数,对于某些过滤器来说是可选的。参数的先后顺序可以参考过滤器文档。
    通用函数接口:

function(startTime, arg1, arg2, arg3){}

HTML文本上使用时:{ {startTime | timeAgo: arg1 : arg2 : arg3} }

  1. 过滤器的返回值是我们所需要的最终输出结果。

关于过滤器的几个要点

1. 视图中的过滤器在每个digest周期都会执行

这是最重要的一点,我们在视图中直接使用过滤器,那么每次在digest周期都会重新计算值,这样,随着数据的增长,我们必须要小心UI中的过滤器可能带来的额外计算导致性能的损失。

2. 过滤器必须快如闪电

正是由于上面的情况,所以在理想情况下,过滤函数要能够在1ms内执行数次,所以一些比较耗时的操作(如DOM节点操作,异步调用等)就不应该出现在过滤器中。

3. 将过滤器置入控制器和服务中以获得最佳性能

如果需要处理大量的复杂数组和数据结构,同时又想利用过滤器的模块化和重用性,那么可以考虑在控制器或服务中直接使用过滤器。在数据没有变化的情况下,就不会重新计算,这样可以节省CPU周期。

二、过滤器的单元测试

需要测试的过滤器timeAgo

这个过滤器的功能是根据当前时间来判断要显示的事件是多久以前,并且根据一个可选参数optShowSecondsMessage来判断是否包含显示seconds ago,还是只包含显示minutes ago以上的级别。

过滤器的单元测试比较简单,测试流程与控制器完全相同。同样需要将过滤器注入单元测试,然后在过滤器中直接调用它们,传入各种不同的参数并观察运行结果是否在所有的分支条件下都正确。

describe('timeAgo Filter', function(){
	beforeEach(module('filtersApp'));

	var filter;
	beforeEach(inject(function(timeAgoFilter){
		filter = timeAgoFilter;
	}));

	it('should respond based on timestamp', function(){
		// new Date().getTime()函数每次返回的结果都不一样,导致无法确定ut的结果。理想情况下,我们需要在timeAgo过滤器中注入dateProvider.
		// 这里使用简洁的做法,我们需要假设测试在几ms内就完成
		var currentTime = new Date.getTime();

		currentTime -= 10000;	// 10ms以前
		expect(filter(currentTime).toEqual('seconds ago'));

		var fewMinutesAgo = current - 1000*60*2;	// 2分钟以前
		expect(filter(fewMinutesAgo).toEqual('minutes ago'));

		var fewHoursAgo = current - 1000*60*60*2;	// 2小时以前
		expect(filter(fewHoursAgo).toEqual('hours ago'));

		var fewMonthAgo = current - 1000*60*60*30*2;	// 2月以前
		expect(filter(fewMonthAgo).toEqual('months ago'));
	});

	// 上面的测试用例中没有测试可选参数,下面需要进行额外的测试
	it('should respond based on timestamp & optional arguments', function(){
		var currentTime = new Date.getTime();

		currentTime -= 10000;	// 10ms以前
		expect(filter(currentTime, false).toEqual('minutes ago'));

		var fewMinutesAgo = current - 1000*60*2;	// 2分钟以前
		expect(filter(fewMinutesAgo, false).toEqual('minutes ago'));

		var fewHoursAgo = current - 1000*60*60*2;	// 2小时以前
		expect(filter(fewHoursAgo, false).toEqual('hours ago'));

		var fewMonthAgo = current - 1000*60*60*30*2;	// 2月以前
		expect(filter(fewMonthAgo, fasle).toEqual('months ago'));
	});
});

三、参考

AngularJS:Up & Running (AngularJS即学即用)



posted @ 2017-05-08 18:55  少东主  阅读(451)  评论(0编辑  收藏  举报