AngularJS 实践总结

搞了快一年的 Angular,Angular 真的是一个非常强大非常齐全非常好用的框架,而且拥有强大的社区支持,虽然踩了很多坑,但是仍然无悔学习了这样一个框架。Angular 2 已经正式发布了,我也打算学 Angular2 去了,现在手头上还有一个用 Angular 的项目,这个项目我是想把我学到的很多 Angular 的最佳实践都用进去,借此我就想干脆也写几篇博客总结下好了,我写的比较散,想到什么就说什么。

使用单次绑定或单向绑定

从 Angular 1.3 开始就有了 once-time binding,Angular 1.5 开始支持了指令和组件的 one-way binding。看看他们的用法:

单次绑定很简单,加上:: 就可以了。

<p>{{::vm.title}}</p>

An expression that starts with :: is considered a one-time expression. One-time expressions will stop recalculating once they are stable, which happens after the first digest if the expression result is a non-undefined value (see value stabilization algorithm below).

当 digest 结束之后并且单次绑定的值不为 undefined 时,这个值将不再被监听。官方称这个算法为 Value stabilization algorithm 还解释了一下。看到这个,再想起 Angular 源码那注释,觉得真的业界良心啊。想具体了解下单次绑定,出门右转 -> docs.angularjs

在视图进行单向绑定也很简单,使用 ng-bind 就可以了,这里主要说的是指令和组件中的单向绑定。

其实 Angular1 还是不断再发展,现在最新的是 1.5.9,加了很多新的东西,也做了很多性能优化。单向绑定也是 1.5 之后才引入的新东西。1.5 也引入了一个新的东西叫 component,和 directive 差不多,具体我没用过我也不太了解,不过写法简洁了很多。单向绑定主要就是用于 component 和 directive 的。

随便找个比较简单的例子来说下,

(function(){
  'use strict';
  var app = angular.module('app');

  app.component('menuBar', {
    // defines a two way binding in and out of the component
    bindings: {
      brand:'<'
     },
    // Load the template
    templateUrl: '/js/components/appComponent.html',
    controller: function () {
    // A list of menus
      this.menu = [{
        name: "Home",
        component: "home"
      }, {
        name: "About",
        component: "about"
      }, {
        name: "Contact",
        component: "contact"
      }];
    }
  });
})();

单向绑定就上面的 < ,我们知道一般有 @, =, & 三种方式,分别表示绑定字面量,绑定表达式,绑定事件,而新增的 < 是用来实现单向绑定的。上面这段代码是从别的地方找来的,由于我自己没有使用过,所以暂时不过多介绍,晚点搞清楚了再补充下。

谨慎使用 $interval

如果我们要实现实时显示当前时间的效果,可以有下面三种方式:

vm.time = Date.now();

// 方式一
setInterval(() => {
  vm.time = Date.now();
  $scope.$digest();
}, 1000);

// 方式二
setInterval(() => {
  $scope.$apply(() => {
    vm.time = Date.now();
  })
}, 1000);

// 方式三
$interval(() => {
  vm.time = Date.now();
}, 1000);

考虑到页面还有其他双向绑定量,你觉得上面三种方式哪种性能消耗会小一点?

我这边测试了下,三种方式导致当前 digest 所执行的 watcher 表达式数量分别是 方式一 (34) < 方式二 (140) = 方式三 (140)。

方式二.png

方式二.png

方式三.png

我感觉有种被骗的感觉,我记得大家都说不要手动调用 $digest 啊要用 $apply 啊... 但事实是这里的方式一是性能最好的,如果要说原因,那是因为$scope.$digest() 只会触发当前 scope 进行 digest,而其余的就会从 $rootScope 下来整个都进行 digest,对于我们这里只是想要实现时间变化的需求来说就显得有点多于了,这种情况下还是方式一合适些。当然了你也可以用原生 JS 来写,但这样就使用不了 Angular 的日期格式化功能了~

不少人认为 Angular 脏检查是轮询,如果不加限制使用 $interval,不就和轮询没区别了。所以能少用就少用,这里不用 $interval 是因为它会在每次循环结束自动调用 $rootScope 上面的 digest,因此使用 setInterval,如果你不触发 digest,那么这个数据的变化是不会同步到视图中的,所以我们手动的触发了当前作用于的 digest。

ng-repeat 使用 trackby 优化

Angular 会为每个 watch 变量生成一个 hashkey,并使用他来跟踪其值的变化,当我们使用 ng-repeat 时,如果数组的内容发生了变化,Angular 不会重新销毁和渲染整个 DOM,而是找出变化的一部分做出修改,由于 hashkey 是根据节点内容产生的,这意味着我们的数组中不能有完全一样的两个节点存在。并且当数组的子项为对象时,用一个类似的新的数组覆盖它会导致 Angular 销毁并重新渲染整个 DOM。我们试一试便知。

<!-- 不使用 track by -->
<p ng-repeat="item in items">
  {{item.value}}
</p>

<!-- 使用 track by -->
<p ng-repeat="item in vm.items track by item.key">
  {{item.value}}
</p>

我们在控制器里面这样写看下

vm.items = [{key: 1, value: 2}, {key: 3, value: 4}, {key: 5, value: 6}];
$timeout(() => vm.items = [{key: 1, value: 2}, {key: 3, value: 4}, {key: 5, value: 6}], 2000);
$timeout(() => vm.items.push({key: 7, value: 8}), 3000);

可以自己试一试,然后调出开发者工具查看下 DOM 的变化

结果是:

  • 不使用 track by

    2 秒后重新渲染全部子节点,3 秒后添加渲染了一个新节点。

  • 使用 track by

    2 秒后没变化,3秒后添加渲染了一个新节点。

他们的区别就在数组重新赋值上面,还有就是数组必须是一个由对象组成的数组。在有些情况下,我们可能需要使用一个新的数组覆盖旧的数组,而他们之间可能有部分是相同的,如果我们使用 trackBy,Angular 就可以利用这一部分已经渲染好的 DOM,从而达到了优化的目的。

如果没有特别的唯一标识可以指定,也可以直接使用 track by $index,也可以起到一样的作用。

关闭调试信息

据说这个方法很多人都不知道?

$compileProvider.debugInfoEnabled(false);

这样就可以关闭 Angular 插在视图里面的任何辅助调试信息例如 ng-ifng-repeat 等注释以及 ng-binding 等 CSS 类。上线的时候关掉会比较好一点。

使用超级强大的 interceptors

这个在做全局请求和相应处理时特别强大,通过它我们可以实现统一修改每个请求的 header,对返回的结果进行处理,全局 HTTP 错误响应处理等等。用法也相当简单:

$httpProvider.interceptors.push(function($q) {
  return {
    'request': function(config) {
      
    },
    'requestError': function(rejection) {
      
    },
    ‘response': function(response) {
      
    },
    'responseError': function(rejection) {
      
    }
  }
});

额,举个粟子:

(function() {
    angular
        .module('app')
        .factory('tokenInjector', tokenInjector);

    function tokenInjector($injector, $q, $cookies, $cacheFactory, $timeout) {
        let jwt = undefined;
        
        return {
            request: function(config) {
                if(void 0 === jwt) {
                    jwt = $cookies.get('jwt');
                }
                config.headers['Authorization'] = "Bearer " + jwt;
                return $q.when(config);
            },
        }
    };
}());

上面新建了一个 tokenInjector,之后我们这样使用就可以了:

$httpProvider.interceptors.push(tokenInjector);

作用就是实现每次请求都自动把 token 添加到 header 的 Aturhorization 中。除此之外还可以做好多好多东西,这里就不详细介绍下去了。

使用 ui-router 替代 ngRoute

自从用了 ui-router 就再也回不去 ngRoute 了,ui-router 使用状态来进行转移,支持多视图和嵌套视图,使用方法更加灵活,并且也有更加丰富的 API。多视图,嵌套视图你知道意味什么吧,这些 ngRoute 做不到,官方现在也是主推 ui-router,前段时间 ui-router 已经出了新的 1.0 版本,相信大部分开发者还是在用老版本吧,可以考虑迁移了,想要了解新版本的迁移事项看这里

对于还在使用 ngRoute 的童鞋,我也强烈建议你去使用 ui-router。

多视图和嵌套视图就不多说了,除此之外还有比较强大实用的地方就是 ui-router 提供的生命周期钩子即 $stateChangeStart, $stateChangeSuccess, $stateChangeError,ngRoute 不知道有没有,没有去了解过。下面举个粟子说下用法:

$rootScope.$on("$stateChangeStart", function (event, toState, toStateParams, fromState, fromStateParams) {
    $rootScope.loading = true;
});
$rootScope.$on("$stateChangeSuccess", function (event, toState, toStateParams, fromState, fromParams) {
  if(ga) {
    let re = /\{(.*?)}/g, url;
    if(Object.keys(toStateParams).length > 0) {
      url = toState.url.replace(re, Object.values(toStateParams).reduce((pre, curr) => curr));
    } else {
      url = toState.url;
    }
    ga('send', 'pageview', url);
  }
  $rootScope.loading = false;
});
$rootScope.$on("$stateChangeError", function (event, toState, toStateParams, fromState, fromParams, error) {
  $rootScope.loading = false;
});

其实上面应该时比较 tricky 的做法,作用就是全局视图切换动画以及让 google analysis 工作,从 stateChangeStart 开始,isLoading 变为 true 直到 stateChangeSuccess 的时候变为 false。我们把 isLoading 挂在 $rootScope 上面方便使用,用这个变量就可以判断是否加载一个 Loading 的 DOM。

其次,由于单页应用特性,google analysis 只被加载一次从而无法实时反馈用户当前所访问的页面,我们可以手动进行调用,上面就是用法了,也是每次在成功状态变化之后发送当前 url。

不过遗憾的是这个 API 在 ui-router 1.0 版本废弃了,不过 ui-router 1.0 目录下带了另一个文件,引入这个文件即可以继续使用。我猜测 ui-router 1.0 应该是有更好的解决方案来替代它,详细的可以自己去了解下。

另外还想说的一个地方就是 ui-router 的 resolve 功能,允许我们在控制器初始化之前先获取和处理数据,如果 resolve 的东西是一个 promise,如果 promise 状态为 rejected,那么视图就不会被成功切换。基于此我们可以把视图需要的一些 HTTP 请求放到 ui-router 的 resolve 上面来做,然后再注入到控制器给控制器使用,并且这样还可以避免数据未到达时视图的数据显示问题以及数据请求失败后的问题。举个粟子:

.state('posts', {
  url: '/posts/:type',
  templateUrl: 'posts/posts_tpl.html',
  controller: 'PostsController as vm',
  resolve: {
    posts: function(Posts, $stateParams, $q) {
      let defer = $q.defer();
      if(['unread', 'mark'].indexOf($stateParams.type) !== -1) {
        defer.resolve(Posts.get({type: $stateParams.type}).$promise);
      } else {
        defer.reject('参数不正确');
      }
      return defer.promise;
    },
  }
})

这是一个路由的状态,这里应用了 promise 的反模式,其实我自己也不是特别了解,之前看到一篇文章再批评这个。我回去学习下再补充。

这个地方的作用就是判断 state 传进的参数是否符合要求,如果不符合要求则无法加载,符合要求的话就会去请求资源,如果请求失败的话视图也不会被加载,即状态不会切换成功,如果请求成功我们可以再控制器依赖注入 posts 取得返回结果.