写点什么

构建前端 UI 组件的新思路

  • 2010-05-26
  • 本文字数:2841 字

    阅读完需:约 9 分钟

前端 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 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2010-05-26 04:2113565

评论

发布
暂无评论
发现更多内容

YashanDB监控报警系统设计与实现技巧

数据库砖家

跟着“苏超”畅游金陵城,打卡江苏电信5GA

极客天地

嘉为蓝鲸CMeas研发效能管理平台:数据下钻技术如何让问题根因“无处遁形”

嘉为蓝鲸

DevOps 研发效能 研发效能度量 研发效能洞察管理

RocketMQ 千锤百炼--哈啰在分布式消息治理和微服务治理中的实践

Apache RocketMQ

阿里云 云原生 MQ 消息队列

YashanDB集群维护与升级实操指南

数据库砖家

再赴香港!嘉为蓝鲸亮相网络安全技术研讨会,与伙伴共探行业发展新趋势

嘉为蓝鲸

DevOps AIOPS 智能运维 研发运维 研运一体化

TRAE cue 迎来月度最大更新,模型能力以及时延大幅优化

火山引擎开发者社区

Trae

基于 RocketMQ 的基金数字化陪伴体系的架构实践

Apache RocketMQ

阿里云 RocketMQ 云原生 消息队列 金融行业

YashanDB架构设计与实现,助力企业数字化转型

数据库砖家

恶疟原虫目标检测数据集(2700张图片已划分、已标注)【数据集分享】

申公豹

数据集

解码监控可视化:IT运维如何通过图形化语言实现从数据到决策的高效转化?

嘉为蓝鲸

数据库监控 智能监控 IT运维 IT监控 IT运维监控

阿里云消息队列 RocketMQ 5.0 全新升级:消息、事件、流融合处理平台

Apache RocketMQ

阿里云 RocketMQ 云原生 消息队列

TRAE cue 背后的挑战与思考

火山引擎开发者社区

火山引擎 大数据 火山引擎 云服务 Trae

博时基金基于 RocketMQ 的互联网开放平台 Matrix 架构实践

Apache RocketMQ

阿里云 云原生 MQ 消息队列 Matrix

CAD图纸如何批量转换成PDF格式?

在路上

cad

抖音商品详情API秘籍!轻松获取商品详情数据

tbapi

抖音商品数据采集 抖音API 抖音商品详情接口 抖音商品详情API 抖音商品数据分析

基于消息队列 RocketMQ 的大型分布式应用上云最佳实践

Apache RocketMQ

阿里云 RocketMQ 云原生 消息队列

EDA 事件驱动架构与 EventBridge 二三事

Apache RocketMQ

阿里云 Serverless 云原生 消息队列 事件总线

数据、情绪与传播链条:全球社交媒体监控的三重任务

沃观Wovision

研发交付的“定心丸”:嘉为蓝鲸CFlow价值流管理平台以稳定替代数量,筑牢业务信任

嘉为蓝鲸

DevOps 研发效能 价值流 价值流管理平台

聚焦日志查询体验!嘉为蓝鲸WeOps V5.25&V4.25用AI破解查询难题

嘉为蓝鲸

智能运维 一体化运维 一体化智能运维平台

Chrome停用Manifest V2?一招教你无缝迁移插件到洋葱头浏览器

贝锐

chrome 浏览器 Chrome插件

化“不可抗力”为“可控影响”:AI时代的项目效能革新

思码逸研发效能

研发效能 效能度量 研发效能管理 思码逸

语音模型初创「宇生月伴」获数千万元融资;游戏 AI 陪伴逗逗发布 1.0 版,引入 RTC 实时通讯丨日报

声网

RocketMQ在搜狐的创新实践

Apache RocketMQ

kafka RocketMQ 云原生 消息队列

DWG格式CAD文件如何转成DXF格式?

在路上

cad cad看图 CAD看图软件 CAD看图王

海外舆情监测数据的商业价值挖掘:从信息到决策

沃观Wovision

数据分析 舆情监测系统

“全”事件触发:阿里云函数计算与事件总线产品完成全面深度集成

Apache RocketMQ

云原生 消息队列 EventBridge

阿里云 EventBridge 事件驱动架构实践

Apache RocketMQ

云原生 消息队列 EventBridge

开口就行!TRAE 2.0 语音输入功能

火山引擎开发者社区

AI 火山引擎 Trae

构建前端UI组件的新思路_Java_王保平_InfoQ精选文章