什么是WebView
WebView 是移动端中的一个控件,它为 JS 运行提供了一个沙箱环境。WebView 能够加载指定的 url,拦截页面发出的各种请求等各种页面控制功能,JSB 的实现就依赖于 WebView 暴露的各种接口。
由于历史原因,IOS以8为分界,Android以4.4为分界,分为高低两个版本。而它们的区别在于 —— 回调。高版本可以通过执行回调拿到 JS 执行完毕的返回值,然后准确进行下一步操作。而低版本无法执行回调!
什么是 JSB
Hybrid App 的核心。我们开发的 h5 页面运行在端上的 WebView 容器之中,很多业务场景下 h5 需要依赖端上提供的信息/能力,这时我们需要一个可以连接原生运行环境和 JS 运行环境的桥梁 。 这个桥梁就是 JSB,JSB 让 Web 端和 Native 端得以实现双向通信。
JSB的目的
JSB 的目的就是“让 Native 可以调用 web 端的 JavaScript 代码,让 web 端可以调用 Native 的原生代码”。
native 调用 web 端代码
无论 Android 还是 iOS,在调用 web 端代码的时候,必须是调用的“挂载在window上的函数”。拿一个例子来说:
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="调用JS方法"
android:onClick="onJSFunction1"/>
public void onJSFunction1 (View v) {
mWebView.evaluateJavascript("javascript:onFunction('android调用JS方法')", new ValueCallback<String>() {
@Override
public void onReceiveValue(String s) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setMessage(s);
builder.setNegativeButton("确定", null);
builder.create().show();
}
});
}
evaluateJavascript
就是调用 JS 中的方法:onFunction1,并传入参数,在回调中进行处理 —— 这个回调至关重要。
在 js 中:
window.onFunction = function(str) {
alert(str);
return "这是 onFunction 方法的返回值";
}
上面的 java 代码中,mWebView
是实例化的 腾讯X5内核组件。在它的配置中首先注意到这样一行代码:
/**
* 允许加载的网页执行 JavaScript 方法
*/
webSettings.setJavaScriptEnabled(true);
允许网页执行 JS 方法。这个非常重要:它是 Native 是否能够向 web 通信的关键!这一点下面我们会提到。
在进行完一些基础配置后,我们会构建一个 JSBridge 对象:
addJavascriptInterface(
new MyJaveScriptInterface(mContext, this),
"AndroidJSBridge");
这个对象就叫做“AndroidJSBridge”,在这个处理中,这个 JSB 对象会被挂载到网页的 window 对象下,从而作为原生端和 web 端通信的桥梁。
web 调用 native 端代码
上面说了在初始化后 window 对象下会有一个 AndroidJSBridge 对象,在网页中也可以直接通过 window.AndroidJSBridge
拿到这个对象,从而调用 Android 端提供给网页端的方法(这里以Android为例):
现在 Android 提供了这样一个方法:
@JavascriptInterface
public void androidTestFunction1 (String str) {
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setMessage(str);
builder.setNegativeButton("确定", null);
builder.create().show();
}
目的是在 APP 弹出一个 Alert 对话框,对话框中的内容为 JavaScript 传入的字符串。
注意:对于 Android 来说,当 js 调用方法并传参时,Android方法接收的参数只能是“基本数据类型”。对于“复杂数据类型”,只能通过JSON.stringify(Object)
转化成 string 类型。
而 iOS 则不受此限制。
在 web 项目中,拿原生项目来说,我们直接这么写即可:
<input type="button" value="调用androidTestFunction1" @click="toAndroidFunction1()" />
<script>
function toAndroidFunction1() {
window.AndroidJSBridge.androidTestFunction1('调用 android 下的 function1 方法')
}
</script>
当然,在 android 方法中,我们也可以直接 return
数据,到了 web 端就是“回调”了。
function toAndroidFunction2() {
let result = window.AndroidJSBridge.androidTestFunction1('androidTestFunction2方法的返回值');
alert(result);
}
诶,为什么调用的 alert
也是弹出的 android 原生弹出框?
因为在初始化的时候,原生对 alert 进行了一次“劫持”:
/**
* 监听网页中的url加载事件
*/
private void initChromeClient () {
setWebChromeClient(new WebChromeClient(){
/**
* alert()
* 监听alert弹出框,使用原生弹框代替alert。
*/
@Override
public boolean onJsAlert(WebView webView, String s, String s1, JsResult jsResult) {
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setMessage(s1);
builder.setNegativeButton("确定", null);
builder.create().show();
jsResult.confirm();
return true;
}
});
}
双向通信
Native向web发送消息
其实原理上面已经说了:
Native 向 Web 发送消息基本原理上是在 WebView 容器中动态地执行一段 JS 脚本,通常情况下是调用一个挂载在全局上下文的方法。
具体来说 —— Native 端可以直接调用挂载在 window 上的全局方法并传入相应的函数执行参数,并且在函数执行结束后 Native 端可以直接拿到执行成功的返回值。
场景:页面上是一个web page,在其上有一个原生控件 Button 和 Input。在输入框中输入一段代码更改 web 页面上某个标签的 innerHTML
:
<EditText
android:id="@+id/editText_name"
android:layout_width="edit_parent"
android:layout_height="edit_content"
android:hint="请输入你想要执行的js代码" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Native向Web发送消息" />
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import cn.sunday.hybridappdemo.views.X5WebView;
public class MainActivity extends AppCompatActivity {
private X5WebView mWebView;
private Button button;
private EditText editText_name;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.Button_quedin);
this.editText_name = (EditText) findViewById(R.id.editText_name);
button.setOnClickListener(new View.OnClickListener() {//注册监听
@Override //监听点击事件
public void onClick(View v) {
String name = editText_name.getText().toString();
mWebView.evaluateJavascript("javascript:onShowPageNative(" + name + ")");
}
})
})
}
window.onShowPageNative = function(str) {
new Function(str);
}
将文本框输入的字符视为 JS 字符串并调用相关 API 直接执行。
你是否想到了著名的用于解决跨域问题的jsonp?其实很多地方都使用了“将代码作为字符串传递再执行”的原理。最出名的就是微信小程序的(双线程)混合架构模型中view层向逻辑层通信。
Web 向 Native 发送消息
Web 向 Native 发送消息本质上就是某段 JS 代码的执行端上是可感知的,目前业界主流的实现方案有两种,分别是拦截式和注入式。
拦截式
和浏览器类似 WebView 中发出的所有请求都是可以被 Native 容器感知到的,因此拦截式具体指的是 Native 拦截 Web 发出的 URL 请求,双方在此之前约定一个 JSB 请求格式,如果该请求是 JSB 则进行相应的处理,若不是则直接转发。
Native 拦截请求的钩子方法:
- Android:shouldOverrideUrlLoading
IOS:
- 8+:decidePolicyForNavigationAction
- 8-:shouldStartLoadWithRequest
拦截式的流程存在几个问题:
通过何种方式发出请求?
Web 端发出请求的方式非常多样,例如 <a>
、iframe.src
、location.href
、ajax 等,但 <a>
需要用户手动触发,location.href
可能会导致页面跳转,安卓端拦截 ajax 的能力有所欠缺,因此绝大多数拦截式实现方案均采用 iframe 来发送请求。
如何规定 JSB 的请求格式?
一个标准的 URL 由 <scheme>://<host>:<port><path>
组成,相信大家都有过从微信或手机浏览器点击某个链接意外跳转到其他 App 的经历,如果有仔细留意过这些链接的 URL 你会发现目前主流 App 都有其专属的一个 scheme来作为该应用的标识,例如微信的 URL scheme 就是 weixin://
。JSB 的实现借鉴这一思路,定制业务自身专属的一个 URL scheme 来作为 JSB 请求的标识,例如字节内部的 bytedance://
。
// Web 通过动态创建 iframe,将 src 设置为符合双端规范的 url scheme
const CUSTOM_PROTOCOL_SCHEME = 'vdian'
function web2Native(event) {
const messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + event;
document.documentElement.appendChild(messagingIframe);
setTimeout(() => {
document.documentElement.removeChild(messagingIframe);
}, 200)
}
拦截式在双端都具有非常好的向下兼容性,曾经是最主流的 JSB 实现方案,但目前在高版本的系统中已经逐渐被淘汰,理由是它有如下几个劣势:
- 连续发送时可能会造成消息丢失(可以使用消息队列解决该问题)
- URL 字符串长度有限制
- 性能一般,URL request 创建请求有一定的耗时(Android 端 200-400ms)
注入式
注入式的原理是通过 WebView 提供的接口向 JS 全局上下文对象(window)中注入对象或者方法,当 JS 调用时,可直接执行相应的 Native 代码逻辑,从而达到 Web 调用 Native 的目的。
—— 也就是上面说的“web 端调用 Native 端代码”。
这种方法简单而直观,并且不存在参数长度限制和性能瓶颈等问题,目前主流的 JSB SDK 都将注入式方案作为优先使用的对象。