阅读 1469

高级前端进阶系列 - webview

从jsBridge功能说起

H5页面被webview承载,需要实现 js 和 native 的通信,常用的功能如图:


例如:


// 打开新容器承载H5
this.state.jsBridge.openWebView({   
 'action': 'https://XXX.XXX.com',   
 'login': true
})复制代码

// 销毁webview
this.state.jsBridge.quitAction()复制代码

// 控制客户端顶部back按钮-触发回调函数
this.state.jsBridge.handleBackAction("openCancelPayModal", window.openCancelPayModal, true);复制代码

所以整个通信流程就像是这样


客户端会将jsBridge对象注入到window对象下,当调用jsBridge触发(例如获取登录态)事件方法都会被客户端特有的拦截器所拦截并处理。

jsBrige通信模型优化

当js触发jsBridge对象触发某一个方法,其实都会走到客户端的拦截器里面然后native响应,这个过程就是在向客户端发消息。而客户端回调前端callback函数并且将数据返回其实就是在向H5页面发消息。那基于这种消息发送机制,可以将通信模型进一步优化。



优点:

1.前端和客户端不需要过多维护jsBridge方法,只需要维护功能短链,例如:

// 打开新容器,并且要求用户已登录
this.state.jsBridge.emit({
    name:'page1Event',
    action: 'XXX/openWebView',
    globle: false,
    params: {
        login: true
    }
})复制代码

其中action就是指的功能短链,而且emit方法就是通用发起消息的bridge。

而用于接收返回消息的函数,在老通信模型中是需要通过callback的形式传递。新的通信模型不再需要繁琐的callback传递,而是使用通用的function 来接收,例如:

//通用的顶层方法接收客户端消息
window.onJsBridgeEvent = function (json) { console.log(json) }复制代码

ps:globle是用来处理全局消息,要避免性能浪费要默认为不开启。开启globle需要针对页面name名称进行处理,需要页面事件名称保证唯一性(例如:A页面配置在tab,在其他tab内还能够再次打开A页面,此时A页面有两个,如果页面事件名称一致会导致全局消息被两个A页面处理,但是我们这时候只想要消息被置于容器顶层的A页面单独接受)


2.满足更多的交互场景


webview3中发送消息通知客户端销毁webview2,那么基于消息发送的通信模型就能够实现。
当前打开的所有webview都能接收客户端全局消息,例如:退登,集体销毁等功能的实现。


通信原理

刚才有提到jsBridge注入,客户端拦截器。

android

android 调用 js 代码的 方法:

1.通过webview的loadurl
2.通过webview的evaluateJavascript

jsBridge注入和拦截器

1.通过WebView的addJavascriptInterface()进行对象映射
2.通过 WebViewClient 的shouldOverrideUrlLoading ()方法回调拦截 url
3.通过 WebChromeClient的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框alert()、confirm()、prompt()消息

ios

使用WebViewJavaScriptBridge

function setupWebViewJavascriptBridge(callback) {
	if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
	if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
	window.WVJBCallbacks = [callback];
	var WVJBIframe = document.createElement('iframe');
	WVJBIframe.style.display = 'none';
	WVJBIframe.src = 'https://__bridge_loaded__';
	document.documentElement.appendChild(WVJBIframe);
	setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}复制代码

为了搭配理解这段代码的作用,可以点击这里

WebViewJavascriptBridge是ios提供的bridge对象,但是在最开始容器层初始化的时候是不提供的,所以代码会继续向下进行,直到 WVJBCallbacks(储存需要回调的函数) 和 wvjbscheme://__BRIDGE_LOADED__执行。

 wvjbscheme://__BRIDGE_LOADED__这个协议的目的就是用来创WebViewJavascriptBridge,相关截图如下:

127行:github.com/marcuswesti…


到这里jsbridge对象就算是提供给了前端使用。

接下来是调用ios能力,例如调用客户端实名认证:

this.state.jsBridge.callHandler("realNameAuthentication");复制代码

callHandler为jsbridge里面的一个对象,callHandler下的_doSend方法用来通知给客户端进行处理,相关截图如下:




这里的 messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;为:

messagingIframe.src = https + '://' + "__wvjb_queue_message__"; 这里的 __wvjb_queue_message__作用为触发ios的拦截器来进行处理。

本质上,最终还是被拦截器所拦截,但是ios的UI webview和WK webview拦截器会不同:

UI   webview : shouldStartLoadWithRequest
WK webview :decidePolicyForNavigationAction


webview性能优化

webview渲染过程


( 摘自:美团技术团队 tech.meituan.com/2017/06/09/… )


从用户使用的过程,大致如下:

1.打开了一个新的窗口

2.页面白屏

3.页面基本骨架渲染出来,但是没有数据

4.数据获取完成,页面整体渲染结束

慢的一部分原因:webview去加载url并不像是 浏览器 加载url的过程,webview存在一个初始化的过程。

webview init


为了提升init时间,常用做法是:

app启动时初始化一个隐藏的webview等待使用,当用户点击需要加载URL,直接使用这个webview 来加载,从而减少webview init 初始化时间。弊端就是带来了额外的内存开销。


webview缓存使用

为了提升H5加载速度,会使用到webview缓存模式。

以安卓为例:

LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据,
LOAD_DEFAULT:根据cache-control决定是否从网络上取数据,
LOAD_CACHE_NORMAL:API level 17中已经废弃, 从API level 11开始作用同- - LOAD_DEFAULT模式,
LOAD_NO_CACHE: 不使用缓存,只从网络获取数据,
LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。
如果一个页面的cache-control为no-cache,在模式LOAD_DEFAULT下,无论如何都会从网络上取数据,如果没有网络,就会出现错误页面;在LOAD_CACHE_ELSE_NETWORK模式下,无论是否有网络,只要本地有缓存,都使用缓存。本地没有缓存时才从网络上获取。如果一个页面的cache-control为max-age=60,在两种模式下都使用本地缓存数据。

提升H5在webview中的渲染速度,只是前端支持是不够的,还需要客户端采用合理的缓存模式,详细介绍点击这里

微信小程序通信模型

渲染和逻辑不在同一个环境中执行,逻辑层在纯js环境中,渲染层交给了webview,所以wxml和wxss是在渲染层,这两个线程的通信会经由微信客户端做中转,逻辑层发送网络请求也经由Native转发。


理解渲染层

在安卓则是往 WebView 的 window 对象注入一个原生方法,最终会封装成 WeiXinJSBridge 这样一个兼容层,主要提供了调用(invoke)和监听(on)这两种方法。开发者插入一个原生组件,一般而言,组件运行的时候被插入到 DOM 树中,会调用客户端接口,通知客户端在哪个位置渲染一块原生界面。在后续开发者更新组件属性时,同样地,也会调用客户端提供的更新接口来更新原生界面的某些部分。

理解逻辑层

微信提供的js执行环境,因为对控件进行了自定义,因此这个沙箱环境不能有浏览器的接口,只提供js执行环境。

js执行环境采用:在iOS下是用内置的 JavaScriptCore框架,在安卓则是用腾讯x5内核提供的JsCore环境。

为什么不使用web渲染?

如果采用纯web技术渲染小程序,在复杂业务场景必然带来性能问题。因为UI渲染跟js脚本都在一个单线程中执行,就容易导致js逻辑任务抢占UI资源。


本文参考:

美团技术团队:tech.meituan.com/2017/06/09/…

51NB:mp.weixin.qq.com/s/BjKeh7gk-…

腾讯Bugly:blog.csdn.net/Tencent_Bug…