深入了解可爱的过滤器
AngularJS
右领军大都督
9人收藏 2203次学习

深入了解可爱的过滤器

本文扩展自我之前的一篇slides:lovely-filter,该slides由于引入了Plunker作为在线示例的工具(被“墙”了),所以如需完整查看该slides,可能需要科学上网。

 

“过滤器”不是个陌生词汇,如果当你看到这个词,想到了这个:

     或者     

不要害羞,你的理解没有错。这个设计理念,并不是非程序不可为的。

 

过滤器是这样一种设计思路:

层层“过滤”,达到最终效果,这种基于“层”的设计思路在软件工程中,我们称之为AOP

因为AngularJS中,directive太受关注了,太牛逼了,以至于似乎没什么人来讨论过滤器的重要性,那么,我们今天就来聊聊这个过滤器

AngularJS是什么?

既然我们要讲的是AngularJS里的过滤器(filter),那就先来简单回顾下AngularJS是什么吧:

  1. 由Google团队维护的开源框架
  2. 为单页应用(SPA)而生
  3. 一个宣称是MV Whatever的设计
  4. 自由扩展HTML能力

关于AngularJS的过滤器,我们要知道点什么?

  1. 只有UI关心的事,就该交给UI,别总让逻辑层担心
  2. 展示层做格式化数据,然后展示给用户,那是萌萌哒
  3. 她是多面手,无论template、controller还是service,她都能穿梭其间,游刃有余
  4. 定制自己的过滤器也方便得紧呢

为什么我说过滤器还挺重要?

让我们现来看一段UI的事,总麻烦逻辑部分操心,那得多闹心的案例:

$.get('ajax/users.json', function(users) {
    var newUsers = _.map(users, function(user){
        return {
            title: item.title.toUpperCase(),//title转大写
            name: item.name,
            birthday: formatDate(item.birthday, 'yyyy-MM-dd'),//生日按指定模板格式化
            salary: user.salary + ' RMB'//收入又加了个货币后缀
        };
    });
    var compiled = _.template(tpl);
    $('#content').append(compiled({
        users: newUsers
    }));
});

 

有朋友问,那我不混在逻辑里,我再提一个专服务于视图层的方法成么?

 

var Service = {
    getDisplayUsers: function(users){
        return _.map(users, function(user){
            return {
                title: item.title.toUpperCase(),
                name: item.name,
                birthday: formatDate(item.birthday, 'yyyy-MM-dd'),
                salary: user.salary + ' RMB'
            };
        });
    }
};

var compiled = _.template(tpl);

$('#content').append(compiled({
    users: Service.getDisplayUsers(users)
}));

 

这做法好些了是么?的确!但好(懒)的程序员会想另一个问题“我们这个视图是不是自理能力也忒差了点”就不能给自己长点儿脸么!?

诚然,我们确实希望有这样一个视图工具,她可以自我调节这类有关显示的问题,不劳他人操心,那我们来看看,AngularJS中的过滤器,是如何谄媚程序员的!

template中的过滤器应用

格式化一个字符串,那一定得是快快哒

<body>
    <!-- 只要一个过滤器,屏幕出现的就是大写HELLO WORLD! -->
    <h1>{{ 'hello world!' | uppercase }}</h1>
</body>

 

格式化一个数组呢?

<!-- 初始化一个数组,包含了5个地名字符串 -->
<div ng-init="items = ['Beijing', 'Shanghai', 'Guangzhou', 'Hebei', 'Shandong']">

    <!-- limitTo控制了数组输出,从下标为1的元素开始,输出2个 -->
    <p ng-repeat="item in items | limitTo: 2:1">
        {{ item }}
    </p>
</div>

 

自己动手玩玩儿?Plunker

controller中过滤器的应用

之前既然说过过滤器是多面手,那在controller中想必也表现不俗:

angular.module('demo', [])
.controller('DmController', ['$scope', '$filter',
    function($scope, $filter){

        $scope.formattedDate = $filter('date')(new Date(), 'yyyy-MM-dd');// 2015-10-12

}]);

通过注入的$filter service,获取一个内置的date过滤器方法,将输入的的new Date()转换成人类可读的格式。

service中的过滤器应用

controller都写了,service如何可以落后?

angular.module('demo', [])
.service('DmService', ['$filter',
    function($filter){

        this.getEuroMoney = function(amount){
            return $filter('currency')(amount, '€');// €26.2
        };

}]);

 

通过注入的$filter service,获取一个内置的currency过滤器方法,将输入的amount转换成了人类喜爱的货币格式

说好的层层过滤呢?

既然说了是过滤器,那多个过滤器叠加产生效果,也在情理之中吧:

<!-- 这里的hello经过两次“过滤”,最后输出了HE -->
<span>{{ 'hello' | uppercase | limitTo: 2 }}</span>

 

这种“前一命令的输入作为后一命令的输入”的链式操作是受Linux的Pipeline (Unix)启发而设计的,为的就是可以在不同的过滤器间通过随意的组合排列达到不同的使用效果,以提高程序的健壮性。

过滤器还能玩出什么花样么?

我们利用过滤器,自己写一个i18n吧?

先定义我们最后希望看到的HTML:

<!DOCTYPE html>
<html ng-app="i18nDemo">

  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.min.js"></script>
    <link rel="stylesheet" href="style.css" />
  </head>

  <body ng-controller="DemoController">
    <div class="group">
      <button ng-class="{active: curLang === 'zh'}" ng-click="toggleLang('zh')">
          中文
      </button>
      <button ng-class="{active: curLang === 'en'}" ng-click="toggleLang('en')">
          EN
      </button>
    </div>

    <div class="jumbotron">
      <h1>{{ 'HELLO' | i  }}</h1>
    </div>

    <script src="script.js"></script>
  </body>

</html>

这里,我们系统将来通过两个按钮“中文”和“EN”来切换界面的文字内容显示。然后HELLO作为key,在经过i这个过滤器后,呈现给用户不同的文字内容

 

然后就来写JavaScript的部分:

var i18nDemo = angular.module('i18nDemo', [])
  .value('language', {//设置语言字典
    zh: {
      HELLO: '你好,世界!'
    },
    en: {
      HELLO: 'Hello, World!'
    }
  })
  .controller('DemoController', ['$scope', '$rootScope',
    function($scope, $rootScope) {
      //在$rootScope上为当前语言设置默认值为中文
      $rootScope.curLang = 'zh';
      //提供切换语言的方法
      $scope.toggleLang = function(lang) {
        $rootScope.curLang = lang;
      };

    }
  ])
  .filter('i', ['$rootScope', 'language',
    function($rootScope, language) {
      var langFilter = function(value) {
        if ($rootScope.curLang === 'zh')
          return language.zh[value];
        return language.en[value];
      };
      // AngularJS 1.3之后,我们必须显式设置过滤器为“有状态”的,否则我们在过滤器里对$rootScope的状态依赖将无效.
      langFilter.$stateful = true;
      return langFilter;
    }
  ]);

 

最终效果请看这里的在线演示:Plunker

 

有朋友看到了我那段“AngularJS 1.3之后,我们必须显式设置过滤器状态。。。”的注释,一定会奇怪这什么意思?我们参考下官网给出的建议:

It is strongly discouraged to write filters that are stateful, because the execution of those can't be optimized by Angular, which often leads to performance issues. Many stateful filters can be converted into stateless filters just by exposing the hidden state as a model and turning it into an argument for the filter.

原来是“有状态”的过滤器常会带来性能问题,这个问题其实和lodash-template一定要返回一个“compiled template function”而非直接注入变量返回模版化之后的数据是一样的道理。一个“pre-compiled”的函数,通过接受不同的参数,生成不同的结果,效率一定高于每次都“compile”然后注入参数生成结果的方式。

OK,知道了问题,那如何改进?我们现来看看HTML部分的变更:

<!DOCTYPE html>
<html ng-app="i18nDemo">

  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.min.js"></script>
    <link rel="stylesheet" href="style.css" />
  </head>

  <body ng-controller="DemoController">
    <div class="group">
      <button ng-class="{active: curLang === 'zh'}" ng-click="toggleLang('zh')">
          中文
      </button>
      <button ng-class="{active: curLang === 'en'}" ng-click="toggleLang('en')">
          EN
      </button>
    </div>

    <div class="jumbotron">
      <!-- 在这里,我们把语言作为参数传入过滤器,免去了过滤器对外部环境的依赖 -->
      <h1>{{ 'HELLO' | i: curLang  }}</h1>
    </div>

    <script src="script.js"></script>
  </body>

</html>

 

然后再来轻微调整下JavaScript部分:

var i18nDemo = angular.module('i18nDemo', [])
  .value('language', {//设置语言字典
    zh: {
      HELLO: '你好,世界!'
    },
    en: {
      HELLO: 'Hello, World!'
    }
  })
  .controller('DemoController', ['$scope',
    function($scope) {
      //注意这里已经去掉了对$rootScope的依赖
      $scope.curLang = 'zh';
      //提供切换语言的方法
      $scope.toggleLang = function(lang) {
        $scope.curLang = lang;
      };

    }
  ])
  .filter('i', ['language',
    function(language) {
      //这里也不需要依赖外部的$rootScope,取而代之的,我们将curLang作为参数传进来了
      var langFilter = function(value, curLang) {
        if (curLang === 'zh')
          return language.zh[value];
        return language.en[value];
      };
      return langFilter;
    }
  ]);

 

来吧,还是老样子,在线演示看这里:Plunker

写在最后

关于AngularJS,有太多的文章都在讨论Directive,Route,Provider。是因为她们似乎更有背景,更显高大上。但过滤器作为那个“小东西”,也是的的确确需要大家关注的,毕竟“毋以善小而不为”嘛!

加入1KE学习俱乐部

1KE学习俱乐部是只针对1KE学员开放的私人俱乐部
标签:
AngularJS