浅谈AngularJS模板
原文链接 http://www.tychio.net/tech/2014/07/21/template-of-angularjs.html
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。
作为最流行的MVVM(Model-View-View-Model)框架之一,相信大部分前端对AngularJS都不会陌生,我也一样久仰大名。不得不说,AngularJS所带来的改变是巨大的,被称为未来浏览器的模式一点也不为过,尤其是思维上的转变。
作为一个常年挥舞着jQuery去指挥无穷无尽的DOM的前端,初次接触AngularJS是有困难的,许多先贤警告我们不要在AngularJS中使用jQuery,不是没有道理的。即使AngularJS中带有jQlite对象,也仅仅是为了弥补一些地方AngularJS的局限性。AngularJS操作UI的方式与jQuery有着极大区别,在深入学习之后,我渐渐的发现了这点。过去使用jQuery的前端就像一个操纵提线木偶的傀儡师,而手握AngularJS的前端简直是不折不扣的魔法师。前端开发者不再需要根据数据去改变DOM,然后填入数据,我们所要做的仅仅是决定数据的表现形式后等待数据的注入。文档流中的元素就像活过来了一样,根据数据表现出了对应的样子。
这一切的核心除了匪夷所思的DOM监听机制,还有就是AngularJS的模板(template)以及其中多不胜数的内置指令(directive)了。因此,我将在本文中谈谈AngularJS的模板以及其思维模式。 <!-- more -->
模板中的内置指令
AngularJS模板和EmberJS的模板相比更为普通一些,仍然是HTML格式的文件。这使得许多人并未真正了解AngularJS的模板,而认为AngularJS只是提供了一堆内置指令并可用于HTML文件。不过就先来说说这些内置指令吧,模板后面再详细讨论。
ngIf是个对于模板很重要的指令,它是基本的条件表达,满足条件时则存在,不满足则不存在。通过它可以轻松的让模板基于数据呈现不同结构。另外它会形成独立Scope,这也是其与ngShow/ngHide的区别之一。如果ngIf出现太多可能会导致页面渲染速度降低,此时可以选择ngSwitch来代替它,不过此时最好先一下检查你的逻辑。
ngRepeat则是另一重要指令,能循环创建DOM。可以说只要数据中有数组等结构,这一指令就必不可少。配合$index等索引变量,ngRepeat可以创造出多种形式的列表。还有ngRepeatStart/ngRepeatEnd可以将2个元素之间的内容循环创建,但我很少使用它们,因为这种混合多种元素的HTML结构不太好。
ngClass是样式层面上的主要指令,它的值可以是存放class名的变量,也可以是带有条件的对象。如此可以通过表达式来选择需要的class,以呈现不同的样式。ngHide与ngShow其实就是特殊的ngClass,ng-hide="[expression]"相当于ng-class="{'hide': [expression]}"。
这些基本的指令构成了一套很有效的模板逻辑,我们可以消除掉各种HTML的重复性代码,还能在单个模板中呈现出无数的形式。但我不赞成将大段的HTML做成partial或directive并通过switch或if来选择性呈现,因为模板应该是可复用的组件,而不是带有逻辑的路由。
真正的页面只有一个
AngularJS的主旨即快速创建单页面应用,所谓单页面就是说真正的页面只有一个,其中变化的只是模板和数据。但许多习惯于传统Web的开发人员往往找不到单页面的感觉,自然而然的将模板当作了以往的HTML页面,然后根据AngularJS的路由一一对应到模板。这是时常发生的情况,即使是在开发的中途也可能转入这个误区,因为模板的逻辑难以划分,还有被控制器牵连进去的,最后只能让每个路由单独使用对应模板。
模板不是页面,不应该和路由有任何逻辑关系,它关心的应该仅仅是数据呈现结构。所谓的数据呈现结构是说数据需要以何种方式呈现,比如列表、统计图、详情或分页等,而不是数据结构。它往往依赖于HTML结构,所以当HTML的结构不够表意时,模板的划分也会跟着变得困难。一个模板就是一个对象,不属于它的东西,无论多麻烦,即使HTML可以放在一起,也一定要排除在外,无论封装成directive还是partial,或者分离出去与之变成平级关系。
模板也不应该关心数据的内容,即使两个路由中显示的数据内容完全不一样,但显示结构一样或类似,模板就应该利用自身的逻辑在注入数据后呈现对应的内容。但值得注意的是,其自身逻辑应该判断数据本身而不是引用控制器的逻辑,也就是说模板要有自身的逻辑,不应该将页面逻辑混合散落在模板和控制器中。那么如何做才好呢,简而言之就是UI逻辑和业务逻辑的分离,模板中应该只存在UI逻辑。我们应该封装模板,像黑盒一样,无论控制器有什么业务逻辑,都不应该干涉UI逻辑。
比如ngIf指令可以使用表达式,我们应该利用数据本身达到逻辑表达,而不是依赖于控制器中依赖于业务逻辑的变量,比如使用data.length > 0而不是hasData。这看起来是一样的,但其实区别很大,前者将具体逻辑放到了模板中,当数据为空时不显示,而后者则把逻辑抛给了控制器。这样就不仅仅是逻辑放置在哪的问题了,这是违背MVVM框架初衷的。因为当data为空后,控制器必须之前在监听data,并将hasData设为false,而这些事应该交由AngularJS在数据绑定时自动完成。所以如果实在分不清什么逻辑是UI逻辑要放在模板中,什么逻辑是业务逻辑该放在控制器中,那就遵循一个原则,能利用Angular性能时就利用,不要做AngularJS已经做过的事。
Scope属于谁?
其实这是思维方式的转变,所以误入歧途是不可避免的,而其中最多的误解在于Scope。Scope本身是作为在控制器中的模板作用域,实际效果就是Scope上的属性在模板中可以直接使用,无论是在花括号还是Angular的表达式中,比如指令中。但开发人员很容易被其表面所迷惑,将其当作传统后端模板的变量来使用,更有甚者发明了$scope.vm作为ViewModule来使用,但其实Scope本身就应该是ViewModule。其原因还是在于思维,不同于传统后端的模板变量,Scope是依赖注入到控制器的,也就是说Scope不属于控制器,而属于模板。很明显在模板中将不需要Scope这一名称来指明,因为所有变量都是Scope的。
Scope属于控制器或模板有区别吗?有而且非常大。当Scope一片空白的来到控制器时,我们自然而然的认定这样一个原先空空的变量肯定是被控制器创造出的,然后在模板中被使用。但我们换个角度想想,如果模板先定义了它需要的Scope的结构,而后控制器仅仅只是按照预先的定义插入对应的数据,Scope的结构是不是明显属于模板。仔细想想其实很好理解,模板和任何控制器组合都是这套Scope结构,而控制器脱离了该模板Scope就变了,而且模板不用的东西,控制器放到Scope中也等于没有。也就是说,往往我们先定义了控制器才开始编辑模板,自然认为Scope是控制器创建好给模板用的,但如果我们先创建模板,控制器其实只能按照模板中规定的结构来填入数据。而这就像是Java中的接口一样,模板定义好接口,然后控制器只要满足这些填入自己的数据,就能在页面上获得需要的东西,而且同样它们都是一对多的关系。也许有人会说,那也存在同样的逻辑和结构却需要不同显示方式的情况。其实,这仅仅是表现形式的改变,数据呈现结构却没有发生本质上的改变,一个列表,无论是列、行还是格子都仍然是一个表格,模板应该用ngRepeat将其呈现,然后就是CSS的事情了。当你遇到无法使用CSS转变呈现方式的时候,首当其冲的应该考虑一下,是否HTML写的不好,不够灵活,没有语义化呢。
当我们完成这样的思维转变,模板将不再依赖于控制器,它也可以完整的自我封装起来。这样做的好处显而易见,HTML的代码将有巨大的精简,重复的代码消失的无影无踪,因为当它们重复时就可以形成一个独立的模板,模板可以套模板,它们还可以互相引用。但要注意循环引用,比如a引用了b而b又引用了a。如此CSS的重构也将变得更加简单,此时考虑使用OOCSS等提高CSS复用性将变得易如反掌。另外如果某个模板具有关联十分紧密的复杂逻辑还可以打包做成指令,这样模板的逻辑将更加的强大。
命名转变思维
要做到这样的思维转变其实是很难的,不过有一个小技巧可以借以完成这样的转变,那就是命名。当设计模板Scope结构时,我们可以更多的考虑这样的一个结构中某数据是什么。在控制器中,也许是一组系统管理员的数据,我们可以命名为admins,也许是一组用户数据users,又或者是一组读者数据reader,还可能是采购员buyer。不过等等,一旦使用了这之中的某个命名,模板就会和控制器绑在一起,你无法为其他的数据使用该模板,而恰恰对于设计统一性来说,这些数据又很可能会放在同种结构之中,也许仅仅是颜色或标题的差异。
此时还能做什么,复制一份相同的模板然后选择另外的命名?完全错误,我们应该抽象它们,它们都是一类数据,列表list。就像之前我说的,设计模板时仅仅应该关心其数据表现结构,它就是一个列表,列表的数据内容完全不会影响到结构。无论是users还是buyer,都可以放入一个list模板中。这是非常重要的,一个长期和数据库打交道的后端,惯性思维会将user和users归为一类,并认为admins和users是完全不同的。但是到了UI层面,这些显而易见的规则已经不适用了,users和admins都是list,而admin和user都是object。如果你担心不同数据类型需要的UI表现不一样,那就应该先考虑CSS实现兼容各种形式,其次是考虑嵌入一些不同的模板或指令来实现差异化,而不是直接复制两个模板。
总之,使用AngularJS一定不要过多考虑流程,而要考虑UI本身,我觉得这有点像面向对象的思维方式,UI既对象。利用AngularJS的监听机制让数据驱动模板产生交互。甚至我还经常利用JavaScript中Object的引用类型特性,使一些关联的数据之间也能产生数据与视图之间的关联,这样连通UI与数据和数据与数据之后就不用再管UI了,只需要操作数据即可,UI自然会表现出应有的状态。