-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MVVM时代的Web控件 ——基于AngularJS实现 #2
Labels
Comments
good |
nice |
good |
👍 |
👍👍👍👍 |
AWESOME |
Good |
ionic framework这个感觉不错 |
👍 |
思路不错,无奈核心不在国人手里掌握 |
👍 |
Closed
不错,学习了 |
分析的比较透彻 |
不错,赞! |
已阅,好! |
写的非常赞,我之前还在思考 |
👍👍 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
MVVM时代的Web控件 ——基于AngularJS实现
前不久,Yahoo宣布了一个消息,停止YUI框架的开发,令人很多感慨。YUI作为最知名的控件库之一,影响了几乎整整一代前端开发人员。
现在这个时代,除了最基本的模式,控件库已经被极大地多样化,差异化了,所以,不用尝试在一个控件中考虑太多,你再考虑也考虑不完需求,反而会把代码变得臃肿。
一些前端MV*模式和Web Components的流行,使得我们可以用更轻松快捷的方式组织界面,在这个过程中,需要重新考虑控件和普通业务界面的分界,控件的概念实际上已经淡化了。
是不是我们就不再需要Web控件库了?也不是。在很多场景下,还是会出现一些固定的UI模式,如果有较好的封装,会对业务开发提供很大的便利。现在流行的新框架很多,AngularJS,Polymer,React,每种都有自己的一套理念,目前各自的生态圈都是不如jQuery的,但我们如果硬把jQuery的控件拖过来,也会很别扭,那怎么办呢?
毛主席教导我们,自己动手,丰衣足食。全新的引擎,就应当有全新的外围,不能开着坦克还射箭,我们试试来自己搞一下。
用每种框架实现控件,都需要遵循它的理念,利用它的优势特性,然后用一些特殊优化来绕过它的弱点。
本文主要基于AngularJS框架,对构建一个Web应用中可能会面临的“控件”进行一些探讨,用下面几个典型的控件实现来大致说明它们的理念差异。
控件的分类方式
对于不同类型的控件,我们的处理方式是不同的,先作一下分类:
容器
所谓容器,意思是主要用于放置一些东西,最多也就做一下状态切换之类的工作,比较典型的是Panel,TabNavigator,Accordion。
在ExtJS这样的控件库里,会有Panel这种纯静态容器,这样的容器其实完全没有封装的必要,封装了可以省一点点编码量,但无足轻重,所以我们无视它。
另有一些界面容器,之前我们会把它做成控件,比如Accordion,TabNavigator,但其实它的内部实现并不复杂,无非是选中项切换,创建或者移除子项等等,而且,还要顺便提供一大堆的参数配置,用于实现界面。
比如,jQuery UI的Accordion实现:https://github.com/jquery/jquery-ui/blob/master/ui/accordion.js,内部封装了各种DOM操作,把HTML打成碎片混在JavaScript中,如果想要做一些UI层面的调整,非常困难。
在MVVM时代,如果借助数据绑定的力量,有可能把这个控件的实现改得跟原先完全不一样。
在AngularJS的主页上,有这么一个Demo:
这个Demo做了两个自定义的标签,用于简化实现TabNavigator的HTML代码。原先我们可能要这么写:
它这么一搞,就比较语义化,代码的可读性增强了,但灵活性丢了很多,想要好用,至少还有一大堆配置项,比如我们想要在每个tab上加个关闭按钮,简直很麻烦。
把容器封装成指令,基本上都是得不偿失的事情,我们还是直接一些,来看一个简单的代码:
http://jsbin.com/homas/6
怎么样,是不是很简单?
因为像Accordion这类控件,内部的逻辑无非是对数组的增删改,使用AngularJS的双向绑定机制,可以极大简化低级的DOM操作,直接用清晰的逻辑把整个业务表达出来,然后给UI充分的自由度。
类似,TabNavigator这个实现选项卡的功能,也可以用一样的方式实现,甚至它的模型跟Accordion都是一样的。看我们的改写:
http://jsbin.com/homas/8
怎样,我们就直接用着Accordion的模型,搞了完全不同的另外一个东西出来,有了这样的方式,还要控件干什么?
数据列表
所谓列表型控件,侧重于数组型数据的表达和展示,比如,List,DataGrid,Tree,一般会有选中等操作。
简单列表
简单的列表其实跟上面的TabNavigator、Accordion的模型一样,就是把数组迭代而已,也基本没有继续成为控件的必要性。
简单的列表可能包含哪些形态呢?
这些其实都是简单的数组迭代,唯一的差别在样式上。
这个示例给出了纵向列表和瓦片列表的大致代码,它们使用同样的数据源:
http://jsbin.com/yeciga/1
数据表格
比列表稍微复杂一些的是表格。表格是一种很常见的展示多列数据的方式,那么,表格有没有必要封装成控件呢?
这个要看业务场景有多复杂。常规的表格是可以不用封装成控件的,只要普通带样式的table,tr跟td绑定到数据源,选中样式再处理一下,就差不多可以了,单元格里面还可以包含各种复杂的其他组件,都很容易。即使是表头需要排序,这个模板也比较容易写。
那什么样的场景需要封装控件呢?比如说,行列都比较单一,纯展示文本数据,表格头可能要带拖动列宽等效果,总之,在表格数据源之外的方面作了比较多的工作,这种就可以搞成控件。
树形结构
比数组型数据更复杂的是树形数据的展示。
其实,树形结构是一个比较麻烦的东西,任何一个前端的MV*框架,都会希望你把数据模型尽量扁平化,避免过深的层次,而树恰好是反着来的,所以这就导致对树形数据的展现非常别扭。
有一些用AngularJS实现的树控件,使用了递归的数组绑定来实现,写起来确实是很简洁的,但效果不一定好,因为它的监控机制在这种场景下有较大的浪费,比如说,树节点的选中样式绑定到一个监控表达式,当很大数据量中一个节点被选中的时候,可能会要把所有的监控都跑一遍。当然这里的数据模型设计也是会有一些技巧的,比如,改在单个节点上存放选中状态,用于判定样式,会比依赖于控件级的selectedNode变量效率要高不少。
那么,对于这类控件,有什么好办法吗?
我觉得这个东西有两种思路:
DOM辅助
这类控件比较典型的是ContextMenu,它的核心特征在于:自身的DOM一般直接从属于document对象,或者某个特定容器,不属于触发它的界面部件。AngularJS讲究的是分层、有序,尽量避免DOM操作,但这类控件的特点使得我们不太容易建立它的映射关系,因而不得不从DOM层面入手。
怎么办呢?
我们可以把两种截然不同的东西分离出来,比如说,右键菜单,它的菜单本体使用数据绑定来实现,而用DOM事件来控制它的弹出和关闭过程:
这类控件的处理方式基本上跟传统的是类似的,一般来说只有很特别的,状态跟事件相关的才需要这么做。像下拉式的菜单就不必通过这种方式,因为它的弹出层可以跟自身的DOM放在一起,绑定一个状态变量,当点击触发的时候,把这个变量改变掉就可以了。
公共服务
什么叫做公共服务型呢?是那种直接插入代码流程的UI展现,比较典型的是自定义的Alert,Dialog和Hint,这类代码的调用,是不涉及DOM操作,也不涉及绑定的,设计成公共服务会比较好。
比如说,当业务操作成功,要给出一个统一的提示,就让他用这样的API:
比如,要弹出自定义的确认取消对话框,就这样:
又比如,要弹出自定义的对话框,就这样:
有些使用AngularJS的人会有认识上的误区,认为一切DOM操作都应当放在directive中,并不是这样,要看这个操作是干什么的,如果它起的是一个公共服务的作用,对业务来说不存在关联关系,那就应该设计成service。放在directive中的东西,应当是可以当做一个“组件”来使用的,
独立功能块
这类控件一般是独立功能的区块,跟外界的联系是松散的,跟业务界面没有明显的区别。主要通过事件来通讯,比较典型的是Calendar,Pager。
用一个分页控件pager来举例,它每次在当前选中页变更的时候对外发送一个事件,外界监听这个事件,并作出相应的操作。分页在很多管理系统中,真是一个很常见的东西。有些UI框架会把分页功能跟数据表格等控件捆绑,内置为它们的一个选项,这么做其实有不少缺点。
首先是增加了控件本身的逻辑复杂度,其次是不灵活了。
当分页控件独立出来之后,它如何跟外界交互呢?这其实跟普通的多块界面部件之间的通信并无差异,实际上,界面部件自身并不通信,因为他们都只是实例化之后的视图模板,真正可以用于通信的是随同它们一起实例化的视图模型,也就是AngularJS中的控制器,在控制器上通过$scope可以进行通信。
所有随界面模板实例化的$scope都挂在$rootScope为根的树上,然后通过事件进行通讯,从上往下是$broadcast,从下往上是$emit。当然,也可以自己造一个事件总线用于跨层级通讯。
那么,对于分页控件这样的东西来说,应当怎样去跟包含它的界面通信呢?
在有些基于AngularJS的控件库中,分页控件直接操作$parent的数据,在我看来,这不是一种好方式,原因稍后说。对于此类控件来说,使用事件与外界交互是最自然的方式,它使得界面组件之间的耦合性大幅降低。
表单增强
比较典型的是DatetimePicker,ColorPicker。
这类控件其实也可以算独立功能型,作这样的划分,主要是考虑到在大部分MVVM框架里,原生的input,select,textarea等都是有特殊增强的,可以直接跟数据模型绑定,它们跟外界唯一的交互就是数据模型。对于像DatetimePicker这样的控件,其实业务方并不关心它内部是怎么实现的,做了什么操作,只需要关注最后的选中值,从这一点来看,它跟普通的input并无区别。
这类控件是最适合封装成Angular指令(directive)的。
现在我们有些纠结了,从形态来说,表单增强类控件跟上面这种独立功能块的差别在哪里?为什么把分页划分到独立功能,而把DatetimePicker划分到表单增强呢?
分类的原则不是说它像不像表单元素,而是它是否应当能直接访问包含它的界面块的数据模型。
对于表单增强型的控件,设计思路一般是没有歧义的,大家都会让它直接绑定数据模型。那独立功能型的控件,为什么不能让它直接绑定数据模型呢?
这个差别主要来源于控件和数据模型的“距离”。表单增强型控件跟数据模型的距离非常近,因此它直接使用数据模型没有问题,但是界面增强型控件,很可能这个距离较远,比如说,至少要从父级视图模型中转一下。
设想我们要构建一个多widgets的门户,其中有一个widget是个日历,使用了Calendar控件,这个日历取值变更的时候,可能影响其他到其他widgets的行为。如果我们让它能访问父级数据,会导致系统结构变得混乱,所以只能限制它用事件。
动画
那么,碰到一些要使用动画的情况,该怎么办呢?
传统的方式,用JavaScript去根据浏览器的支持度,封装不同的实现,通常是三种:JavaScript动画,CSS Transition,CSS Animation。
在AngularJS中,如果用于状态变迁的动画,用后两种非常方便,只需要把各状态对应成CSS样式类,然后使用ng-class来绑定样式名就可以了。
如果是专门的动画效果,可以用directive封装起来,根据特征的不同,选择封装成元素或者属性。
图表
以AngularJS为代表的MVVM框架,使我们能够远离烦琐的DOM操作。我们回想在业务中使用的不同控件,似乎还有一类没有覆盖到,那就是图表库。
传统的JavaScript图表库,有些是基于Canvas的,从实现机制上来说,无需依赖jQuery这样的DOM操作库,这类通常封装了自己的基础操作,自成一体,本身做得很优秀,典型的有百度的ECharts。如果想把它跟Angular这样的框架集成,一般来说在上面套一个directive的壳即可,在内部调用真正的实现。
注意到还有另外一些图表库,核心是适配了SVG或者VML实现的,比如说,基于RaphaelJS做的图表控件。我们看一下Raphael的常见代码写法:
哎,这代码的样子怎么这么熟悉?像不像jQuery?因为使用SVG或者VML来显示图形,本质是跟DOM操作一样的,所以它也选用了像jQuery一样的代码方式。
我们大胆再想一步,普通的基于HTML元素的控件,我们不用jQuery了,而是通过绑定的方式,那图表库是不是也可以这样呢?
来尝试一下:
http://jsbin.com/yokik/1
是不是很有意思?
这个例子本身很简单,用来代替成熟图表库的话,可以说差得非常远,但它说明了我们有可能用怎样的思路去实现图表库。
传统图表库的缺点是,整个视觉方面都只能由程序员控制,对视觉方面有经验的人只能给出配色和布局的建议,然后等程序员实现了之后,再回头来继续提出建议修改。
使用我们提到的这种方式,就把数据逻辑和界面展现分离得非常好,可以像写普通HTML界面那样,分别由不同的人员协作,然后组装在一起。
如果我们想要把同样的数据换一种图形来展示,也会非常容易,不需要改变模型,只要把视图层换掉,立刻完成。
比如这个例子,使用了同一个数据模型:
http://xufei.github.io/ng-charts/index.html
这个例子还可以进一步封装成directive,以SVG片段作模板,从元素属性和上级作用域中获取参数,这样使用起来更便利。
小结
我们回过头来想一想,控件的本质是什么?是特定数据结构的交互展现。会有哪些数据结构呢?总结起来,真的是很简单,因为常见的就这么几种:
其他好像都没有了。
传统的控件,封装的主要逻辑是数据模型跟DOM之间的对应关系,而这种关系被AngularJS这种MVVM框架作为基础设施提供了。把代码重构之后,我们会惊奇地发现,控件的界面和逻辑分离得干干净净,我们可以复用这个逻辑,在不同的场景下把控件界面多样化,以此来面对不停变更的需求。
因此,在MVVM的时代,我们需要把控件库用与以往完全不同的方式来重新设计,去掉一些不再适合作为控件的,把其他的控件展现跟行为分离,让模型更精炼,给UI层更多的自由度,控件这个概念会淡化很多。
从这一点看,新的模式会对我们的HTML和CSS规划能力要求更高,因为之前在控件内部封装了DOM的处理,当需要整体调整的时候,有机会在控件这个层面去统一处理,但把控件界面分离并多样化之后,这部分压力就会转移到DOM和样式规划者手中。
所以,我们会发现,那些使用AngularJS的人,会很倾向于用BootStrap或者Foundation这类样式框架,因为对他来说,样式和界面结构规划变成了一个非常重要的事情了,而这类框架会帮助他们把这个部分的基础工作做好。
总而言之,把数据模型从控件中提取出来,把UI层配置化,是使用AngularJS这类框架的核心要点。
随着时代的发展,浏览器特性逐渐增强,新框架层出不穷,我们能够有机会选用一些较新的实现技术,大幅简化或者完全改变之前的实现方式。
未来会更加美好。
本文提到的一些控件的基础demo可以参见这里,因为比较仓促,所以问题还有很多,只是大致说明了构建不同控件的思路,以后会逐步完善。
http://xufei.github.io/ng-control/index.html
The text was updated successfully, but these errors were encountered: