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

1、过滤器的基本概念

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

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

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

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

经常使用的内置过滤器

1. currency

函数源码:json

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,源码很简单,只是作个类型判断:ide

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]);

参数:
objObject | 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';
    }
};

不一样按钮的显示结果是:

您的浏览器不支持 iframe 标签。


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

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周期。

2、过滤器的单元测试

须要测试的过滤器timeAgo

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

你的浏览器不支持iframe

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

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'));
    });
});

3、参考

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

相关文章
相关标签/搜索