做者:范怀宇(轻芒联合创始人、CTO)前端
感谢掘金的邀请,可以有机会在这里跟你们分享一些轻芒作小程序的具体实践,结合咱们在作轻芒杂志过程当中的技术选择,重点和你们聊一些轻芒杂志交互实现的经验。小程序
看整个客户端的技术发展会发现,从「端」的技术来说是愈来愈薄的。原来作 Windows 开发,你可能面临不少的光碟,很是厚的 MSDN 的文档,还有海量的 Windows32 API。而到小程序,已经愈来愈薄,须要了解的细节也更少。这是由于,平台上的框架会变得愈来愈厚,咱们开发者入手一个平台会变得愈来愈简单。今天「端」的发展趋势是,用户在哪里,「端」就在哪里,咱们更多的是来适应「端」,并且不管在哪一个「端」都须要提供最好的产品体验。后端
轻芒在小程序发布的第一天就上线了和小程序相关的产品,包括为用户推荐高品质内容的轻芒杂志,咱们也把产品经验开放给内容创做者使用,经过轻芒小程序+,他们无需代码就能够搭建本身的小程序。微信小程序
除了内容的呈现和渲染,还有很重要的一点是要处理好全部和用户的互动,这块其实占了前端很大的开发量,所以,会有的人可能前端开发无聊,有的人则会以为很是有魅力。作交互其实没有什么银弹。在软件开发里咱们一直会须要一个「银弹」,就是「当我知道那个秘籍以后,我就能够作得很是轻松,什么事情均可以变得很是潇洒」,其实并不存在。大量的实践混杂在细节里,包括我今天和你们分享的不少东西,听上去没有那么神奇,甚至说有的一点都不工程,但这是真正的实践。你真正要把一些东西作好,不少都是在细节里面的。性能优化
首先,我先介绍一下小程序的平台特色,由于全部的前端开发,都是在一个平台上进行,你得对这个平台很是熟悉,就像在一个舞台上作演出,你须要知道舞台在哪里、光在哪里、声音在哪里等等。你须要了解这个平台的特性,才能在后面的实践中更加自如。微信
跟我经历过的其余平台相比,首先我以为小程序是个很是纯粹的数据驱动的前端平台。好比,如今去用 Web 或者 Android 开发,若是你须要在界面点一个按钮来隐藏一个地方,或者点一个按钮来展开某个地方,你会怎么作?按刚刚 MVC 模型,你可能不会穿过 Controller 去到 Model,而后改了 Model 再回来,不会走这个路径。你确定会在前端用一些简单的脚本或技巧,让这个东西隐藏起来、再展开,这样不会影响核心数据模型,并且很便利。框架
但在整个小程序里其实没有这一块东西,你不能操做任何 DOM 节点,这意味着无论你作什么事情,包括改变界面的任何一点点状态,都须要经过控制层传到逻辑层,用 js 通过一些计算,再调用 setData 函数,触发对 Data 变化的比对,从新对界面进行一次渲染,这是小程序整个的内核驱动模型。这个模型坦白讲有不少部分很是重,会限制你作一些特别灵活的实现,但对咱们开发者来说,只有理解这个模型,才会知道若是去作开发,哪些环节须要特别注意。ide
还有一个你们很是熟悉的点,小程序是传统的 Web 组件混着一些原生组件,就是用本地语言,好比说 iOS 上的 Objective C 或者是 Android 上 Java 来实现的一个本地组件,这和 Web 上面实现的组件是不同的。小程序采起的技术路线,是所谓的分层渲染,底下是 Web 层,上面是原生层,当我滑动 Web 层时,原生层会接到一些事件,它会尝试跟 Web 层联动,但它们其实不是一块儿动的,中间有一个延迟,这个延迟会带来不少麻烦事情,尤为是因为这个分层是原生层永远活在 Web 层上面,这也会给交互设计、交互实践带来一些麻烦。函数
还有一个特点是单窗口,固然在移动时代其余平台也如此,在一个交互页面,咱们只会跟一个窗口打交道。小程序特别的一点是它完全取消了窗口这个概念,可能咱们都感受不到窗口的存在。若是你们作过 Android,就知道 Android 能够作悬浮窗,因为小程序平台机制的限制,悬浮窗这种交互形态是没法实现的。性能
以上是我总结的小程序平台交互设计的显著特色,这也是咱们后面围绕小程序平台作交互设计、交互实践的一个很重要的起点。
下面跟你们分享一些轻芒的案例,首先讲一个你们用得最多的:列表。列表为何重要?理清楚列表的逻辑对于整个产品后续的实现、对于工程师更好的去理解业务和设计,是很是重要的一个环节。在传统的通常界面组件里,列表是最复杂的一个模块,它会有不少数据的联动,它的交互、数据传递、事件分发都会涉及到不少问题,这也是咱们为何要妥善思考列表怎么作。并且在不少场景里,长列表的性能是不少性能瓶颈的来源,这也是须要额外注意的地方。
什么叫列表?好比下图左边是很是显然的列表,能够无限加载、不断滚动的内容流,咱们认为每个卡片都是一个列表项。右图看上去是一个内容详情页,在实践中咱们也把它作成了列表,咱们会把它每一个段落抽象出来,按照列表来渲染。
小程序里面最大的特点是,它并无一个原生的列表组件。咱们刚刚说 MVC,而 MVC 中最核心的是模型(Model)的设计,就是整个产品中的数据模型究竟是什么样的,这是真正影响开发时间的。若是模型设计得好,就像种了一棵树,树上长着枝桠,若是你把全部的数据模型抽象的很是漂亮,那树上的枝桠,也就是 View 和 Controller,实现起来会很是轻松。若是你的模型彻底不作抽象,随便收拾一下就开始作交互开发,那即使你在界面层选了不少漂亮的转型、用不少 fancy 的框架,你也很难把代码复杂度降下来,整个产品的复用度也不会高,这也是我为何会说若是拿一个项目最好从列表入手,由于列表的数据模型一般是最复杂的,列表清楚了,整个产品也就清楚了。
在轻芒里,好比刚刚那个瀑布流,咱们会抽象成一个 event 对象,固然,咱们不是在全部的小程序开发里都这么作,由于在其余小程序中业务不一样,抽象模型也可能不同,咱们配套的列表实现也会不同。在轻芒杂志,由于有很是复杂的卡片类型或者不一样类型的交互样式,咱们会把不一样类型的数据统一抽象成一个 event 对象,每一个 event 会有一个类型,而后会围绕 event 来设计列表控件。具体的实现,咱们用了微信的模板(Template)来作抽象和封装,这是由于咱们这个项目作太早了,咱们作的时候没有任何开源项目,只可以所有本身封装。
这里面最重要的实际上是对 event 对象的抽象,也就是产品中最核心的数据模型,若是这个搞清楚了,你会发现后面东西会变得很简单。在这个代码中,每个卡片的渲染,可能有两个不一样的实现方式,有一些卡片是用模板实现的,好比说 single-card-*,在实现中,咱们会直接用 single-card 加上具体的 type 来实现列表项,好比说 single-card-article、single-card-image、single-card-video 等等。在微信有了自定义组件以后,咱们把一些后来实现的新卡片放成了自定义组件,好比这里的 universe card,咱们把新的卡片用了新的方式来作。
早期由于咱们用 Template 对界面元素作封装,这种封装包含了任何互动的实现。由于微信早期没有提供任何可封装机制,如今你也许能够经过不一样的开源框架搞定,早期惟一的办法就是把一些和界面互动相关的函数抽象成一些 Mixin 模块,而后在页面配置里直接整合进去,加强复用度。这个方案咱们如今依然在用,这也是在小程序里最经常使用的实践方案之一。
另外一方面,封装列表也是为了统一相关联的界面组件,好比统一的 loading 样式,统一的空白页样式,翻页的逻辑和多级嵌套等。举例来讲,全部数据的加载,在列表中定义了翻页方式,就定义了产品的客户端和服务端的交互方式。在轻芒,全部的服务端翻页都会使用 Next Url 模式,服务端会告诉客户端有没有下一页,有的话客户端滚到底就会加载下一页。这告诉咱们,若是服务端具备统一的 API 模式,客户端开发会变得很轻松,抽象和复用能够作得更充分。固然,这反向也约束了服务端,须要服务端提供统一的或者整合的 API,这样落地到公司所有团队,能够提高总体的效能。对前端来说,也只有这样的方案才是可以极大地简化开发复杂度。
列表实现中,还有一个和小程序特性特别相关的点,就是如何在列表中嵌入一个原生组件,好比我想在列表里播视频,我想在列表里嵌个地图、嵌个输入框,该怎么办?若是用微信原生组件的渲染机制,若是嵌一个视频在内容流里,它可能会遮住不少你想弹出来的对话框;有可能你想实现页面快速滚动,视频会有很是慢的拖影,这都很影响产品体验。
列表实现中还有一些问题,就是如何处理数据和状态。每一个列表项里面会包含不少元素,好比列表项里会有一篇文章的基本信息,同时右下角有个交互按钮,用户点击以后按钮状态发生变化,对轻芒来说这就是「马克」,而这些都不属于原生数据,会随着用户的交互而变化,所以被称为状态。在早期小程序开发中,你们可能不太会区分状态和数据,会把数据和状态放到同一个数据模型上,由于实现逻辑很是复杂,在纯数据驱动的模式下会碰到很是多问题。
而轻芒的实践经验,就是要让数据和状态分离。好比拿到数据后,咱们会把数据分红两部分,一部分是原生数据,它们不会随着用户交互而发生变化,用列表来进行存储;而另外一部分是状态,它会随着用户交互发生变化,这里咱们会用字典进行存储。图示的代码中, ui-switch 和 subscribed 的对象都存储了状态信息,当用户对特定元素进行操做后,只须要在控制层改变 switch 对应 id 的某一个值,就能够对界面进行快速的更新。这看上去是一个很是小的优化,但若是你在全部界面交互中都能把控好这一点,带来的收益是很是大的。它还有一个特色,是中心化。好比刚刚看到的视频播放,咱们同时只容许一个视频播放,用户点了某个视频后其余视频会中止播放,这时候你只须要在控制层把整个状态清空,对交互对象的状态从新设定便可,这都是一些听上去比较细节,但从实践来说很是重要的事情。
最后一个问题,是长列表性能优化,与其说是经验分享,不如说教训分享。在轻芒杂志中,咱们会用到大量的长列表,无限滚动的瀑布流、很是长的文章等等。这些都会致使列表变长,带来卡顿。为何?从小程序的渲染机制来看,从设定数据到界面渲染呈现,也就是调用 setData 函数到 setData 函数返回,这里面有两件可能比较耗时的事情,一个是数据比对:小程序须要找到有哪些数据更新了,这些数据关联了哪些界面元素。还有一件就是刷新界面:把更新的数据呈现出来,先在虚拟节点上作渲染,从新放到前端来呈现。而小程序最大的问题,在于这两个环节太「黑盒」了,它告诉开发者的信息很是少,所以优化起来就比较困难。咱们知道轻芒杂志的渲染都花在这里了,因此会有卡顿,但究竟是哪里耗了时间,其实咱们不能知道。
因而咱们作的性能优化方案就是试,不停地作二分,少作一部分逻辑,看看性能有没有好转。咱们最先把性能点放在 setData 不能追加上,虽然 setData 能够修改一个列表中的元素,但它一旦要增删元素,就要所有替换,这是如今微信整个数据驱动模型的一个核心问题。在这里,咱们尝试使用固定的列表项个数,先虚拟一百个列表项,可能只有十个有数据,若是新的元素须要追加,就只用修改而不须要所有替换。最后的结论是,有效果,但没太大的效果,而开发成本却增长了很多。后来咱们把性能问题,更多地放在了渲染上,发现一个有效的方案是,让界面设计变简单一些,须要渲染的元素变少,性能就行了很多。因此在长列表中,很大的瓶颈仍是在 DOM 渲染上,小程序并无为列表提供一个元素回收机制,这致使它会完整渲染整个列表,它并不关心这个列表项需不须要在界面上呈现、需不须要给用户看,这就会形成不少性能问题。
在这里咱们也没有太完整的经验,更多的是教训。那对于你们来讲,若是你能预见到你的产品会有很长列表,你能够提早考虑是否是要下降列表项的设计复杂度,控制每一个列表项 DOM 节点的数量,减小一些 DOM 节点上绑定的数据和事件等等,这些可能会对产品的性能有所优所。固然总体来看小程序的渲染仍是个黑盒,咱们能用的优化手段也比较少。总结一下,其实最想和你们聊的是交互的开发和业务是密不可分的,须要去和设计、后端、产品,一块儿去改进,在技术设计中更多的去理解需求。
咱们知道微信小程序是一个单窗口的交互平台,那什么是全局窗口呢?好比,上面左边这个分享卡片,它须要在任何轻芒杂志的页面都能被呼出,从而对页面进行分享。右边是咱们自定义的 toast,不少时候咱们须要的 toast 和微信小程序官方提供的不同,这时候就须要基于全局窗口来实现。但由于在小程序中没有全局窗口机制,对咱们而言惟一的办法就是经过把组件放进每个页面,随时能够呼出使用,模拟全局窗口,咱们把这个称为全局组件。在这个部分,我会重点来聊组件的封装,在小程序中最好的组件方案是怎样的。
刚刚你们听了不少第三方的框架,在轻芒,咱们主要还都是基于原生的机制来实现的。常见原生方式有两种:基于模板(Template) 和基于自定义组件的,这两种封装方式咱们都很经常使用。像这个代码展现的,咱们有的卡片使用了自定义组件来进行封装,有一些加载的进度条多是模板来封装的。但若是我把它作成全局组件,让每一个页面都包含,这两种方式都须要我在各个页面拷贝大量的重复代码,可能有上百行,一旦须要变动,就会须要同时修改整个小程序里几十个页面。这种方案咱们早期使用过,但发现效果很差,维护起来太麻烦了。因而咱们从新设计了一下,发如今小程序中还有一些更简单的封装方案更适合,就是你们今天可能不多用的 include 方案,就至关于直接引入一个别的代码块。这是一个很是传统的一个方案,咱们如今每每会强调组件,强调封装,要封装得漂亮,要让组件边界变得干净,但实际上在这样的场景里,咱们打破一些简单的组件的限制,反而会让事情变得简单。
咱们会写一个全局窗口的文件叫 global.wxml,把全部小程序中会用到的全局组件的数据和实现都放在这里面,图上的代码是叠过的,其实展开仍是比较长的,由于在 include 中不能再使用其余 Template 进行封装了。对应的,还配套一些 js 代码,咱们封装了 action.js 的文件,里面会包括各类全局组件的控制,好比模态对话框、输入对话框、toast、分享卡片等等。在最后使用里面,每个页面只要加两行,一个是 include global,一个 Mixin 这个 action.js,这解决了问题。
这个例子很是小,背后想聊的主要仍是组件封装的思路。作技术设计,始终要考虑怎么封装组件、怎么复用,由于这个对全部前端开发、服务端开发都是重要的,若是不复用、不去尝试作一些抽象,那么久而久之代码会变得很是散,难以维护、难以阅读。但怎么作封装,怎么作实践?除了把组件抽象得很是漂亮外,也能够尝试用一些边界更模糊的封装方案,能够显著的让代码变短变简单,更易于变动和维护。好比,在前面例子中,我要变动某个全局组件的样式,只要修改 global.xml,而调用该文件的地方是不用改的,用开闭原则来看,它是个很是知足开闭原则的实现策略。
在咱们掉过不少坑,当用户移动手指去修改选中区域的时候,若是咱们用了微信的条件判断 wx:if 去改变底色之类的,极可能会让整个控件树的结构发生变化,整个交互界面会陷入一个很是可怕的状态,彻底失去响应,咱们推测这是触发了微信底层的 bug。因此在这样的交互中,最好的方式是只改变 CSS 的,尽量不去改变界面元素的结构。
这里有个细节,在触发了调整马克选中区域以后,咱们再拖动手指的时候,整个界面是不滚动的,直到咱们放开手指以后,整个界面才能够从新开始滚动。这时候就用到了我刚刚说过的事件传播机制的流程处理,在小程序中,只要你设定了捕获函数就必须捕获事件,不像其余平台,能够经过捕获函数的返回值来控制是否须要捕获,而若是不进行捕获,后续又无法中断整个页面的滚动。那怎么办?咱们后来用了一些比较 Hack 的方式,咱们去动态的修改绑定的捕获函数名。当须要捕获时,就把须要的函数设定上,而中止捕获时,就把捕获函数设置为空白字符串,这样能够绕过它的捕获机制。这时候,就能够控制界面在何时,能够滚动页面,何时只能够调整选中区域了。
这个马克交互咱们改版过不少次,早期因为没有 query 任何 DOM 元素的机制,很是难以实现。而如今,微信提供了 query DOM 节点的 API,我能够大概知道 DOM 节点在哪里,如今咱们会反复用到 query 函数,了解界面状态,来调整能够进行的交互。整个马克的实现过程当中,工程师和设计师有很是多的讨论,设计师根据技术限制从新想一些交互方案,工程上不断地尝试采起一些相似 Hack 的手段去落地这些方案。若是你但愿把交互作得更往前,毫无疑问也会须要这样进行设计和实现,也会踩到一些平台的坑,这些经验也能够供你们参考。
上面分享的三个案例,不所有是单纯的技术,有很多技术外的事情。交互,看上去是一个纯前端实现的问题,其实它的实现是整个团队的事情,包括产品、设计、以及后端的 APIs 设计等等。也是平台能力和产品设计的交互融合。
另外,在小程序这个平台上尤为须要重视数据模型的设计,必定要让整个数据模型理解好业务,把整个数据模型设计得清晰,什么是数据、什么是状态、哪些应该隔离、哪些应该放在一块儿,把这些事情区分好,可让整个前端开发变得很轻松。此外,咱们在处理原生组件时也要特别当心,须要规避掉一些设计方案,不然会带来一系列 Bug。
这就是我今天跟你们分享的内容,这是轻芒的实践,我相信和你们的具体的业务需求、目标会不彻底一致。但也指望今天的分享能够给你们一些启发,若是你们能运用到其中的一些,对咱们来说就足够了。
谢谢你们。