写点什么

构建前端 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:2113629

评论

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

pygame 二次 hello world 项目感知

梦想橡皮擦

10月月更

数字货币合约交易APP系统开发介绍(案例)

Shopee ClickHouse 冷热数据分离存储架构与实践

Shopee技术团队

数据库 后端 Clickhouse 存储 S3

音视频:播放器与H.265播放探索

程序员架构进阶

视频流 播放器 H.265 10月月更

Prometheus 查询操作符(四) 示例合集

耳东@Erdong

Prometheus 10月月更

理解 std::declval 和 decltype

hedzr

算法 元编程 C++11 c++17 纯虚函数

Vue进阶(幺肆叁):如何用绝对定位(position:absolute)完美定位布局及其注意事项

No Silver Bullet

Vue 绝对定位 10月月更

Java 面试八股文之数据库篇(三)

Dobbykim

第一周作业

沐风

持续测试、持续集成、持续交付、持续部署和DevOps

FunTester

持续集成 持续交付 持续测试 FunTester 持续构建

数字货币期权交易软件系统开发内容(源码搭建)

一文读懂公共部门、区块链和数字太空竞赛三者之间的角色关系

CECBC

【新】虚拟机深层系列「GC本质底层机制」SafePoint的深入分析和底层原理探究指南

码界西柚

JVM safepoint JVm虚拟机 10月月更

Go 中如何写注释

baiyutang

golang 10月月更

代码质量管理:SonarQube + Jenkins Pipeline配置

看山

DevOps 10月月更

官方线索|#1024小鹏汽车科技日#如约而至!关于未来出行,你有什么想象?

搬砖人

1024我在现场

揭秘!探访百度AI反诈第一线

白洞计划

docker 系列:实践工具

yuexin_tech

架构营模块一作业

GTiger

架构实战营

ZooKeeper分布式配置——看这篇就够了

牧小农

zookeeper 分布式配置

【LeetCode】加一Java题解

Albert

算法 LeetCode 10月月更

永续合约软件系统开发源码搭建

把握数字经济发展趋势和规律 推动我国数字经济健康发展

CECBC

👊 【Spring技术特性】采用protostuff和kryo高性能序列化框架实现RestTemplate的序列化组件

码界西柚

spring 序列化协议 序列化机制 10月月更

期货合约系统APP开发简介(搭建)

区块链的监管架构基本成型

CECBC

【Flutter 专题】23 图解自定义 Dialog 对话框

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 10月月更

对自己深度学习方向的论文有idea,可是工程实践能力跟不上,实验搞不定怎么办?

Giant

自然语言处理 机器学习 深度学习 算法 论文

数字货币合约交易系统开发内容(源码)

技术分析| WebRTC开源服务器商业化过程中遇到的问题及挑战

anyRTC开发者

开源 音视频 WebRTC 服务器 实时通信

模块一的命题作业

月影之臣

架构实战营

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