InfoQ

InfoQ

文章

我的书签

登录注册 以永久保存书签。

该内容已经被标记书签!

标记书签错误,请重试!

构建前端UI组件的新思路

作者 王保平 发布于 2010年5月26日

领域
架构 & 设计,
语言 & 开发
主题
Java ,
用户界面 ,
架构 ,
JavaScript
标签
基于组件的架构 ,
《架构师》月刊

前端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/


作者简介:王保平,前端架构师,网名射雕,花名玉伯。崇尚简洁而不简单,相信付出才有收获。就职于淘宝网UED部,忙并快乐着。个人博客:http://lifesinger.org/

本文已被收录在《架构师》(5月刊)。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。

实用就好 发表人 Guo Ford 发表于
启发很大 发表人 Dong Yang 发表于
简单意味着功能受限,复杂意味着灵活, 发表人 谷 钰 发表于
Re: 简单意味着功能受限,复杂意味着灵活, 发表人 谷 钰 发表于
路过 发表人 a xing 发表于
工厂模式 发表人 fung peter 发表于
Re: 工厂模式 发表人 张 中原 发表于
思路大家都一致,权衡最重要 发表人 周 柏民 发表于
开发思维 发表人 张 凯 发表于
Re: 开发思维 发表人 胡 捷 发表于
为淘宝做广告 发表人 chen index 发表于
很好 发表人 Angel Black 发表于
总觉得内容与标题不符 发表人 YingQuan Huang 发表于
不错 发表人 Lu Adam 发表于
玉伯考虑得很仔细 发表人 超群 陈 发表于
  1. 返回顶部

    实用就好

    发表人 Guo Ford

    很欣赏你这样深入的思考.

  2. 返回顶部

    启发很大

    发表人 Dong Yang

    启发很大,谢谢

  3. 返回顶部

    简单意味着功能受限,复杂意味着灵活,

    发表人 谷 钰

    如题说成前端组件的新思路,不如说成淘宝应用的新思路,以上也就是一种有效应用,并非框架级别的前端组件应用。考虑问题过

  4. 返回顶部

    Re: 简单意味着功能受限,复杂意味着灵活,

    发表人 谷 钰

    考虑问题过于简单

  5. 返回顶部

    工厂模式

    发表人 fung peter

    思路有些类似涉及模式中的工厂模式

  6. 返回顶部

    思路大家都一致,权衡最重要

    发表人 周 柏民

    事实上前两者是同一种组件,也就是说:同一份DOM结构是不需要做侵入式包装即可实现前两种效果。

    后者从根本上来看也算是前两种的变体,但实现中不对DOM做侵入式包装,几乎很难实现。
    即便是可以非侵入式实现往往也可能会因DOM拮据,而效果不够到位。
    也许CSS3丰富的样式定义可以解决一部分问题,或是通过 XBL 来搞定.

    这里可能存在着项目更重视“可访问性平稳退化原则”还是“富客户端实用原则”的问题。

    最后,在前端实践中,若UI组件在简单DOM原型上累积“富”到一定程度,会自然产生一个量变,这时“参数配置”或是“插件机制”恐怕就不适合,新的组件就是必须的了。

  7. 返回顶部

    开发思维

    发表人 张 凯

    感谢作者的创意.不管作者考虑的有否完善。但是能提供给我新颖的想法,这点就非常好.
    其实控件库有控件的健壮性和完备性.
    只是在做某些东西的时候,我们要的仅仅是自行车,不一定非要去找个奔驰才满意。

  8. 返回顶部

    Re: 工厂模式

    发表人 张 中原

    更高一层抽象

    状态模式+策略模式

  9. 返回顶部

    为淘宝做广告

    发表人 chen index

    rt

  10. 返回顶部

    很好

    发表人 Angel Black

    作者还是下了一番功夫的.写的不错.

  11. 返回顶部

    Re: 开发思维

    发表人 胡 捷

    同意。性价比要首先考虑。

  12. 返回顶部

    路过

    发表人 a xing

    感觉不错!

  13. 返回顶部

    总觉得内容与标题不符

    发表人 YingQuan Huang

    总觉得内容与标题不符

  14. 返回顶部

    不错

    发表人 Lu Adam

    玉伯写的不错,有一些启发:)

  15. 返回顶部

    玉伯考虑得很仔细

    发表人 超群 陈

    我觉得对于大多数程序员来讲,EXTJS的UI组件,jquery插件这种现成的方式使用起来更简单方便,不用考虑太多。而这种东西冒似更前端,复杂些,可能更适用于有专业前端团队的公司吧,就比如淘宝!

    玉伯非常值得我学习,以你为榜样!

深度内容

"伤得起"的云计算应用——对云端应用之架构的思考

2011年4月21日至22日是值得云计算从业者纪念的日子。Amazon的IaaS服务出现故障,导致许多商业网站的服务中断,影响非常严重。作为云计算用户,我们需要思考的是,如何保证即便在云服务不可用的情况,我们的应用架构仍然能够屹立不倒?本文正是站在云计算用户的角度试图探讨这一问题。

让交付的速度跟上思考的速度

12人的技术团队,4组刀片服务器,每月20亿的访问量,每日1次准时部署,99.9%的可用性。这可能吗?当然。想知道如何做的吗?百姓网将与您分享他们在DevOps实践过程中的经验和技巧。
本次演讲视频录制于QCon杭州2011

架构之路——穿行在产品和业务之间

篱笆作为一家起源于社区的电子商务公司,反映到技术层面就是同时要面对产品和业务,以及经营战略的变化调整。如何在产品和业务的夹缝之间完成技术架构的抽象与平衡,寻找更有效的价值定位,这当中有些经验教训和个人感悟愿与众人分享。
本次演讲视频录制于QCon杭州2011

特性注入:成功三部曲

本文将对特性注入以及相关方法做一个扫盲性的介绍。我们会解释这个框架的关键要素,并附上实例来证实它们。为了让文章保持相对较短,我们不会深入到某个工具或方法中,而是会给出一些参考资料,以便大家做进一步的研究。

解析JDK 7的动态类型语言支持

随着JDK 7的发布,字节码指令集终于迎来了第一位新成员——invokedynamic指令。这条新增加的指令是JDK 7实现“动态类型语言(Dynamically Typed Language)”支持而进行的改进之一,也是为JDK 8可以顺利实现Lambda表达式做技术准备。在这篇文章中,我们将去了解JDK 7这项新特性的出现前因后果和它的意义。

Java Remoting远程服务(下)

随着互联网应用的发展,Java分布式远程服务技术受到越来越多的关注,本文将对各种相关实现以示例的形式逐一介绍,并总结其中的优缺点,使读者能够在技术选型时有所准备。这是文章的下篇。

深入浅出Node.js(四):Node.js的事件机制

专栏的第四篇文章《Node.js的事件机制》。之前介绍了Node.js的模块机制,本文将深入Node.js的事件部分。

采访和书评:精通HTML5和CSS3设计模式

《精通HTML5和CSS3设计模式》一书记录了目前HTML5应用程序的许多常见设计模式。InfoQ对该书作者之一Dionysios Synodinos进行了采访,谈到了该书以及HTML5应用的相关内容。