作者简介
ZiLin Wang,前端开发者,函数式编程爱好者,最近沉迷于低代码平台和WebAssembly;Ivan Zhang,擅长前端打杂,最近专注于Sketch插件和DesignOps平台;Sheila,携程资深前端开发工程师,关注前端性能优化。
在进入页面的过程中,用户不可避免地会看到一个加载动画。但加载动画往往比较古板,如果加载耗时稍微长一点,用户就会失去耐心离开页面。为了让用户有更好的浏览体验,骨架屏是一种较好的渐进式加载方案。
骨架屏的实现,也是视觉稿直出代码实现方案的 MVP (Minimum Viable Product)。本文会探讨骨架屏方案的优劣,以及一种前端智能化的骨架屏代码自动生成方案实践。
早在2013年,Luke Wroblewski 就介绍了Skeleton Screens的概念。骨架屏作为一个空白的页面载体,它的作用是传递出一种页面正在渐进式加载中的信息。
A skeleton screen is essentially a blank version of a page into which information is gradually loaded.
Skeleton Screens 比单纯的 spin 优秀的地方,就在于让用户知道即将加载的页面信息结构,给予了一定的信息量,可以缓解用户等待页面的焦虑情绪。苹果也把骨架屏作为一种加载标准,在应用中推荐包含文本或者元素基本的轮廓。
UX 研究咨询公司 Nielsen Norman Group 提出了在提升 web / app 性能方面的四个重要的响应时间节点:0.1s、1s、4s以及10s。
在 1s 以内,是不需要额外的加载信息提示的,因为用户尚在心流之中;在 1-4 秒,需要 loading 指示器来告知用户正在加载中;在4s以上,则需要告知用户当前的加载进度,超过一定的时长需要有失败反馈,让用户进行重试。
除了一些前后端同构的方案可以快速返回前端数据,对页面进行直接渲染以外,大部分的页面都需要经过一定的加载时间,来获取服务返回数据进行展示。如果呈现给用户的是白屏,或者是一个固定的加载动画,都会让用户产生一种焦虑情绪,或者怀疑页面是否加载出来了。
骨架屏是一种有效减少用户负面体验的方案,通常在页面首屏或者一些关键性节点会进行骨架屏的渲染。同时,它也是缩短 FMP (First Meaning Paint) 的重要方式。
Lara Swanson 在 《Designing for Performance》 中提到,大部分用户期待在2秒内完成加载,超过3秒后会有 40% 的用户离开页面。骨架屏给出了页面的大体轮廓结构,让用户产生一种已经有内容返回了的错觉,也是一种有效降低用户焦虑情绪的方式。
如果返回服务的速度够快,或者页面的结构简单,对于骨架屏的需求是没有那么强烈的。推荐在以下场景中使用骨架屏:
如果我们能采用自动的骨架屏渲染的方式,也会大大减少我们的使用和维护成本,配置的灵活性高,保持高还原度,不要影响页面本身的加载性能。
首屏展现骨架屏,能够提前给予用户一定的信息量,在短时间内获取到用户的关注点,聚焦到他所关心的模块位置。
加载后使用真实的数据模块替换骨架屏结构,可以自然平滑地进行过渡。渐进式的渲染让用户可以更快地感知,提供更加良好的用户体验。
但是方案的缺陷在于人工成本稍高,需要针对设计师给出的首屏内容结构,手写一份对应的骨架屏代码。为了降低开发与维护成本,我们需要一套自动化的骨架屏生成方案。目前业界对于自动化骨架屏的实现方式有以下两种主流方案。
针对浏览器环境的 web 页面,可以使用 puppeteer 无头浏览器抓取页面相应的 DOM 节点,进行骨架屏的结构渲染。业界实现方案中,这样的方案最后通用化为平台自动抓取。
通过传入页面的 url 地址,使用 puppeteer 去打开需要渲染的首屏页面,抓取到整个页面的 DOM 节点结构后,给页面上的部分内容填充类 loading 态的灰色背景。
得到骨架屏后,接下来可以有两种使用方法:
优点:
缺点:
在上一小节中提到的 puppeteer 自动化,在方案设计的缺陷上面,除了要进行标签预处理、配置干预,最重要的是该方案只能适用于 web 端。
如果要采取这套方案,需要先使用 React Native Web 生成对应的 web 端代码。最大的弊端在于 DOM 节点嵌套过深,生成的代码内容过于冗余。而我们的源代码依然是 React Native 的,得到基于 web 的骨架屏代码也无法进行使用。
业界对于 React Native 中的骨架屏,就是提供一套标准化的骨架屏组件方案,让开发人员直接编写对应的骨架屏的代码。
在这之中,较为流行的 npm 组件库为 react-native-skeleton-placeholder以及 react-native-skeleton-content。前者的下载量更大,体积为17kb,更受欢迎,看上去虽然体积小很多,实际上又依赖于另一个 npm 库。
在 React Native 方面的实现方案更加偏向于在细节动画上面的展现,也就是对于目前最流行的 loading 动画效果的实现,从各个方向进行呼吸动态的闪烁效果。对于整个骨架屏的结构代码,则依然依赖于开发进行手工编写。
优点:
缺点:
经过以上两种方案的调研,浏览器环境中的实现侧重于自动抓取 DOM 节点,React Native 中的实现侧重于复杂动画效果的封装,都不满足我们想要达到的效果。
骨架屏代码的重复率非常高,如果让开发根据视觉稿去还原骨架屏,每次都是根据骨架屏的结构重新写一套对应的代码,工作效率也很低。并且骨架屏部分其实是没有任何业务逻辑与之关联的,其实也只是一个结构较为复杂的 spin 界面。
我们的预期是降低开发人员的工作量,让开发人员可以直接上手使用骨架屏,而不需要编写对应的代码。
我们实现的骨架屏方案主要基于 React Native,但从 DSL 层面来说,这样的方案可以移植到任何前端框架方案中进行实现。
最终实现的方案不仅达到了预期,还具有以下特点:
在这里视觉稿标注平台,使用的是携程内部的 kirby store 平台,它支持由设计师在本地配合使用 kirby 插件,同步 sketch 标注到 web 平台(即 kirby store),给开发同学直接使用。如有兴趣可阅读《携程机票Sketch插件开发实践》。
我们的实现角度是从视觉设计师给出的 sketch 设计标注稿,根据 sketch 文件的数据结构,转化为我们需要的前端框架骨架屏代码。
如何从普通的骨架屏 sketch 文件生成可用的代码片段?下面从 sketch 数据结构、通用算法、中间代码、平台特定代码几个维度进行阐述。
可以把 sketch 数据想象一个数据量比较大的 JSON 文件,里面包含很多我们需要的信息:
既然能够拿到 sketch 中所有包含数据的原始信息,那么就有可能通过某种算法生成一种可靠的中间代码来表示信息。其中,基于骨架图的特殊性 (几乎不用考虑文字图层),我们可以对当前算法进行适当的调优或者删减。
要想从一堆原始的图层数据中获取可用性比较高的中间代码,必须对现有的图层进行处理——将杂乱的图层删除、合并及整合,以便进行后续算法的优化。
骨架图通常的做法都是在原有的图层上放置一个新的图层 (通常为矩形) 来遮盖,这样看来,原有的被遮盖在下面的图层就看不见了,最后生成的代码就不应该包含它们,所以需要将这些不可见图层删除。
幸运的是,sketch 提供了层级这样一个概念,当我们发现某个图层被包含在更大的图层的时候,就比较这两个图层的层级,如果这个元素层级较小,那么它就是不可见的。如图:
这里的文字被矩形框包含了,从视觉上来看感知不到文字的存在,故其不应该存在于最终的代码中。 基于这种处理后我们生成的代码就会比较工整,没有太多不可见图层的干扰。
在具有背景色的场景,设计师往往会放置一个背景色的图层,然后将这些图层和其他需要这种背景色的图层编组,这样我们的图层中就会有两个大小相同的图层,如果不做处理,生成的代码就会出现多余的元素,而这是不必要的。
这里设计师将灰色背景 Mask 及其他骨架图组件编组为 Card_主题,如果我们不合并图层,那么这个编组的图层就是冗余的。
基于这种场景,合并相同大小的图层就可以做到图层提升,使得生成的代码元素更少(图层需要考虑颜色、透明度、层级等信息,具体逻辑可能比较繁琐)。
由于设计稿中的编组信息通常是设计师按照自己的意愿添加的,不具备一定的规范,这些内容的参考价值不是很高,所以我们在这一步需要删除掉这些无用图层然后按照算法的通用逻辑生成相应的图层。
Note: 上面合并图层的步骤也会有删除编组图层的过程,这里删除的主要是那些没有被合并的编组图层。
由于设计稿中带有状态栏图层而状态栏是手机固有的,所以要删除这些图层。
目前的做法是根据图层名称查找名为 Carrier、Wi-Fi、Battery 的父级图层然后删除 (可能有风险,但对于骨架屏来说风险极低)。
基于上一步图层整理,我们目前的图层处于扁平的数据结构,这一步就是将这些扁平的数据结构重新组合成树状的数据结构。
判断两个图层的包含关系很简单,根据图层的坐标和宽高能够做到。这里面,三个灰色的骨架图Block组件被包裹在白色的图层中。
需要注意的是,如果我们判断某个图层被包含在另一个图层里面,不应该急于将这个图层放进去。这是因为如果这个图层还可以放到其他的图层里,并且这个图层的层级比刚才的高,那么这个图层很有可能是应该放在当前的这个图层里。
因为我们删除了不可见图层之后所有的图层理论上可见的,如果放进之前的图层里面,在生成的代码里面不可见(被遮挡)。这里的文本应该包含在红色的矩形框内,如果我们错误的将其包含在灰色的矩形框内,那么生成的文本将会不可见。
经过处理后的图层拥有一定的树状结构,但是和能生成代码的树状结构相差甚远。横竖切割的目的是将目前的树状结构细分,判断哪些是行、哪些是列、哪些是不可分割的(元素相交或者只有单个元素)。
具体的做法是我们利用投影切割来进行横向和和纵向切割。如下面的设计稿我们横向和纵向切割如下:
然后按照生成的结构中节点数目和扰乱布局指数来选择最优解。
扰乱布局指数:指flex布局中那些明显偏离主辅轴方向的最小元素个数。
这一步主要需要做一些样式处理方面的工作。
这部分主要处理图层的基础样式信息部分,例如边框、圆角、背景等等,将其生成可复用的平台无关的中间代码表示形式。
上面横竖切割的结果使得我们能够判断哪些是行、哪些是列、哪些是不可分割的,本步骤是将这些生成的元素添加平台无关的样式代码例如 margin、padding、top 和 flex 等。
通过以上的处理步骤,我们就能够从一个杂乱的设计图层,最后生成完整的、高可复用性且平台无关的特定代码。
我们选择UIDL来作为中间代码的表现形式。主要用到UIDL中的ComponentUIDL和UIDLElementNode。关于UIDL的playground,可以点击这个链接。当然,UIDL提供生成平台无关代码的Generator API,我们也可以通过代码的形式来生成。
有了中间代码的表现形式,我们可以把它转换为任意一种对应的代码表达。在本文的实践方案中,标注了骨架屏的视觉稿内容。进行最小可行产品的实现,除了验证方案的准确性以外,更重要的是实用性。
RN 骨架屏组件的实现,在前端智能化中,是为了达到一个简化代码结构、高可复用性的目的代码生成内容。
骨架屏本身的元素就是比较固定的,针对文字部分有块级元素,针对图片部分也有带有圆角的块级元素等等。这里根据视觉设计的内容,主要提取了几大类:卡片、块以及分割线。提取大类的主要目标,是为了简化实现,把容器和元素分开,注入每个类别的公用样式。
在这个过程中,也需要遵从通用的前端组件设计原则。最终实现的目的代码,既可供开发进行单独使用,也可作为视觉稿直出的目的代码。
除了简化实现结构以外,对于 react-native 组件还需要实现通用的 loading 动画效果。最后的目的代码内容,大致如下:
<Skeleton style={{ flex: 1 }} commonStyle={{
block: {
backgroundColor: '#EEF1F6',
borderRadius: 4
},
divider: {
height: 1,
backgroundColor: '#EEF1F6'
},
card: {
backgroundColor: '#FFF'
}
}}>
<View style={{ backgroundColor: '#CCD6E5', padding: 16, paddingTop: 76 }}>
<Skeleton.Block height={28} width={92} />
<Skeleton.Block height={16} width={92} marginTop={8} />
<Skeleton.Block height={44} width={720} marginTop={10} />
</View>
<Skeleton.Card marginTop={-8} borderTopLeftRadius={8} borderTopRightRadius={8}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end' }}>
<Skeleton.Block height={45} width={132} />
<Skeleton.Block height={20} width={132} />
</View>
<Skeleton.Divider />
</Skeleton.Card>
</Skeleton>
按照目的代码的结构,需要将相应的图层识别为 Divider、Card、Block、LinearGradient 和 Container。
根据上面的识别,可以生成一段可用的 react-native 代码。
展示部分主要通过 Sketch Symbol 标记,使用 UIKit 组件库中的标准骨架屏占位符 Symbol,在数据处理环节会将使用该 Symbol 的设计稿标记。通过识别被标记的设计稿,设计平台将设计稿 JSON 数据发送到 DSL 处理接口,接口经过处理后返回对应代码。
设计师使用通用骨架屏占位符 Symbol 制作设计稿,设计稿通过 Sketch 插件上传到设计平台。平台识别设计稿数据包含通用骨架屏占位符 Symbol,标记该设计稿类型为骨架屏。在标注模式下,类型为骨架屏的设计稿的右侧信息栏会展示对应的代码展示入口。
点击进入骨架屏代码展示状态后,右侧信息栏会展示对应骨架屏代码以及 react-native-web 渲染出的实时效果预览,开发人员可以根据自身需要选择对应代码使用。
骨架屏也许是一种能带来更好用户体验的实现方式,我们希望在不断追求用户体验的同时,提升开发效率,减少重复劳动。
本文的方案还存在一些不足之处,例如暂未支持开发自主选择部分区域生成代码。但是自动生成骨架屏只是我们在前端智能化方向上的一个探索,希望未来前端智能化能够应用在更多的场景上。
团队招聘信息
我们是携程机票研发团队,负责携程APP/PC端机票业务开发及创新。机票研发在搜索引擎、数据库、深度学习、高并发等方向持续不断地深入探索,持续优化用户体验,提高效率。
在机票研发,你可以和众多技术顶尖大牛一起,真实的让亿万用户享受你的产品和代码,提升全球旅行者的出行体验和幸福指数。
如果你热爱技术,并渴望不断成长,携程机票研发团队期待与你一起腾飞。目前我们前端/后台/数据/测试开发等领域均有开放职位。