小程序自发布以来,为开发者和用户提供了一种轻量级的App。作为一种不需要下载安装即可使用的应用,它实现了应用“触手可及”的梦想,用户扫一扫或者搜一下即可打开应用。小程序也体现了“用完即走”的理念,用户不用关心是否安装太多应用的问题。 微信客户端为小程序的运行提供了框架支持,如service运行环境、页面缓存机制以及控件原生化支持等,本文将对这些部分实现原理做一一介绍。
6. 原生控件的创建与交互机制
小程序内部提供了部分非H5实现的原生控件。原生控件可以提供H5控件无法实现的一些功能,原生控件的用户体验感受上也会更加流畅,另外,使用原生控件减少了Objective C代码与WebView通信的流程,降低了通信开销。
以画布为例,前端提供了wx-canvas控件给开发者,当开发者在页面中设置一个画布标签<canvascanvas-id="xxx" ></canvas>,并调用绘制接口时,前端SDK将会有如下JSAPI的调用流程:
(图6. 画布控件原生化创建逻辑)
如上图所示,wx-canvas控件初始化时,将会通过Webview SDK的封装调用,执行客户端提供的“组件API”:insertCanvas接口以及updateCanvas接口(可选),绘制时通过调用客户端的drawCanvas接口,将绘制命令传递给客户端,客户端解析drawCanvas接口所带的参数,获取绘制命令集,并使用了Quarz2D来进行图形绘制。
insertCanvas通知客户端,在当前WebView上插入一个画布控件,客户端根据传入的位置和宽高参数来决定插入控件的位置和大小;
当开发者改变了wx-canvas控件的位置大小时,通过updateCanvas接口通知客户端,客户端对原生控件frame位置大小属性做对应的修改;
页面离开时,removeCanvas接口的调用将画布控件从webview上移除。
除了画布以外,Video组件对AVPlayer进行了封装,利用系统组件功能提供了边下边播的功能,并定制了原生化全屏等更加友好的用户操作界面;Map组件对QQ地图组件的封装将QQ地图的丰富功能引入到小程序,让开发者具有更广阔的开发想象空间;输入控件分别引入了iOS原生的UITexField和UITextView,提供了HTML输入框无法满足的定制化输入键盘等功能。
为了提供更加灵活可控的控件功能,小程序还对H5中的Toast、Alert、Picker、ActionSheet等控件做了原生化。这些组件是采用“开发API”的方式提供给开发者。
7. 原生控件插入到网页DOM节点
控件原生化带来了更加流畅的原生化体验和更加丰富的控件功能,但是同时也带来了新的难题。如前所述,原生控件是插入到webview控件上(实际实现时是插入到WKWebView下的WKScrollView下),如图7,网页元素总是绘制在WKContentView控件上——WKContentView负责绘制网页中的全部HTML元素,视频控件插入后将覆盖网页中的所有HTML元素:
(图7. 原生控件插入到WKWebView后将覆盖控件树中的HTML节点)
如上图,插入的原生控件必然总是盖住网页(节点树中越靠下的节点,显示层级越高),这样就会导致:
1
如果开发者期望在原生控件上覆盖一些自定义HTML元素,将无法被支持到。
2
所有的H5弹出元素都会被原生控件遮挡,比如alert对话框。这一问题可以通过将H5的弹出组件都原生化得以解决,如上节提到的Toast、Alert、Picker、ActionSheet的原生化;
3
如果开发者在div滚动条中插入原生控件作为div的子节点,预期原生控件应该随着父节点div滚动条的滚动而移动,并且超出div区域的内容应该被裁掉,但是由于原生控件是直接插入到webview下,与div之间没有关联,所以不会跟随移动也不会被裁减,在表现上会出现与开发者预期不一致的情况,影响用户体验。
为了解决这一问题,客户端尝试对WKWebView解析HTML元素的原理进行分析,WKWebView在进行HTML解析时,会根据页面DOM元素在WKWebView控件下生成对应的iOS原生控件,通过分析,普通情况下生成的原生控件与HTML节点无对应关系,但是在某些特殊情况下,一些特殊DOM元素会在WebView的对应位置生成位置、大小完全一致的原生控件,如包含overflow属性的DIV标签,如下图所示:
(图8. WKWebView解析HTML在客户端生成对应的原生控件示例)
如上图所示,WKWebView将在解析HTML时将该标签位置生成一个对应的UIScrollView控件。利用这个属性,我们可以在开发者期望插入原生控件的位置,预生成一个包含overflow标签的DIV节点,然后在插入原生控件时,将原生控件插入到该标签对应的UIScrollView上,就可以做到“原生控件不遮挡HTML元素”。例如将一个视频播放器插入到DOM节点以后,节点树如下:
(图9. 将视频控件插入到网页DOM节点后的节点树)
客户端采用的“原生控件插入到网页DOM节点”方案,具体实现原理如下:
a、WEB端预先在需要插入原生控件的预留位置插入一个具有overflow属性的DIV标签,并通过“组件API”insertContainer通知客户端该滚动条的位置、大小;
b、客户端根据insertContainer传入的位置和大小,在WKWebView下遍历找到这个DIV标签对应的UIScrollView(大小位置均一致),保存其对象指针,并分配一个id返回给WEB端;
c、当WEB端插入原生控件时,通过接口传入id通知客户端:该原生控件属于哪个div滚动条,客户端找到该滚动条对应的原生UIScrollView,并将控件插入到该UIScrollView下;
d、当页面的DOM元素发生变化时,需要通过updateContainer告诉客户端调整指定的原生控件的大小,客户端根据参数调整原生控件的大小(位置不需要调整,因为总是在相对于父控件的原点位置)。
插入DOM节点后原生控件事件处理。由于WKWebView会接管用户的所有操作事件,因此按照上述方案插入后,原生控件是无法响应用户事件的。因此需要对事件做特殊处理:通过重载WKWebView的hitTest方法,在该方法的处理逻辑中优先处理网页上的事件,如果网页未处理,再传递给原生控件。
8. 总结
微信客户端为小程序提供了整套运行环境:包括js脚本的运行时支持、小程序任务管理、service中的js脚本与webview之间的通信桥接机制,以及对复杂控件进行了原生化。从而为开发者及用户提供了良好的小程序体验。
--------------------------------------------------------------------------
原文作者:腾讯工程师王召伟。
来源:腾讯内部KM论坛。