前端早早聊大会,前端成长的新起点,与掘金联合举办。 加微信 codingdreamer 进大会专属小程序群。
第十三届|前端构建专场,8-15 直播,8 位讲师(蚂蚁金服/淘宝等),大会报名地址
本文是第十届 - 前端早早聊第 65 场,来自 DCloud 前端架构师 崔红保(uni-app 产品负责人) 分享讲稿简要整理版(完整版含演示请看录播视频和 PPT):
大家好,我是 DCloud 崔红保,很高兴受邀参加早早聊的这个活动,今天跟大家分享 uni-app
在跨端、性能方面的一些探索。
这是我今天要讲的主要内容,大致分为 4 个部分:
一句话介绍,uni-app
是一个使用 Vue.js
开发跨平台应用的前端框架。
这是 uni-app
的功能框架图,uni-app
将常用的组件和能力进行了跨平台封装,可覆盖大部分的业务需求,这些就是下图中第一行 uni-app
内置组件和 API。
在内置组件的基础上,uni-app
封装了很多扩展组件(比如 indexedList
)和模板(比如新闻模板、看图模板等),这些也都是跨所有平台的,即下图第二行的内容。
uni-app
相比其他跨端框架,有很多功能的拓展兼容,比如微信小程序自定义组件可同时运行到 App、H5、微信、QQ 小程序平台,实现了原有生态内容的最大复用。
在追求跨平台的过程中,uni-app
不牺牲平台特色,可优雅的调用平台专有能力,比如可以调用微信运动、微信卡劵等业务 API,这就是条件编译的能力,下面在跨端方案中会讲到。
uni-app
已被腾讯、京东、华为、ViVO、CSDN 等知名公司在各种产品线中所采用,用户众多、案例丰富,了解更多案例参考 uni-app案例。
uni-app
同时被阿里、华为、ViVO 等公司的开发者工具、编辑器所内置集成,助力跨端开发。
一个技术框架的成熟,除了框架自身的高度产品化外,生态的完善度也是更为重要的一环。
uni-app
在这方面很有优势,插件市场 有 2000 余款各种插件模板,热门插件下载量有 6 万 + 。
uni-app
实现了一套代码,同时运行到多个平台;如下图所示,一套代码,同时运行到 iOS 模拟器、Android 模拟器、H5、微信开发者工具、支付宝小程序 Studio、百度开发者工具、字节跳动开发者工具、QQ 开发者工具(底部 8 个终端选项卡代表 8 个终端模拟器):
实际运行效果如下,有没有很震撼?
眼见为实,欢迎扫码体验 hello-uniapp
,该示例实现一套代码,发行多家平台,用于演示 uni-app
的组件、接口、模板等能力。
业内主流的小程序跨端框架,基本都是编译器 + 运行时配合实现,uni-app
同样如此。
uni-app
遵循 Vue.js
语法规范,Vue.js
是单文件、三段式结构。而小程序是多文件结构,以微信为例,小程序有 wxml/wxss/js/json
4 个文件组成。
uni-app
会在编译阶段,将 .vue
格式的单文件拆分成小程序开发工具所接受的多文件。
Vue.js
和小程序都是典型的逻辑视图层框架,逻辑层和视图层之间的工作方式为:数据变更驱动视图更新;视图交互触发事件,事件响应函数修改数据再次触发视图更新。
Vue.js
和小程序这两个机制接近的框架之间,如何分工协同?
这就需要 uni-app Runtime
作为中间桥梁,uni-app
提供了一个运行时,打包到最终运行的小程序发行代码中,该运行时实现了 Vue.js
和小程序两系统之间的数据同步、事件同步以及生命周期管理。
具体来说,实现方式如 PPT 上原理图:
这样,开发者就可以将精力聚焦在 Vue.js
上,遵循 Vue.js
规范编写业务逻辑,也就实现了完整的 Vue 开发体验。
各家小程序规范各不相同,uni-app
如何制定统一开发规范,如何兼顾各家特有能力?
直观从文档上看,小程序主要分为框架、组件、API 三个部分,我们可以从这三个维度分别实现跨端兼容。
框架 编译器配置各家小程序的文件后缀,面向目标平台编译时动态生成新文件。
自动转换插值、列表、条件判断等语句:
组件 一个组件定义,通常有标签名、属性名、属性值、事件、事件回调几部分组成,跨端框架需抹平每个部分的差异。
API 一个接口定义,通常有前缀、方法名、参数、回调几部分组成,跨端框架需抹平每个部分的差异。
uni-app
的 API 前缀统一是 uni
,在运行时通过 Proxy
映射为对应平台的 API,如 wx
、 my
等。
以 showActionSheet
为例,微信和阿里在参数项、参数名称、事件信息等维度都存在差异:
uni-app
的做法是分平台做配置,比如发行到阿里小程序时:
uni-app
发行到 H5 平台,主要是按照小程序规范实现一套 SPA 框架,这里不详细阐述,提一点,注意处理因渲染引擎差异导致的布局差异。
如下图,小程序的底部选项卡是原生渲染的,其它页面内容则是 Web 渲染的,也就是所谓的混合渲染;而 H5 平台则全部是 Web 渲染。
这样的差异,会导致基于 fixed 定位的元素出现位置差异,如下:
因引擎及运行机制导致的类似差异有很多,跨端框架需要抹平这些差异,才能让跨端开发更顺畅。
uni-app
已将常用的组件、JS API 封装到框架中,开发者按照 uni-app
规范开发即可保证多平台兼容,大部分业务均可直接满足。
但每个平台有自己的一些特性,因此会存在一些无法跨平台的情况。
在 C 语言中,通过 #ifdef
、#ifndef
的方式,为 Windows、Mac 等不同 OS 编译不同的代码。 uni-app
参考这个思路,提供了条件编译手段,在一个工程里优雅的完成了平台个性化实现。
uni-app
追求极致的性能体验,做了很多工作,本次主要讲解如下几点:
我们从 swipeaction
这个例子讲起,需求是用户在列表项上向左滑动,右侧隐藏的菜单跟随用户手势平滑移动。
若想在小程序架构上实现流畅的跟手滑动,是很困难的,为什么?
我们回顾一下小程序架构,小程序的运行环境分为逻辑层和视图层,分别由 2 个线程管理,小程序在视图层与逻辑层两个线程间提供了数据传输和事件系统。这样的分离设计,带来了显而易见的好处:
环境隔离,既保证了安全性,同时也是一种性能提升的手段,逻辑和视图分离,即使业务逻辑计算非常繁忙,也不会阻塞渲染和用户在视图层上的交互。
但同时也带来了明显的坏处:
基于这样的架构设计,我们回到 swipeaction
,分析一次 touchmove 的操作,小程序内部的响应过程:
实际上,用户滑动过程中,touchmove 的回调触发是非常频繁的,每次回调都需要 4 个步骤的通讯过程,高频率回调导致通讯成本大幅增加,极有可能导致页面卡顿或抖动。为什么会卡顿,因为通讯太过频繁,视图层无法在 16 毫秒内完成 UI 更新。
为解决这种通讯阻塞的问题,各家小程序都在逐步提供对应的解决方案,比如微信的 WXS、支付宝的 SJS、百度的 Filter,但每家小程序支持情况不同,详细见下表。
另外,微信的关键帧动画、百度的 animation-view
Lottie 动画,也是为减少频繁通讯的一种变更方式。
其实,通讯阻塞是业界普遍存在的一个问题,不止小程序,react native
、weex
等同样存在通讯阻塞的问题。只不过 react native
、weex
的视图层是原生渲染,而小程序是 Web 渲染。我们下面以 weex
为例来说明。
大家知道,weex
底层使用的 JS-Native Bridge,这个 Bridge 使得 JS 和 Native 之间的通信会有固定的性能损耗。
继续以上述 swipeaction
为例,要实现列表项菜单的跟手滑动,大致需经如下流程:
同样,手势回调事件触发的频率是非常高的,频繁的的通信带来的时间成本很可能导致界面无法在 16ms 中完成绘制,卡顿也就产生了。
weex 为解决通讯阻塞,提供了 BindingX 解决方案,这是一种称之为 Expression Binding 的机制,细节不展开了,有兴趣的同学可以到 weex 官网查看。
React Native 同样存在类似问题,为避免频繁的通信,React Native 生态也有对应方案,比如 Animated
组件及 Lottie 动画支持。以 Animated
组件为例,为实现流畅的动画效果,该组件采用了声明式的 API,在 JS 端仅定义了输入与输出以及具体的 transform 行为,而真正的动画是通过 Native Driver 在 Native 层执行,这样就避免了频繁的通信。然而,声明式的方式能够定义的行为有限,无法胜任交互场景。
uni-app
在 App 端同样面临通讯阻塞的问题,我们目前的方案是采用类似微信 wxs
的机制(我们叫 renderjs
),但放开了 wxs
中无法获取页面 DOM
元素的限制,比如下图中多个小球同时移动的 canvas
动画,uni-app
在 App 端的实现方案是:
Tips:大家需要注意,并不是所有场景都是原生性能更好,小程序架构下,如上多球同时移动的动画,原生 Canvas并不如在 wxs(viewjs)中直接调用 Web Canvas。
下表总结了跨端框架在通讯阻塞方面的解决方案。
回顾下 uni-app
的运行时原理,Vue.js
负责数据管理,小程序负责页面渲染,因此我们可以得出如下结论:
换句话说,Vue.js 的 vnode 管理在小程序端没有意义,徒增资源消耗,应该移除。
对应着 Vue 的执行流程,我们大概可以做三方面优化:
修改 Vue.js 源码后,Vue Runtime 减少了1/3,提升运行性能的同时,还提升了小程序加载性能。
假设我们有更改多个变量值的需求,示例如下:
change:function(){ this.setData({a:1}); ... //其它业务逻辑 this.setData({b:2}); ... //其它业务逻辑 this.setData({c:3}); ... //其它业务逻辑 this.setData({d:4}); } 复制代码
如上 4 次调用 setData,会引发 4 次逻辑层、视图层数据通讯。这种场景,开发者需意识到 **setData **有极高的调用代价,自己需手动调整代码,合并数据,减少数据通讯次数。
部分小程序三方框架已内置数据合并的能力,比如 uni-app
在 Vue Runtime 上进行了深度定制,开发者无需关注 setData 的调用代价,可放心编写如下代码:
change:function(){ this.a = 1; ... //其它业务逻辑 this.b = 2; ... //其它业务逻辑 this.c = 3; ... //其它业务逻辑 this.d = 4; } 复制代码
如上 4 次赋值,uni-app 运行时会自动合并成 {"a":1,"b":2,"c":3,"d":4}
一条记录,调用一次 setData 完成所有数据传递,大幅降低 setData 的调用频次,结果如下图:
减少 setData 调用次数,还有个注意点:后台页面(用户不可见的页面)应避免调用 setData。
假设我们有一个 “列表页 + 上拉加载” 的场景,初始化列表项为 “item1 ~ item4”,用户上拉后要向列表追加 4 条新记录 "item5 ~ item8",小程序代码如下:
page({ data:{ list:['item1','item2','item3','item4'] }, change:function(){ let newData = ['item5','item6','item7','item8']; this.data.list.push(...newData); //列表项新增记录 this.setData({ list:this.data.list }) } }) 复制代码
如上代码,change 方法执行时,会将 list 中的 "item1 ~ item8" 8 个列表项通过 setData
全部传输过去,而实际上变化的数据只有 "item5 ~ item8"。
开发者在这种场景下,应通过差量计算,仅通过 setData
传递变化的数据,如下是一个示例代码:
page({ data:{ list:['item1','item2','item3','item4'] }, change:function(){ // 通过长度获取下一次渲染的索引 let index = this.data.list.length; let newData = ['item5','item6','item7','item8']; let newDataObj = {};//变化的数据 newData.forEach((item) => { newDataObj['list[' + (index++) + ']'] = item;//通过list下标精确控制变更内容 }); this.setData(newDataObj) //设置差量数据 } }) 复制代码
每次都手动计算差量变更数据是繁琐的,新手不理解小程序原理的话,也容易忽略这些性能点,给 App 埋下性能坑点。
此处建议开发者选择成熟的小程序三方框架,这些框架已经自动封装差量数据计算,对开发者更友好。比如 uni-app
借鉴了 westore JSON Diff
库,在调用 setData 之前,会先比对历史数据,精确高效计算出有变化的差量数据,然后再调用 setData,仅传输变化的数据,这样可实现传递数据量的最小化,提升通讯性能。如下是一个示例代码:
export default{ data(){ return { list:['item1','item2','item3','item4'] } }, methods:{ change:function(){ let newData = ['item5','item6','item7','item8']; this.list.push(...newData) // 直接赋值,框架会自动计算差量数据 } } } 复制代码
Tips:如上 change 方法执行时,仅会将 list 中的 "item5 ~ item8" 4 个新增列表项传输过去,实现了 setData 传输量的极简化。 _
下图是一个微博列表截图:
假设当前有 200 条微博,用户对某条微博点赞,需实时变更其点赞数据(状态);在传统模式下,一条微博的点赞状态变更,会将整个页面(Page)的数据全部通过 setData 传递过去,这个消耗是非常高的;而即使通过之前介绍,通过差量计算的方式获取变更数据,这个 Diff 遍历范围也很大,计算效率极低。
如何实现更高性能的微博点赞?这其实就是组件更新的典型场景。
合适的方式应该是,将每条微博封装成一个组件,用户点赞后,仅在当前组件范围内计算差量数据(可理解为 Diff 范围缩小为原来的 1/200 ),这样效率才是最高的。
提醒大家注意,并不是所有小程序三方框架都已实现自定义组件,只有在基于自定义组件模式封装的框架,性能才会大幅提升;如果三方框架是基于老的 template
模板封装的组件开发,则性能并不会有明显改善,其 Diff 对比范围依然是 Page 页面级的。
uni-app 接下来会在用户体验、开发效率两个方向上努力。
先说用户体验的问题,主要也是两个方面:
如果你也想快速搭建的自己的小程序引擎,并更优的解决如上体验问题,该怎么办?
欢迎使用 uni 小程序 SDK,如下为录屏演示:
uni-app 小程序 SDK 具备如下几个特征:
uni-app
插件,uni-app
插件市场目前已有上千款成熟插件。关于小程序 SDK 更多资料详见:nativesupport.dcloud.net.cn/。
开发效率应该从跨端、跨云两个维度进行分析。
跨端开发 目前的小程序都带有明显的厂家属性,每个厂家各不相同。之前更遭,阿里内部有多套小程序(支付宝、淘宝、钉钉等),幸好阿里圆老板给力,目前已基本统一。但腾讯体系下,微信和 QQ 小程序依然是两队人马,两套规范。
小程序之前是手机端的,去年 360 出了 PC 端小程序。
接下来,会不会还有其它厂家推出自己的小程序?会不会有新的端的出现?比如面向电视的小程序、面向车载的小程序?
一切皆有可能。
逐水草而居是人类的本能,追求流量依然是互联网的制胜法宝。当前的小程序宿主,都是亿级流量入口,且各家流量政策不同。比如微信的流量最大的,但有各种限制;百度和头条系是支持广告投放的,通过广告投放,可以快速获得大量较为精准的用户;百度小程序还有个 Web 化的功能,可以通过将 Web 的搜索流量,转化成小程序的流量。
面对众多小程序平台及各自巨大的入口流量,开发者如何应对?
等待 w3c 的小程序标准统一,短期不太现实。当下,若想将业务快速触达多家小程序,借助跨端框架应该是唯一可行的方案。
跨云开发
开发商借助 uni-app
或其它跨端框架,虽然已可以开发所有前端应用。但仍然需要雇佣 PHP 或 Java 等后台开发人员,既有后端人员成本,又有前/后端沟通成本。
腾讯、阿里、百度小程序虽陆续上线了云开发,但它们均只支持自己的小程序,无法跨端,分散的服务器对开发商更不可取。
故我们认为跨厂商的 Serverless 是接下来的一个重点需求,开发者在一个云端存储所有业务数据及后端逻辑,然后将前端小程序发行到各家小程序平台,也就是“一云多端”模式。
uniCloud
是 DCloud 联合阿里云、腾讯云,为开发者提供的基于 Serverless 模式和 JS 编程的云开发平台,目前已有上万开发者使用。
这里顺便打个广告,欢迎各位参加 DCloud 插件大赛,贡献轮子的同时,顺道领个 iPhone 手机啥的也挺好 😝。
我们是一家有极客追求的创业公司,无管理的自驱型团队,欢迎对跨平台、Serverless 感兴趣的小伙伴加盟,详见:dcloud.io/hr/。
推荐一本我最近正在读的书《闪电式扩张》,它会启发你从不同的视角看待这个世界,比如在扩张时期,要坦然接受混乱及糟糕的管理,甚至可以忽略部分客户的诉求。
我的分享到此结束,欢迎大家体验并使用 uni-app
,传送门:uniapp.dcloud.net.cn。
如有疑问,也欢迎到 GitHub 提交 issue 交流。
谢谢大家!
本文使用 mdnice 排版