小程序的当下和未来可能-----------引用

网友投稿 266 2022-09-22

小程序的当下和未来可能-----------引用

HTML5 于 2007 年在 W3C 立项,与 iPhone 发布同年。乔布斯曾期待 HTML5 能帮助 iPhone 打造起应用生态系统。但 HTML5 的发展速度并不如预期,虽然它成功地打破了 IE+Flash 垄断的局面,却没有达到承载优秀的移动互联网体验的地步。苹果公司在 iPhone 站稳脚跟后,紧接着发布了自己的 App Store,开启了移动互联网的原生应用时代。大家知道现在手机端主要是 iOS、Android 两大系统,实际上在早期有 3 大系统竞争,还有一个就是诺基亚的 MeeGo 系统,MeeGo 采用 C + HTML5 的双模应用生态策略。然而,C 的开发难度太大,HTML5 体验又不行,所以后来 MeeGo 就掉队了;与之对应,Android 依靠 Java 技术生态,在竞争中脱颖而出。于是在移动互联网初期,应用生态被定了基调 —— 原生开发。

国内有一批做浏览器的厂商,尝试去改进 HTML5。比如,百度在 2013 年的百度世界大会上发布了轻应用,通过给 WebView 扩展原生能力,补充 JS API,让 HTML5 应用可以实现更多功能。

不过这类业务没有取得成功,HTML5 的问题不止是功能不足,性能体验是更严重的问题。而体验问题,不是简单地扩展 JS 能力能搞定的。

与浏览器不同,Hybrid 应用是另一个细分领域,开发者使用 JS 编写应用,为了让 JS 应用更接近原生应用的功能体验,这个行业的从业者做出了很多尝试。我们 DCloud 公司是业内主流 Hybrid App 引擎提供方之一,我们提出了改进 HTML5 的“性能功能”障碍的解决方案 —— 通过工具、引擎优化、开发模式调整,让开发者可以通过 JS 写出更接近原生 App 体验的应用。

多 WebView 模式,原生接管转场动画、下拉刷新、Tab 分页,预载 WebView……各种优化技术不停迭代,终于让 Hybrid 应用取得了性能体验的突破。

C/S 的应用在每次页面加载时,仅需要联网获取 JSON 数据;而 B/S 应用除了 JSON 数据外,还需要每次从服务器加载页面 DOM、样式、逻辑代码,所以 B/S 应用的页面加载很慢,体验很差。

可是这样的 C/S 应用虽然体验好,却失去了 HTML5 的动态性,仍然需要安装、更新,无法即点即用、直达二级页面。

那么 C/S 应用的动态性是否可以解决呢?对此, DCloud 率先提出了“流应用”概念,把之前 Hybrid 应用里的运行于客户端的 JS 代码,先打包发布到服务器,制定流式加载协议,手机端引擎动态下载这些 JS 代码到本地,并且为了第一次加载速度更快,实现了应用的边下载边运行。

就像流媒体的边下边播一样,应用也可以实现边用边下。

在这套方案的保障下,终于解决了之前的各种难题:让 JS 应用功能体验达到原生,并且可即点即用、直达二级页面。

三、架构引发的性能坑点

1. 逻辑层 / 视图层通讯阻塞

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

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

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

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

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

大家知道,“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”的机制,简要介绍一下:

接收手势事件的视图,在移动过程中的偏移量以“x,y”两个变量表示;期望改变(跟随移动)的视图,变化的属性为“translateX”和“translateY”,对应变化的偏移量以“f(x),f(y)”表达式表示;将”交互行为"以表达式的方式描述,并提前预置到 Native UI 层;交互触发时,Native UI 根据其内置的表达式解析引擎,去执行表达式,并根据表达式执行的结果驱动视图变换,这个过程无需和 JS 逻辑通讯。

伪代码 - 摘录自 Weex 官网

​​{​​​​ anchor: foo_view.ref // ----> 这是"产生手势的视图"的引用​​​​ props:​​​​ [​​​​ {​​​​ element: foo_view.ref, // ----> 这是"期望改变的视图"的引用​​​​ expression: f(x) = x, // ----> 这是具体的表达式​​​​ property: translateX // ----> 这是期望改变的属性​​​​ },​​​​ {​​​​ element: foo_view.ref,​​​​ expression: f(y) = y, // ----> y 属性​​​​ property: translateY​​​​ }​​​​ ]​​​​}​​

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

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

2. 数据 / 组件差量更新

尽量少调用“setData”;每次调用“setData”,传递尽可能少的数据量,即数据差量更新。

减少 setData 调用次数

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

​​change:function(){​​​​ this.setData({a:1});​​​​ ... // 其它业务逻辑​​​​ this.setData({b:2});​​​​ ... // 其它业务逻辑​​​​ this.setData({c:3});​​​​ ... // 其它业务逻辑​​​​ this.setData({d:4});​​​​}​​

如上,4 次调用“setData”,会引发 4 次逻辑层、视图层数据通讯。这种场景,开发者需意识到“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”。

数据差量更新

​​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) // 设置差量数据​​​​ }​​​​})​​

​​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),这样效率才是最高的。

混合渲染

通过配置项创建的:选项卡、导航栏,还有下拉刷新;通过组件名称创建的,比如:camera、canvas、input、live-player、live-pusher、map、textarea、video;通过 API 接口创建的,比如:showModal、showActionSheet 等。

为什么要引入混合渲染

接下来的问题,为什么要引入原生渲染?以及为什么仅针对这几个组件提供了原生增强?其他组件为什么没有做原生实现?

这就需要我们针对每个组件单独进行分析思考,这里举了几个例子:

tabs/navigationbar:避免切换页面白屏,提升新窗口进入时的用户体验。虽然不使用原生的 tabbar 和导航栏,可以做出更灵活的界面,但在切换页面那短短 300ms 内,想保证页面不白屏,还是需要使用渲染更快的原生 tabbar 和导航栏;video:全屏后的滑动控制(声音、进度、亮度等);map:更流畅的双指缩放、位置拖动;input:Web 端的 input,键盘弹出时,只有“完成”按钮,无法让键盘显示“发送”“下一个”这样的按键。

提到“input”控件的原生化,可以稍微发散一下。

在 Android 平台,还有一种做法是基于 WebKit 改造,定制弹出键盘样式;这种方案,在键盘弹出和关闭时,input 控件都是 Web 实现的,故不存在“placeholder”闪烁的问题。

混合渲染引发的问题

原生组件虽然带来了更丰富的特性及更好的性能,但同时也引入了一些新的问题,比如:

层级问题:原生永远在最高层,无法通过“z-index”设置不同元素的层级,无法与 view、image 等内置组件相互覆盖,不支持在“picker-view”“scroll-view”“swiper”等组件中使用;

通讯问题:比如一个长列表中内嵌视频组件,页面滚动时,需通知原生的视频组件一起滚动,通讯阻塞,可能导致组件抖动或拖影;

混合渲染改进方案

既然混合渲染有这些问题,对应就会有解决方案,目前已有的方案如下。

方案⓵:创造层级更高的组件

既然其它组件无法覆盖到原生组件上,那就创造出一种新的组件,让这个新组件可以覆盖到 video 或 map 上。“cover-view/cover-image”就是基于这种需求创造出来的新组件;其实它们也是原生组件,只不过层级略高,可以覆盖在 map、video、canvas、camera 等原生组件上。

cover-view/cover-image 在一定程度上缓解了分层覆盖的问题,但也有部分限制,比如严格的嵌套顺序。

方案⓶:消除分层,同层渲染

既然分层有问题,那就消除分层,从 2 层变成 1 层,所有组件都在一个层中,“z-index”岂不就可生效了?

这个小目标说起来简单,具体实现还是很复杂的。

同层渲染

iOS 平台

创建一个 DOM 节点并设置其 CSS 属性为 overflow: scroll;通知原生层查找到该 DOM 节点对应的原生 WKChildScrollView 组件;将原生组件挂载到该 WKChildScrollView 节点上作为其子 View。

Android 平台

原生层创建一个原生组件(如 video);WebView 创建一个 “embed”节点并指定其类型为 video;Chromium 内核创建一个 WebPlugin 实例,并生成一个 RenderLayer;原生层将原生组件的画面绘制到 RenderLayer 所绑定的 SurfaceTexture 上;Chromium 渲染该 RenderLayer。

这个流程相当于给 WebView 添加了一个外置插件,且“”节点是真正的 DOM 节点,可将更多的样式作用于该节点上。

未来可能

更优秀的用户体验

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

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

跨云开发

开发商借助“uni-app”或其它跨端框架,虽然已可以开发所有前端应用。但仍然需要雇佣 PHP 或 Java 等后台开发人员,既有后端人员成本,又有前 / 后端沟通成本。

五、小结

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:PHP网站安全日志系统开发与部署
下一篇:被置顶的视频号直播 被“围猎”的12亿微信人!
相关文章

 发表评论

暂时没有评论,来抢沙发吧~