QCon北京2014《CardKit & DOMO UI - 移动时代技术与设计的十字路口》技术篇

Dexter.Yy 2014-04-27 12:27:03

这次是跟蒙晨一起在Qcon上分享,他讲设计我讲技术,在此之间参加了QCon举办的讲师训练营,接受了TEDx演讲教练的洗脑,真的非常赞!受益匪浅。搞明白了原来『事先把讲稿完整写出来』并不是新手的行为,而是所有高大上的讲者们必做的准备,区别只是你愿不愿意投入时间反复练习,等到了现场两手空空,随意走动,什么YES-SET、J Cutting…都天衣无缝,流畅到让人看不出你背过稿……显然,我还是选择了站在笔记本后面埋头念稿。

而且我的讲稿写得很书面,上下文之间有引用,其实不适合口头传达……但是就像Paul Graham谈写作与演讲时所说的:

『Any given person is dumber as a member of an audience than as a reader(任何人在听演讲的时候,都会比阅读的时候更迟钝)』

『a person hearing a talk can only spend as long thinking about each sentence as it takes to hear it(观众也一样,没时间对听到的句子进行思考、消化)』

『Every audience is an incipient mob(任何受众群体都可以被看做乌合之众的雏形)』

一份字词都被仔细推敲过的讲稿加上几页形象的幻灯片,其实是很好的文档,自己读的效果比现场听要更好……

以下是演讲后半部分——『从技术出发』的讲稿,可以当作CardKit相关技术的介绍来看,完整的幻灯片PDF看这里:slidesharespeakerdeck




在之前的部分里,蒙晨同学讲了移动时代我们面对的问题,这些问题同时来自两方面,一方面,豆瓣这家公司的特点,让我们在成本、适用性、维护性等方面都需要对自己提出更高的要求,另一方面移动时代本身,也让一些web上原来就有的趋势和问题,变得更加突出。

其实这些问题从根本上来说,是对我们的思维方式提出了要求,既要求我们重新思考和跳出一些传统思维习惯,又要求设计和技术在某些思维方式上协调一致,比如蒙晨同学刚才在讲方法和协作时,反复强调的积木概念,就是我作为前端工程师长久以来特别希望设计师能具备的思维:

在传达和讨论一个设计时,不要只告诉我这些东西具体是什么样子,而是告诉我这些东西在你的理解里『是什么』,不是描绘像素,而是描述结构和模式。

接下来从技术角度出发,虽然会展示更具体的方案和成果,但其实核心仍然是思维方式,首先要说的是能够很好达成刚才这些方法的『实现』。



为了实现那些好的愿望和想法,我们需要对自己的力量有足够的了解,了解这些力量的光明面和黑暗面,而在移动时代,就更加需要回到起点,从根本上重新思考web技术这种历史悠久、发展迅速、又无比强大的力量。



如果要从传统web技术中抽出几个根本特色的话,首先就是它在内容展现方面的能力。



我们知道web的起源就是为了交流信息,而具体交流的是一种叫作超文本的文档,定义这种超文本的语言叫作HTML,所以基于这种文档的——布局和渲染技术、语义的设计、结构表现行为分离——都是久经考验的,相对非常成熟。

但同样因为起源于文档,导致HTML的语义主要都是围绕文档的需求,是在很低的抽象层级上,用很具体的结构,去描述内容。一旦除了内容之外,还要实现大量交互界面和功能的时候,这种低级语义就成为缺点了。HTML5中的新标签提高了抽象层级,但解决不了根本问题。



传统web技术的第二大特点是它解决交互需求的方式,用链接这种声明式风格的代码,就可以满足大部分基本交互,开发成本和设计成本都很低,而客户端代码的实例运行在一个个文档里,每次加载页面重新运行,生命周期短,状态少,同样可以简化实现。

但这种形式反过来又意味着,大部分交互是以文档或者说页面为基本单位的,这是一种粒度很粗也相对很重的交互,比如要消耗网络资源,等待响应和页面重新渲染,设计开发成本低,用户使用的成本却高了。另外客户端程序的状态少,意味着服务器端程序要承担太多界面状态的维护,容易与特定客户端的UI关系过于亲密。ajax带来了更细的交互粒度,但同样不能解决根本问题。



第三大特点是动态语言和配置语言,也就是常说的JS+HTML+CSS,它们让开发者,特别是前端开发者,可以像手拿画笔一样随心所欲,按需实现特定UI或交互,用法灵活,容易修改,不怕产品经理频繁的『改版』。

但可能正是因为这种灵活满足需求的基因,在实际项目中,我们经常会看到前端的业务需求混杂在UI或交互的实现中,UI或交互的实现又混杂在具体内容或者说数据的描述和维护中。开发者习惯对每个需求都从零开始实现,也就是从很低的抽象层级开始。设计变更时会更倾向于丢弃和重写代码。前端MVC项目们能将数据描述和业务逻辑,与视图——也就是大量DOM操作分开,甚至不用你自己写DOM操作,但是它们的关注点其实都不在视图这边,对于上述这些围绕UI和交互的问题,同样不能解决。



第四大特点是网址,它既是整个web产品的一种状态,也是一种入口,而这种入口的特色,自由传播、随时访问、按需获取、及时更新,可以说是web技术和web产品最大的杀手级特性,跟传统软件相比它也带来一些小问题,比如不好控制的访问路径、平等的网状结构等,但真正的问题其实是……在解决web技术的其他问题时,经常会丢失网址的优势。



最后这个特点是最常听到的,跟C/C++时代梦想的『可移植性』和Java时代梦想的『一次编写到处运行』相比,web技术在跨平台方面确实有另一个层次上的优势,但还是有很多烦恼,比如说,严重依赖厂商的节操、理念和具体实现,很容易成为平台的次等公民,在平台上的控制力目前也相对小一些,此外还有前端领域的老生常谈,是渐进增强?平稳退化?还是入乡随俗?

现在已经总结了web技术最主要的五大特点,列举了它们的正能量和负能量,但如果没有特定场景,孤立分析一项技术其实是没有意义的,所以我们再来看一下移动场景下的特点



屏幕中间列举的关键词因为时间关系就不展开讨论了,但是可以看到其中的大部分就是移动时代的设计需求。其实我们采用web技术,就是为了得到屏幕左边这些好处,但是把右边这些东西,跟设计需求相比,就会发现有很多矛盾,所以即使设计师不了解这些技术问题,他们在移动web项目中,实际上总是有意无意的背着这些沉重包袱,容易形成思维定式,改善用户体验和创新都会有很高的成本。



在这种技术与设计的相互影响下,产生了当前常见的移动web实现,我们可以先不关注具体的技术实现方式,比如用jQuery UI还是bootstrap,而是从解决问题的角度,对它们做一个简单分类。



第一个例子是虎扑的触屏版,我想将这种称作『WAP风格』,注意我并不是想贬低这种设计不好或低端,而是说这类移动网页,主要解决的是内容传播和性能优化上的问题,而对于交互是否方便,功能是否完整,会做出很多妥协,比如在这个页面里在论坛版面之间跳转和翻页看回帖,对我来说都比较痛苦,经常需要回到桌面版。



第二类的例子包括github前不久才出的移动版,以及果壳网的移动版,我称作『MVP风格』,MVP是产品经理的黑话,就是『最低限度的可行产品』,这类移动网页的代码经常是另起炉灶重新写的,快速实现产品人员基于数据或主观判断决定的最核心功能,因为包袱少、成本低,可以把交互体验做的更好,不过为特定的核心功能专门去做的设计和实现,可能会无法适用于其他类型的、后续的、更多的需求,加上代码要额外维护,这个『最小版』可能无法跟上『完整版』的发展,给人一种停滞感,很难做到『移动优先』。



第三类包括Mozilla的开发者网站和dribbble,后者可能很多人都知道是没有官方原生应用的,我将它们称作『响应式风格』,这种类型的关键特征不是响应式设计和技术,而是能确保桌面版网页的所有内容和绝大部分功能都一定有对应的移动版,通常也有相同的网址,代码很可能也属于同一份codebase,一起实现一起改进,而代价是,桌面网页的设计和实现,可能会成为移动版的包袱,造成设计上的妥协乃至停滞。



第四类包括大名鼎鼎的金融时报web应用和一个第三方的hackernews应用,它们的设计和体验都类似原生应用,包含丰富的交互,几乎不使用页面跳转,而网址也不太有用,这一类移动网页我称作『SPA风格』,也就是单页web应用,它们的代码经常跟桌面版完全不相关。



最后的例子是豆瓣阅读的web客户端在手机中的效果,以及一个天气应用,它们除了SPA风格的特征之外,通常还有更独特更创新的功能和交互,可以归类为富媒体或创新工具。

以上这些例子都有各自的优点,包括:对性能的优化、对界面的优化、内容的完整、功能的完整、同一个网址、同一套代码、不背包袱的设计、丰富自然的交互。我们其实最希望的是能同时实现这些优点,避免它们之间的冲突。



这种理想位置介于『响应式风格』和『单页应用风格』之间,是一种能兼具它们优点的实现方案,而这也正是豆瓣移动化项目中想追求的



这个实现方案从最开始就是作为一个独立于豆瓣业务需求、通用的移动UI工具集来开发,它既是技术上的解决方案,也是设计上的解决方案,它的设计风格或者说默认皮肤叫作DOMO UI,而这套工具集本身叫作CardKit,这个名字中的card同样是从最开始就被当作核心抽象来对待的。

CardKit的设计理念和它提供的价值,可以归纳为三个关键词:积木、配置和分离



积木的概念,从技术角度来说,就是用可复用、可配置、可组合的组件,来封装UI模式,尽可能以组件为最小粒度,从更高的抽象层级上,来开发移动web应用的界面



CardKit对移动场景下的基本用户界面,做了非常彻底的抽象,这些抽象既包括内容的模式、交互的模式,也包括不同模式之间的关系,图中的Page Card组件就是一个很好的例子,它封装了『全屏视图』这个最基本模式,全屏视图把传统桌面上塞在一个网页里的内容,拆分并『折叠』到很多个单一目的的视图里,每次只激活其中一个,这种视图除了能容纳其他内容模式,还包含各自的导航、标题、动作栏,动作栏这种模式像Android原生UI一样提供『动作溢出』的设计。



CardKit把组件分成两类,第一类针对内容展现上的需求。当前的扁平化设计趋势、卡片化设计趋势、以及移动设计本身的特点,都在很大程度上纠正了传统web设计中照搬平面设计的毛病,为我们创造了很好的条件,可以抽象出非常通用的内容模式,这个示例图收集了豆瓣各产品中出现的列表子项的线框图,表面上变化很多,但如果我们将相似的语义部分涂上相同的颜色,可以看到其中有很简单的模式可循,或者说,很容易在设计中遵循一定的模式。



这些模式本质上是信息组织的模式,我们把封装这类模式的组件都叫作卡片组件,卡片组件就好像不同种类的容器,我们在为桌面网页设计移动版的时候,大部分内容都可以分门别类的放进相应容器里,设计师直接使用这些容器和结构,不需要每次都绘制具体的布局。



因为高度抽象,目前的卡片组件只有这五种,移动界面中经典的导航抽屉模式,在CardKit里用普通的视图卡片就可以实现,图片网格、导航菜单、条目列表、评论列表……这些包含子项的内容虽然外观差别很大,但是都可以用列表卡片来实现。



用抽象的卡片组件来实现具体UI的方法,就是通过这四类基本元素,其中『状态』会对应着一些值,有些值会影响组件的外观和结构,比如列表组件的『plainStyle』状态可以消除边框背景等修饰,有些值是组件的『元数据』,比如组件没有内容时显示的文案。『子组件』是非常重要的概念,它意味我们可以用小组件来配置大组件,有些子组件是父组件外观中的固定结构,比如标题栏,有些子组件是内容的一部分,可以在混杂在其他普通html里。



除了卡片组件,CardKit还提供一系列便捷的交互组件,但是跟bootstrap这类组件库不同,我们对移动UI的交互模式也做了高度的抽象,因此所有交互组件实际上都是用三种更基础的组件来实现的,这意味着应用开发者也可以直接用这三种基础组件来快速实现自己的交互功能。甚至这三个组件之间也有组合关系,最基础的是能在『激活』和『没激活』两个状态之间切换的状态组件(Control),取值组件(Picker)会包含多个状态组件,其中处于激活状态下的组件的值,就是取值组件本身当前的值,如果设置成激活后立刻恢复初始状态,取值组件就变成了一个动作选择器。



浮层组件(Overlay)的子类中有两种最重要的,一种是模态视图(modalView),实现特定情景的功能,另一种是动作视图(actionView),这种视图的核心就是一个取值组件。



无论卡片组件还是交互组件,在用来实现具体UI的时候都需要一定程度的配置,而且组件越抽象,配置就越重要。CardKit组件的是完全用标记语言——也就是HTML来配置的,但这里的HTML跟传统的HTML有很大不同。



CardKit的组件在最终显示的时候,为了保持web技术的布局能力和兼容性,必须是用传统HTML来实现的,但如果我们在配置组件时也需要自己写这些HTML代码,就会发现我们大部分的代码不是在解决真正的业务需求,而是在描述很多琐碎的实现细节,比如半透明背景、嵌套结构。



CardKit能让我们用更抽象的、描述组件本身的标记语言来编写配置,这种标记语言与HTML兼容,既可以是自定义标签,也可以在普通HTML标签上扩展。从示例代码中可以看到,代码更清晰、更扁平,与业务逻辑无关的代码和嵌套结构都消除了。



不是把HTML看作内容或界面本身,而是看作一种配置,是很重要的理念,这意味着之前介绍的组件的基本元素,包括状态和子组件,都能在HTML里,用声明式的风格进行管理。



传统HTML中的链接和网址其实也是声明式风格的配置,所以在CardKit中,链接和网址不但完全得到保留,还用来实现视图卡片的交互,这种实现对应用开发者来说是透明的,应用开发者还是按照传统web的方法,用网址上的hash指向页面内的id,CardKit会自动判断id对应的元素,如果是导航抽屉就会触发侧滑,如果是其他视图卡片内的元素,会先切换视图,再滚动到这个元素上。这样不但桌面网页中的网址都不会失效,移动界面的每个视图卡片也都会有对应的网址。



交互组件同样可以用HTML来配置,示例中,上面的HTML代码完全等价于下面的JS代码。CardKit为交互组件提供的JS API,会自动为相同的DOM元素提供同一个组件实例,所以即使在写JS的时候,我们仍然用HTML当作配置来访问组件,而不需要额外的ID或JS命名空间。



对于卡片组件来说,连额外的JS API都不需要了。DOM对象就是组件对象本身——这意味着不但在静态环境里,可以用抽象语义来配置组件,在浏览器运行时,也是围绕抽象语义的DOM对象来管理组件的。这种DOM对象不需要为组件扩展额外的方法,只需要允许用传统的DOM操作方式来修改组件的状态。所以用CardKit开发的界面里,JS代码通常会很少,即使有也几乎都是胶水式的脚本,虽然是朴实的脚本,却又是高度抽象的,只是响应事件,修改状态,而不描述状态变化的步骤和细节,不需要接触具体的外观和交互实现。



基于抽象语义的HTML配置,修改抽象状态的JS脚本,还有组件本身,都贯穿着一个很重要的目的——就是『分离』,把不同层级的实现分离开。



第一层实现是组件自身的实现(next),既包括UI和交互的实现,比如渲染外观、过渡效果,也包括组件的设计和功能,CardKit用一种专门的JS API来实现这个部分。



组件的配置语言,相当于组件的接口,是完全分开来实现的,CardKit同样用了一种专门的JS API来简化这里的实现。



最后一层是应用本身的实现,它使用上一层提供的接口。




分离会给这每一层的实现都带来好处,拿豆瓣来说,组件的分离,能让我们在UI设计上不背包袱,自由的探索和使用最合适的UI,配置语言的分离,让CardKit目前可以同时支持新旧版本的配置风格,保证老代码的兼容性,也可以满足某些局部项目中开发者的个人习惯。业务实现的分离,对于之前说过产品线、资源和维护方面的问题都很有好处。



CardKit强调的声明式风格,实际上很容易从编写代码的方法,发展到更直观更快捷的可视化设计工具,让设计师也轻易上手,直接组装和调试真实的UI,制作原型,甚至直接产出产品级代码。



为了实现CardKit的这些设计,我们需要解决很多技术问题,也产生了一些更通用的开源项目,其中扮演最主要角色的是DarkDOM和Moui。



CardKit里的卡片组件,其实也可以叫DarkDOM组件,除了具体的UI模式和配置语言,其他部分都是在DarkDOM这个库里实现的。

DarkDOM简单来说就是在两段DOM树之间建立联系,一边是开发者编写的HTML,一边是用户实际看到和交互的HTML,开发者只需要跟自己编写的HTML打交道,DarkDOM会负责将变化反映到用户看到的HTML上,也可以把交互事件转发过来。用户看到的HTML由组件开发者实现,组件开发者不需要了解应用开发者怎样编写HTML,而是用独立的model数据写渲染代码。具体的设计和示例可以看这个完整版的大图。



CardKit里三种最基础的交互组件,其实就是Moui里的基础组件,Moui是一个面向对象的、专注于抽象交互行为的UI库,不涉及具体的HTML结构和外观。CardKit在Moui组件的基础上扩展了一些应用层的功能,然后在事件代理里面,把特定形式的html元素作为宿主传给这些组件的实例。



CardKit也用了其他一些自己开发的模块,比如DollarJS是一个移动优先、极简化的DOM操作库,既提供jquery API的易用性,也提供接近原生API的性能,并且可以避免jquery的很多弊病,比如DOM被意外清除后在内存里残留垃圾数据。



CardKit和DarkDOM的很多理念其实都已经是越来越明显的趋势,比如声明式风格、组件化、把HTML看作配置、负责维护DOM的视图层技术,因此跟一些新流行起来的开源项目比较都会发现相似的地方,但也有很多根本上的区别,比如解决不同范围的问题,提供不同的方案,这里简要列举了一些。



不过对于Shadow DOM和DarkDOM,可以有一些很相似的比较,这两个方案在一定范围内是完全等价的,可以实现相同的自定义元素和组件,但设计思路上有一些很有意思的差别,其中有些可能会造成更深的影响。比如这个示例图,圆圈代表DOM节点,方框代表一层组件封装,为了用扩展组件的方式实现结果中这颗节点树,Shadow DOM和DarkDOM的这两颗树有很多微妙的差别。



最后想分享一下实际应用中产生的结果和遇到的问题



在豆瓣主站的代码里,CardKit相关的部分出现在图上的红框里,可能很多人知道豆瓣网是用一个叫作『堂吉诃德』的、很老的pyton框架开发出来的,这个框架只提供了model和controller两层,视图是默认跟controller写在一起的,豆瓣后来把视图层拿出来,用mako模板来实现。从图上可以看到当框架收到移动设备的请求时,会先通过侦测UA来打上一个mobile app的标记,然后我们不是在controller,而是在接近视图层,也就是mako查找模板的时候,优先用带有mobile前缀的模板替换掉同名模板,用这种方法来确保每个桌面网页的网址,都可以有对应的移动风格,然后我们把模板分成三类,桌面网页和移动网页会有各自的布局模板和动作模板,移动网页的这些模板里只写组件配置,几乎不包含数据,而对于内容模板则尽量共用同一套代码,最终生成的移动网页里会包含但不显示桌面版网页的所有html内容,还会自动过滤桌面版的js和css,移动网页里的卡片组件会自动从桌面网页内容里提取数据。用这种方法来实现维护一套代码,又不互相成为包袱。



摸索和实施这套方案的过程,其实是蛮纠结的,可以看这条时间线。需要注意的是CardKit在13年3月就已经稳定了,但我们在13年大部分时间里,其实并没有像预想的那样去快速迭代移动UI,覆盖产品线的速度也没那么快,而到了年末,又回头来开发CardKit2,之所以会这样是因为我们踩了很多坑……



这些坑都是实践问题,有些是判断的过于乐观,比如我们项目开始于iOS6和Android4发布之后,感觉技术条件比较成熟了,就以mobile safari为标准去开发,对其他平台做适配和平稳退化,不但全面依赖div内的滚动来实现各种全屏视图,还用history api和hashchange实现了一种很精巧的hack,让页面之间的跳转和页面内视图的切换,都有完全一样的跟原生app接近的过渡效果,包括浏览器前进后退触发的跳转。为了尽可能保留原生实现,比如用原生的惯性滚动来实现横向列表的逐项切换,我们还又做了很多hack,由于移动浏览器在这些特性上的差异有很多都不能用特性来划分,所以大部分时候是直接侦测UA,最后的结果当然是陷入了各种浏览器和各种android设备的大坑。

还有开发过程中的理念问题,我们想直接解决产品问题,所以很快就开始自顶向下的设计,把实现过程中的抽象层次压扁合并,最终产出的CardKit是一个非常接近具体应用的框架,有很多约束和要求,对文档的要求非常高,卡片组件实现的很紧凑,新增和改进组件都很麻烦。



在设计和开发CardKit2的时候采取了很不同的作法,自底向上的构建了更通用的基础库,比如DarkDOM,形式上从框架改成了要求更少功能更强的库,同时完全避免hack,尽可能不做任何UA或特性侦测,避开不成熟的浏览器特性,在交互标准上做一些妥协。最终的效果,包括易用性、上手门槛、兼容性和稳定性都要好很多。

这个项目是开源的,欢迎有兴趣的同学来一起帮忙改进它:

https://github.com/douban-f2e/CardKit

最后要解释一个必然会被黑的问题:当前豆瓣的移动页面仍然还在使用CardKit1.5.1,并不能作为上面这些介绍的例子(新版的demo),豆瓣的移动页面上目前会遇到一些用户体验上的bug,也都是在CardKit2里彻底解决的,所以…且黑且努力罢,希望能尽快把全站都升级到CardKit2…

更新:

有两个后续讨论有兴趣可以看一下
http://www.douban.com/people/huangmou/status/1385103676/
http://www.douban.com/people/yajc/status/1385105498/
Dexter.Yy
作者Dexter.Yy
34日记 18相册

全部回应 13 条

查看更多回应(13) 添加回应

Dexter.Yy的热门日记

豆瓣
免费下载 iOS / Android 版客户端