跨端跨栈连载 4/7:如何打磨 uni-app 的高性能和易用性

8,135 阅读19分钟

前端早早聊大会,前端成长的新起点,与掘金联合举办。 加微信 codingdreamer 进大会专属内推群,赢在新的起跑线。


第十四届|前端成长晋升专场,8-29 即将直播,9 位讲师(蚂蚁金服/税友等),点我上车👉 (报名地址):


正文如下

本文是第十届 - 前端早早聊第 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 上原理图:

  • uni-app Runtime 将小程序的数据绑定功能,托管给了 Vue;Vue 数据变更后,通过 uni-app Runtime  的数据同步机制将最新数据同步到小程序;
  • 小程序负责视图层展示及用户交互,用户在小程序视图中触发点击、滚动等操作后,先触发小程序的事件函数,接着通过 uni-app Runtime 的事件代理机制,触发 Vue 的事件函数,因此业务逻辑同样收敛在 Vue 中;
  • 小程序的生命周期,纳入 Vue 实例的生命周期中。

这样,开发者就可以将精力聚焦在 Vue.js 上,遵循 Vue.js 规范编写业务逻辑,也就实现了完整的 Vue 开发体验。

小程序跨端

各家小程序规范各不相同,uni-app 如何制定统一开发规范,如何兼顾各家特有能力?

直观从文档上看,小程序主要分为框架、组件、API 三个部分,我们可以从这三个维度分别实现跨端兼容。

框架 编译器配置各家小程序的文件后缀,面向目标平台编译时动态生成新文件。

自动转换插值、列表、条件判断等语句:

组件 一个组件定义,通常有标签名、属性名、属性值、事件、事件回调几部分组成,跨端框架需抹平每个部分的差异。

API 一个接口定义,通常有前缀、方法名、参数、回调几部分组成,跨端框架需抹平每个部分的差异。

uni-app 的 API 前缀统一是 uni,在运行时通过 Proxy 映射为对应平台的 API,如 wxmy 等。

showActionSheet 为例,微信和阿里在参数项、参数名称、事件信息等维度都存在差异:

  • 微信的弹出按钮数组为 itemList,阿里为 items;
  • 微信支持 itemColor 参数,阿里不支持;
  • 用户点击按钮索引,微信为 tapIndex,阿里为 index。

uni-app 的做法是分平台做配置,比如发行到阿里小程序时:

  • 将 uni-app 规范中的 itemList 转换为 items;
  • 丢弃 itemColor 参数,并控制台告警。

H5 平台

uni-app 发行到 H5 平台,主要是按照小程序规范实现一套 SPA 框架,这里不详细阐述,提一点,注意处理因渲染引擎差异导致的布局差异。

如下图,小程序的底部选项卡是原生渲染的,其它页面内容则是 Web 渲染的,也就是所谓的混合渲染;而 H5 平台则全部是 Web 渲染。

这样的差异,会导致基于 fixed 定位的元素出现位置差异,如下:

因引擎及运行机制导致的类似差异有很多,跨端框架需要抹平这些差异,才能让跨端开发更顺畅。

条件编译

uni-app 已将常用的组件、JS API 封装到框架中,开发者按照 uni-app 规范开发即可保证多平台兼容,大部分业务均可直接满足。

但每个平台有自己的一些特性,因此会存在一些无法跨平台的情况。

在 C 语言中,通过 #ifdef#ifndef 的方式,为 Windows、Mac 等不同 OS 编译不同的代码。 uni-app 参考这个思路,提供了条件编译手段,在一个工程里优雅的完成了平台个性化实现。

三、性能优化

uni-app 追求极致的性能体验,做了很多工作,本次主要讲解如下几点:

renderjs 解决通讯阻塞

我们从 swipeaction 这个例子讲起,需求是用户在列表项上向左滑动,右侧隐藏的菜单跟随用户手势平滑移动。

若想在小程序架构上实现流畅的跟手滑动,是很困难的,为什么?

我们回顾一下小程序架构,小程序的运行环境分为逻辑层和视图层,分别由 2 个线程管理,小程序在视图层与逻辑层两个线程间提供了数据传输和事件系统。这样的分离设计,带来了显而易见的好处:

环境隔离,既保证了安全性,同时也是一种性能提升的手段,逻辑和视图分离,即使业务逻辑计算非常繁忙,也不会阻塞渲染和用户在视图层上的交互。

但同时也带来了明显的坏处:

  • 视图层(Webview)中不能运行 JS,而逻辑层 JS 又无法直接修改页面 DOM,数据更新及事件系统只能靠线程间通讯,但跨线程通信的成本极高,特别是需要频繁通信的场景。

基于这样的架构设计,我们回到 swipeaction,分析一次 touchmove 的操作,小程序内部的响应过程:

  • 用户拖动列表项,视图层触发 touchmove 事件,经 Native 层中转通知逻辑层(逻辑层、视图层不是直接通讯的,需 Native 中转),即下图中的 ⓵、⓶ 两步;
  • 逻辑层计算需移动的位置,然后再通过 setData 传递位置数据到视图层,中间同样会由微信客户端(Native)做中转,即下图中的 ⓷、⓸ 两步。

实际上,用户滑动过程中,touchmove 的回调触发是非常频繁的,每次回调都需要 4 个步骤的通讯过程,高频率回调导致通讯成本大幅增加,极有可能导致页面卡顿或抖动。为什么会卡顿,因为通讯太过频繁,视图层无法在 16 毫秒内完成 UI 更新。

为解决这种通讯阻塞的问题,各家小程序都在逐步提供对应的解决方案,比如微信的 WXS、支付宝的 SJS、百度的 Filter,但每家小程序支持情况不同,详细见下表。

另外,微信的关键帧动画、百度的 animation-view Lottie 动画,也是为减少频繁通讯的一种变更方式。

其实,通讯阻塞是业界普遍存在的一个问题,不止小程序,react nativeweex 等同样存在通讯阻塞的问题。只不过 react nativeweex 的视图层是原生渲染,而小程序是 Web 渲染。我们下面以 weex 为例来说明。

大家知道,weex 底层使用的 JS-Native Bridge,这个 Bridge 使得 JS 和 Native 之间的通信会有固定的性能损耗。

继续以上述 swipeaction 为例,要实现列表项菜单的跟手滑动,大致需经如下流程:

  • 在 UI 视图上绑定 touch 事件(或 pan 事件);
  • 当手势触发时,Native UI 层将手势事件通过 Bridge 传递给 JS 逻辑层,这产生了一次 Native UI 到 JS 逻辑的通信;
  • JS 逻辑在接收到事件后,根据手指移动的偏移量驱动界面变化,这又会产生一次 JS 到 Native UI 的通信。

同样,手势回调事件触发的频率是非常高的,频繁的的通信带来的时间成本很可能导致界面无法在 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 端的实现方案是:

  • renderjs 中获取 Canvas 对象;
  • 基于 Web 的 Canvas 绘制动画,而非原生 Canvas 绘制。

Tips:大家需要注意,并不是所有场景都是原生性能更好,小程序架构下,如上多球同时移动的动画,原生 Canvas并不如在 wxs(viewjs)中直接调用 Web Canvas。

下表总结了跨端框架在通讯阻塞方面的解决方案。

定制 Vue,移除 vnode

回顾下 uni-app 的运行时原理,Vue.js 负责数据管理,小程序负责页面渲染,因此我们可以得出如下结论:

  • 小程序负责视图渲染,页面 DOM 由小程序负责生成,小程序只接受 data 数据传递;
  • Vue 的 vnode 遍历对比维度复杂,但 Vue 维护的 vnode 无法和小程序的真实 DOM 对应。

换句话说,Vue.js 的 vnode 管理在小程序端没有意义,徒增资源消耗,应该移除。

对应着 Vue 的执行流程,我们大概可以做三方面优化:

  • compile:optimize 过程可取消,因为该环节是为了标注静态文本节点,而 Vue 只负责数据,不需要关注 DOM 节点;
  • render function:不生成 vnode;
  • patch:不比对 vnode,因为 setData 仅能传递数据,所以我们只比对 data。

修改 Vue.js 源码后,Vue Runtime 减少了1/3,提升运行性能的同时,还提升了小程序加载性能。

减少 setData 调用次数

假设我们有更改多个变量值的需求,示例如下:

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 接下来会在用户体验、开发效率两个方向上努力。

更优秀的用户体验

先说用户体验的问题,主要也是两个方面:

  • 解决现有的性能坑点,比如前面分析的这几项,通讯阻塞、分层限制等,这里不再赘述;
  • 支持更多 App 的体验,更自由灵活的配置,比如高斯模糊。

如果你也想快速搭建的自己的小程序引擎,并更优的解决如上体验问题,该怎么办?

欢迎使用 uni 小程序 SDK,如下为录屏演示:

uni-app 小程序 SDK 具备如下几个特征:

  • 性能更高:支持 native 渲染,扩展 wxs,更高的通讯性能;
  • 开放性更强:更灵活的配置,支持更多 App 的体验;
  • 生态丰富:支持微信小程序自定义组件,支持所有 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/

hr.png

hr.png

荐书

推荐一本我最近正在读的书《闪电式扩张》,它会启发你从不同的视角看待这个世界,比如在扩张时期,要坦然接受混乱及糟糕的管理,甚至可以忽略部分客户的诉求。

尾声

我的分享到此结束,欢迎大家体验并使用 uni-app,传送门:uniapp.dcloud.net.cn

如有疑问,也欢迎到 GitHub 提交 issue 交流。

谢谢大家!