构建前端UI组件的新思路
前端UI组件,目前流行的实现方式大多源自传统客户端的UI设计体系。无论是早期的Bindows,还是近几年兴盛的ExtJS,其UI组件都在模仿客户端软件,代码实现建立在复杂的继承体系上。好处是可以构建出和客户端体验一致的一整套UI组件,但弊端也很明显:组件长得都差不多,代码则继承太深,牵三挂四,不够轻便。
如何才能让前端UI组件轻便灵活起来呢?首先得意识到Web UI设计有自己的独特性。Web页面可分为两种:一种是以展现信息为主的Web页面(web page),另一种是以操作信息为主的Web应用(web app)。对复杂的Web应用来说,可以采用ExtJS等类库来构建类客户端体验。但是,越来越多的Web应用已逐步脱离模仿客户端的阶段,开始从Web的独特性出发,将传统UI组件的功能融入到Web页面中。
举一个例子:国内的Web邮箱,无论是163还是QQ邮箱,都会让人联想起Outlook或Foxmail等客户端软件。这种模仿成本很高,然而带来的效果个人觉得却不是很好。反观Gmail,整体UI设计,简明轻快,但功能一点也不逊色。在Gmail里,看不到Tree,看不到DataGrid,脱离了这些传统UI组件的框框,将功能融入到Web元素里,反而让用户更自然、更高效。
前端UI组件的Web特性,需要我们打破传统思维,换一个角度,重新去思考UI组件的基本组成要素。换什么角度去思考?什么角度才是合适的?这离不开具体场景。下面以一个实例来说明。
在淘宝页面中,以下是几个常见的UI组件:
传统思路里,我们会构建三个组件来实现:
- Slide(轮播)组件
- Tabs(标签页)组件
- Carousel(旋转木马)组件
Tabs组件,我相信任何一个前端开发工程师最多半天都能搞定,而且还能把延迟触发、动态加载等特性也给实现了。Slide组件也不难。Carousel组件看起来稍微复杂一些,但熬上一个通宵研究研究循环的实现,也不是什么大难题。
上面是传统思路。新思路是:我们真的需要分三个组件来实现吗?
仔细思索,我们可以提取出这三个组件的共同点:
- 都有一组触点(Triggers)。 Slide的触点是数字,Tabs的触点是标签头,Carousel的触点是小图。
- 都有一组面板(Panels)。就是内容区域。
- 通过触点可以让面板切换(Switch)。
上面的几条里有一个很重要的概念:切换。所有这些组件的共同点是可切换的。如果我们实现了一个可切换(Switchable)组件,上面三种组件就都是特例了。
根据上面的抽象,很容易实现switchable.js:
function Switchable(container, config) {
…
}
S.mix(Switchable.prototype, {
init: function() { }
switchTo: function() { }
next: function() { }
prev: function() { }
});
上面仅实现了基本的切换功能,我们可以进一步通过插件来实现更多功能:
plugin-autoplay.js
– 提供自动播放功能。plugin-effect.js
– 提供切换时的各种特效。plugin-circular.js
– 提供循环切换功能,比如Slide自动播放到第5张时,能无缝循环播放到第1张。plugin-lazyload.js
– 提供数据的延迟加载功能。
插件的实现在JavaScript这种动态语言里是小菜一碟。至少有两种思路,一种是埋好钩子(hooks),插件根据钩子进行扩展:
S.mix(Switchable.prototype, {
init: function() {
…
S.each(Switchable.Plugins, function(plugin) {
if(S.isFunction(plugin.init)) {
plugin.init();
}
});
},
switchTo: function(index) {
…
this.fire(‘beforeSwitch’, {toIndex: index});
…
}
});
在插件的代码里,定义好init方法,以及监听相关事件(事件可以看成是一类hooks)即可实现需要增加的功能。
插件的第二种实现方法是动态修改基础对象,可以重写某些方法,也可以利用AOP特性,将增加的功能织入到基础对象中:
S.weave(function() {
// plugin code
});
}, ‘after’, Switchable.prototype, ‘init’);
上面的代码表示在Switchable的init方法执行完成后,再紧接着执行plugin code。
通过这种方式,我们无需用到任何继承概念,没有super,没有extend,利用JavaScript的原生动态语言特性,就比较完美地解决了问题。
从Switchable的角度看,上面三个组件可以描述为:
- Tabs是普通的Switchable组件。
- Slide是可自动切换且切换时有特效的Switchable组件。
- Carousel是可自动切换、切换有特效、可循环切换的Switchable组件。
来看下Slide的实现,变得非常简单:
var defaultConfig = {
autoplay: true,
circular: true
};
Function Slide(container, config) {
config = S.merge(defaultConfig, config);
Slide.superclass.constructor.call(this, container, config);
}
S.extend(Slide, S.Switchable);
这里用到了类似YUI的extend方法,实现了继承,同时较好的保持了JavaScript的原汁原味。
可以看出,Tabs、Slide和Carousel组件,彼此之间没有本质差别。封装出这三个类,仅仅是为了让开发者能方便快捷的调用(这些是高级API)。对于资深开发者来说,在实例化Switchable时,通过传入不同参数即可实现所需效果(Switchable是中级API)。
更有意思的事情是,换一种思路实现代码后,也能帮助我们换一种思路看世界:
这个组件,可以看成是触点为图片的Tabs组件。左右两个翻页,无非是调用next/prev方法。进一步:
- Tabs组件可以看成是仅有基本切换功能的Slide。
- Slide可以看成是触点悬浮在图片上面的Tabs。
- 等等等等
最后会发现,这三个组件,本身就是相通的。原本同一物,何必分开实现呢?我们可以得到一个结论:
只要符合switchable可切换特性的UI组件,原则上都可以通过Switchable实现。 唯一限制的是您的想象力!
比如,在Switchable的基础上,我们可以进一步实现Album(画报),实现CoverFlow(仿iTunes的封面切换效果)等等。
上面对UI组件的思维角度是可切换(Switchable),这是一个形容词。进一步思考,我们还可以得到以下形容词:
- Draggable – 可拖曳的
- Positionable – 可定位的
- Selectable – 可选择的
- Sortable – 可排序的
- Stackable – 可堆叠的
- Clickable – 可点击的
- Ajaxable – 可ajax的
- …
在这种新思路下,前端UI组件的基本组成要素已不是Panel、Memu和Button等传统UI基础单元,而是上面这些形容词。假设我们要实现一个可拖曳的可动态加载数据的可排序的表格时,或许像下面这样写一行代码就实现了:
S.Widget(‘tableId’).draggable().ajaxable().sortable();
这是一个梦想。但有梦,去追,说不定就能实现。
备注
上面的代码里使用了KISSY UI类库,详细请参考:http://kissy.googlecode.com/
Switchable的详尽代码实现请参见:http://kissy.googlecode.com/svn/trunk/src/switchable/