框架技术细节说明 must
该文档详细说明了基于Angular的机制及关键技术。
目录: - 路由机制 - 通过路由来切分页面模块 - Lazyload机制 - 指令 - 程序bootstrap - 数据绑定 - filters - 何时编写自定义指令 - controller之间的通信 - 依赖注入 - 统一的http请求拦截器 - 根据不同的页面,显示不一样的头部菜单选中 - 如何进行国际化 - 如何进行表单校验 - 动画 - 测试
路由机制
路由机制是指页面是如何根据url跳转的。如,访问#/home
,该加载哪个页面模板,这个页面模板该哪个控制器来控制。
在SPA中,路由由前端控制,整个工程只有一个真正的页面。如,我们这只有一个index.html页面或者index.jsp页面。
在我们的工程中,前端路由交由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这个状态时,我们得到的是这样的页面。
如果需要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路由里定义了homeBanner
及homeCount
,在home.tpl.html这样定义
这样,就能把页面拆分成子模块。如果在别的页面也需要引入这些模块,只需要在路由里面定义,然后在父级模板里面声明需要的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里的数据绑定是好用的一种。看下我们需要写什么:
{ {name}}
function Ctrl($scope) { $scope.name = '炸油条';}
一旦name这个数据发生变化,就直接在视图上反映出来了,中间的过程全部交给angular来处理。
filters
filter用来format展现的数据,这样用
{ {date | dateFilter}}
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表单校验的插件,是不是没几个好用的。。
表单校验为什么难,是因为你不仅仅需要校验,而且你需要不同的校验的展示方式。所以往往会根据具体情况来进行处理,下面说一种比较常见的校验处理方法。
{ {'AD_CONTENT_NO_URL' | translate}}
input需要输入合法的url,url不合法的会马上在右侧显示错误提示。这点angular非常轻松的就做到了。。。然后我们可以让提交按钮无效
但是变态的需求又来了,按钮永远可以点。。。好,那我们在提交的时候做判断。我们同样通过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。