Angular细节说明

来源:互联网 发布:js select 选中值 编辑:程序博客网 时间:2024/05/09 00:52

框架技术细节说明 must

该文档详细说明了基于Angular的机制及关键技术。

目录: - 路由机制 - 通过路由来切分页面模块 - Lazyload机制 - 指令 - 程序bootstrap - 数据绑定 - filters - 何时编写自定义指令 - controller之间的通信 - 依赖注入 - 统一的http请求拦截器 - 根据不同的页面,显示不一样的头部菜单选中 - 如何进行国际化 - 如何进行表单校验 - 动画 - 测试

路由机制

路由机制是指页面是如何根据url跳转的。如,访问#/home,该加载哪个页面模板,这个页面模板该哪个控制器来控制。

在SPA中,路由由前端控制,整个工程只有一个真正的页面。如,我们这只有一个index.html页面或者index.jsp页面。

<body ng-controller="CtrlApp" class="{{lang}}">    <div class="wrap">      <div ui-view="header" class="app-header"></div>      <div ui-view="body" class="app-body"></div>    </div>    <div ui-view="footer"></div>  </body>

在我们的工程中,前端路由交由ui-router组件控制。ui-router控制的路由其实是一个状态机,可以由一个状态跳转到另一个状态。每一个状态有url的映射,有views的映射。

// Home      $stateProvider.state('home', {        url: '/home',        views: {          'body': {            templateUrl: baseUrl + 'home/home.tpl.html',          },          header: headerConfig,          footer: footerConfig        }      });

上图,我们定义了一个状态home,对应的url是/home,加载的views有body、header、footer。再参照index.html,我们可以知道home.tpl模板会append到

中,header、footer也做相应的事情。 也就是说,当我们流转到home这个状态时,我们得到的是这样的页面。

 

<body>        <div class="wrap">          <div ui-view="header" class="app-header">              <!-- header content -->          </div>          <div ui-view="body" class="app-body">              <!-- home content -->          </div>        </div>        <div ui-view="footer">            <!-- footer content -->        </div>      </body>

如果需要Controller来控制页面,状态中还可以加上Controller依赖,如下

// Home      $stateProvider.state('home', {        url: '/home',        views: {          'body': {            templateUrl: baseUrl + 'home/home.tpl.html',            controller: 'CtrlHome',            resolve: {              ctrl: $couchPotatoProvider.resolveDependencies(['home/CtrlHome'])            }          },          header: headerConfig,          footer: footerConfig        }      });

为什么要加上controller: 'CtrlHome'还要加上$couchPotatoProvider.resolveDependencies(['home/CtrlHome'])这句呢,详见 Lazyload机制

通过路由来切分页面模块

有时候页面会比较复杂,我们希望将页面拆分。考虑下如下页面结构 

可以看到这个页面看似简单,但其实包含了很多内容:头部、轮播、数字统计、推荐创意、最热创意五个部分。内容很多,但相关性却没有。好,那我们不希望一团糟,所以我们做出改变。我们希望把每一块的模板和业务逻辑拆分开来,这里我们用路由帮我们做这件事。看下下面代码

// Home      $stateProvider.state('home', {        url: '/home',        abstract: true,        views: {          body: {            templateUrl: baseUrl + 'home/home.tpl.html',            controller: 'CtrlHome',            resolve: {              ctrl: $couchPotatoProvider.resolveDependencies(['home/CtrlHome'])            }          },          header: headerConfig,          footer: footerConfig        }      })        .state('home.facade', {          url: '',          views: {            'homeBanner': {              templateUrl: baseUrl + 'home/banner/homeBanner.tpl.html',              controller: 'CtrlHomeBanner',              resolve: {                ctrl: $couchPotatoProvider.resolveDependencies(['home/banner/CtrlHomeBanner'])              }            },            'homeCount': {              templateUrl: baseUrl + 'home/count/homeCount.tpl.html',              controller: 'CtrlHomeCount',              resolve: {                ctrl: $couchPotatoProvider.resolveDependencies(['home/count/CtrlHomeCount'])              }            }        });

我们可以看到,我们将home这个状态定义为一个抽象的状态abstract,然后定义一个home的子状态叫home.facade,url为'',这样访问#/home这样的url,就会触发home的路由,以及home.facade路由。注意到home.facade路由里定义了homeBannerhomeCount,在home.tpl.html这样定义

<div>    <div ui-view="homeBanner"></div>    <div ui-view="homeCount"></div>  </div>

这样,就能把页面拆分成子模块。如果在别的页面也需要引入这些模块,只需要在路由里面定义,然后在父级模板里面声明需要的ui-view,就能够复用了。

Lazyload机制

Lazyload机制可以使状态流转的时候去按需加载页面所需的资源,而不是在一开始就加载。AngularJS本身没有实现lazyload,这样的话就导致了webapp的性能受影响,在首次加载的时候就要加载全部的依赖。

//定义angular模块  var app = angular.module('app', ['scs.couch-potato', 'ui.router', 'ui.bootstrap', 'ui.bootstrap.tpls', 'chieffancypants.loadingBar']);

上述代码片段展示了Angular module的加载机制。而在实际应用中,特别是业务系统,一般是业务比较繁杂,功能模块比较多,在这种情况下,angular module的默认机制就不符合我们要求了。

因此,我们采用requirejs + angular-couch-patato来实现Lazyload。angular-couch-patato负责托管angular的各种注册,包括controller,directive,filter,service等。平时我们写

angular.module('app').controller(function () {    //ctrl code here    });

为了使用lazyload,现在我们要这样写

angular.module('app').registerController(function () {    //ctrl code here    });

将部件注册,这样所有的部件都交给couch-patato来管理,在需要的时候(下文会提到如何使用)。加上使用requirejs,app是统一管理的angular module,我们就这样写:

define(['app'], function(app) {    app.registerController(function () {    //ctrl code here    }); })

首先加载依赖app,app是之前定义好的angular module,如var app = angular.module('app', ['scs.couch-potato', 'ui.router', 'ui.bootstrap', 'ui.bootstrap.tpls', 'chieffancypants.loadingBar']);。然后在app上注册controller等。

上文路由机制部分提到,在注册路由的时候需要调用$couchPotatoProvider.resolveDependencies来实现lazyload。这里设计到couchPatato实现的一个关键,就是利用angular在路由里面的resolve机制,这里的resolve是指,路由在触发之前,必须拿到resolve里面定义的所有值。这样couchPatato就有机会去获取定义的依赖,而在每个依赖里面,会将controller注册到app,然后在路由触发后,定义的controller才得以调用。

指令

Directive。angular的一大特性,他可以定义一个标签,或者说赋予dom一定的功能,他也是angular程序里面唯一操作dom的地方。一处定义多处使用,是组件化的一个利器!


如何定义指令? javascript app.registerDirective('dragable', function() { return { link: function(scope, element, attrs) { //code make it dragable } } }); 上述代码定义了一个指令,这个指令有赋予拖动功能的能力。

如果在模板里面声明,说,需要这个能力,他就有了这个能力 html <div dragable></div> 这个div可以拖动了!

没错就是这么神奇,当你还在等待html5规范出来,当你还在等各个浏览器厂商都实现这个规范的时候,angular帮你做了这件事,在技术上解决了这问题。

angular的zen之一,声明式优于命令式!当你声明了,你就拥有了,这是多么的nice。具体的directive教程请到https://docs.angularjs.org/guide/directive。


常用的angular的内置指令 - ng-repeat 迭代 - ng-if 控制显示 - ng-model 数据模型绑定

程序bootstrap

angular是如何解析指令的,是如何获得控制权的?(因为什么还有入口,相信很多后端选手都没有这个概念,他们从来不需要关心main函数的东西。但web不一样,web是先去加载html,渲染完成之后加载js,然后js才能托管整个app)

angular的bootstrap分两种,手动初始化和自动初始化。

自动初始化方法,在html标签定义 ng-app指令,在angular.js加载完成之后,会compile整个html,将ng-app所在的dom作为根来生成动态的html。 html <html ng-app>

</html>

手动初始化 javascript angular.element(document).ready(function() { angular.bootstrap(document, ['myApp']); });

我们是选择在main.js里面手动初始化angular的,这样我们可以做更多的事情,比如在初始化加载一些我们需要的东西,比如权限,用户。

数据绑定

在angular里,数据绑定是指双向绑定。双向绑定是指视图改变的时候,对应的数据发生改变;反之亦然。数据绑定在很多mvc框架里面都有,但是angular里的数据绑定是好用的一种。看下我们需要写什么:

<div ng-controller="Ctrl">{{name}}</div>
function Ctrl($scope) {  $scope.name = '炸油条';}

一旦name这个数据发生变化,就直接在视图上反映出来了,中间的过程全部交给angular来处理。

filters

filter用来format展现的数据,这样用

<div class="date">{{date | dateFilter}}</div>

dateFilter就是日期的过滤器,这样数据源就不用变,我们考虑的只是怎么展现,下面是个自定义filter的写法

app.registerFilter('PortionFilter', function () {    return function ( input ) {      if ( input <= 1 ) {        return ( input * 100 ).toFixed( 2 ) + '%';      }      return input;    };  });

何时编写自定义指令

  • 指令其实可以理解为组件化的思想,当有多个场景需要相同的功能时,应该编写一个指令。
  • 当不得不进行dom操作时,编写指令
  • 当需要扩展dom功能时,编写指令
  • 如果开源的社区有更好的相同功能的指令,使用或者参考之

controller之间的通信

有时候,ctrl之间需要通信。我们根据ctrl的不同关系做不同的处理。 当ctrl具有父子或者祖宗关系时,我们采用$broadcast,$emit来进行通信。$broadcast往下传播,$emit反之。 当不具有直接层级关系的时候,我们通过他们共同的父级ctrl来进行交互,因为无论ctrl如何分布,他们最终都有一个共同的root。具体的做法是在父级ctrl定义一个他们共同维护的变量,然后监听这一变量的变化,最终通过$broadcast,$emit来进行通信。父级ctrl等于做了个中间人。

使用. 来进行通信 https://egghead.io/lessons/angularjs-the-dot

依赖注入

angular最好用的特性之一。依赖注入也是声明式优于命令式的一个体现。看下面代码

function Ctrl($scope, $http) {  $scope.doSth();  $http.doSth();}

我们可以看到,我们在想使用$http服务的时候,仅仅需要声明一下,我们就可以获得该引用。编写angular的service非常简单

app.registerService('util', function(){    return {      compress: function A(),      sthElse: function(){        console.log(1);      }    };  });

有了这个能力,我们就可以精简ctrl里面的代码,把可重用的或者冗长的业务代码抽离出来,使ctrl更多的关注业务流程,具体的业务代码维护在service里面。

统一的http请求拦截器

angular允许你注册http请求拦截器。有什么用么?有,在想统一对某类请求做统一处理的时候非常有用。如希望后端返回404的情况下,跳转到404页面。这样,就不用在每一个http请求的处理函数都加上这段。还可以针对ie做防缓存处理,及判断,如果是ie低版本浏览器,在每个get请求都加上时间戳,nice!

play ground:youtube的顶部加载效果!

根据不同的页面,显示不一样的头部菜单选中

比如说在首页,命中的是首页,当到列表页的时候,头部命中的应该是列表。 如何实现? 1. 得在相应的controller里面定义一套命中规则,规则规定了哪个路由应该高亮哪个item 2. 定义一个service来管理头部选中,在每个路由级的控制器中调用service来改变高亮

如何进行国际化

什么是国际化,国际化是根据不同的语种,来显示不同的文案。思路,我们根据不同的语种来加载不同的资源文件。如,当是中文的时候我们使用zh-cn.json,英文的时候我们使用us-en.json。

在angular中,filter负责显示。所以我们在filter上做文章。对于静态的文本,我们用一个key来代替如 '语言',我们用 'lang'来替换,在html我们这样写 {{'lang' | translate}}, translate的filter的作用就是从我们之前加载的语言包找到'lang'这个key所对应的文本。

{  "lang": "中文哦"}

那对于动态的数据应该怎么做呢,这个就需要跟后端做一定的约定。 - 值列表,我们直接去翻译好的i18n数据 - 枚举型,约定返回具有意义的字符串,如'success','failed'。然后我们在前端做一定的拼接,如{{'type' + type | translate}},然后我们在资源文件里加入typesuccess和type_failed的key,就ok了~ - 这里我们鼓励rd返回具有意义的字符串,而不是0,1,2这种


另外,国际化不仅仅影响了文本,还可能影响布局,就是说不同的语言环境,页面长得不一样,这种情况下,我们的处理类似换肤,我们在body上绑定一个语言环境的class,针对特殊的需求,我们做不同的处理。

如果国际化影响了业务逻辑,那建议对模块进行语言环境的逻辑判断。

如何进行表单校验

表单校验是个世纪难题,同学们可能都用过各种jquery表单校验的插件,是不是没几个好用的。。

表单校验为什么难,是因为你不仅仅需要校验,而且你需要不同的校验的展示方式。所以往往会根据具体情况来进行处理,下面说一种比较常见的校验处理方法。

<div class="material-url form-group">    <label class="control-label col-md-2">      url:    </label>    <div ng-class="{'has-error':e.materialValidator && formAd.materialUrl.$error.required}">      <div class="col-md-3">        <input placeholder="" type="url" class="form-control" name="materialUrl"        ng-model="e.adSolutionContent.materialUrl" />      </div>      <div class="col-md-3 help-block" ng-show="formAd.materialUrl.$error.url">        {{'AD_CONTENT_NO_URL' | translate}}      </div>    </div>  </div>

input需要输入合法的url,url不合法的会马上在右侧显示错误提示。这点angular非常轻松的就做到了。。。然后我们可以让提交按钮无效

<button ng-show="form.$valid" />

但是变态的需求又来了,按钮永远可以点。。。好,那我们在提交的时候做判断。我们同样通过form.$valid很容易的判断form有没有ok,如果ok,那ok,否则我们要focus到第一个出错的input。那我们就只能悲催的挨个判断input是否合法,然后写个指令控制下focus动作。

综上表单校验,根据不同的需求做不同的处理。

动画

angular推荐的是用css3来进行动画处理,因为angular可以很方便的操纵数据,也就能方便的切换css,再经由css3来动起来。https://docs.angularjs.org/guide/animations ,通过angular的教程可以搭建list的简单动画。

测试

angular程序是可以做测试的,这非常神奇,原因是angular将所有的dom操作代码都限制在了指令上,而dom操作代码是很难进行测试的。前面提到的尽量将逻辑处理的代码封装到service,这样就可以对service进行测试,保证代码的质量。

angular的测试还得力于他的依赖注入,因为这正是跑测试的关键。测试用例会首先创建一个angular-mock,假的壳,然后利用里面的$injector.get()来加载想要测试的service,这个时候已经能获得service的实例了,就可以进行测试。 angular推荐的是jasmine。