JSBridge
定义JSBridge
就相当于js
与native
之间进行全双工通信的一座桥梁,其内部定义了一套用于js
与native
进行通信的规范(包括协议、方法、传参及回调等);JSBridge
可以桥连js
与native
的通信,从而使基于容器的web
开发和优化成为可能,如比较火的hybrid app
技术;能够提升页面性能,丰富页面功能等;JSBridge
原理简析6+框架简析:
JSBridge
框架其实主要由两部分组成:第一部分是Native
调用js
,主要用于消息推送、状态同步及回溯调用结果等;第二部分是js
调用Native
,主要用于调用系统api、事件监听及状态同步等;
Native
调用js
Android
有两种方式:4.4.0
以前使用方法loadUrl
——调用方便,无法获取回调结果,会刷新webview;4.4.0+
使用方法evaluateScript
提供更加高效完善的功能——可以获取返回值并且不刷新webview;# loadUrl
mWebView.loadUrl("javascript: methodName(paramStr)");
# evaluateScript
mWebView.evaluateScript("javascript: method(paramStr)", new ValueCallback<String>() {
@override
public void onReceiveValue() {
// do something after receive js callback value
}
}
# UIWebView
mWebView.stringByEvaluatingJavaScriptFromString("methodName(paramStr)");
#WKWebView
mWebView.evaluateScript("methodName(paramStr)");
js
调用Native
url schema
拦截
h5
和native
约定一套通信协议作为通信基础,一般如下:
schema://methodName?params=xxx&cb=xxx
;
其中schema
为双方协商的协议名,methodName
为js调用native的方法名,params
为参数集字符串,cb
为接收回调结果的js
方法名;在h5
中发起请求时,一般通过构建一个不可见的iframe
发起请求;请求以约定的方式以url
形式发送,native
会拦截h5
的所有请求(如进行长连接优化等),如果发现url
中的协议名是约定的协议名(如jsbridge),则会解析其中的methodName
、params
及cb
等信息。如下给出了简单实现:
window.callId = 0;
const callNative = (method, params = null, cb) => {
const paramsObj = {
data: params ? JSON.stringify(params) : null,
}
if (typeof cb === 'function') {
const cbName = cb + window.callId++;
window[cbName] = cb;
paramsObj['cbName'] = cbName;
}
// 设定通信url供native拦截
const url = `jsbridge://${
method}?${
JSON.stringify(paramsObj)}`;
const iframe = document.createElement('iframe');
iframe.src = location.href;
iframe.style.width = 0;
iframe.style.height = 0;
iframe.frameborder = 0;
iframe.style.display = 'none';
document.body.appendChild(iframe);
setTimeout(() => {
iframe.parentNode.removeChild(iframe);
}, 100);
}
缺点:消息传输通过url
传输,因此传输数据长度受到限制;
prompt
、alert
、confirm
拦截
一般通过prompt
进行通信,其他实现与url schema
拦截类似;native
收到prompt
事件后会通过onJsPrompt
等类似事件对prompt
做处理,从而获取js
传入的method
、params
、cb
等;
function callNative(method, params, cb) {
...
const url = `jsbridge://${
method}?${
JSON.stringify(paramsObj)}`;
prompt(url);
}
缺点:ios
的UIWebkit
不支持;
注入JS上下文
这种方法一般是将需要供js
调用的native
方法通过实例对象的方式通过webview提供的方法注入到js全局上下文,这样位于webview内的h5
页面中js
可以直接调用native
的实例方法;
注入方式:Android的webview通过addJ avascriptInterface
;ios UIWebview
通过JSContext
;ios WKWebview
通过scriptMessageHandler
;
下面是来自文献2的Android客户端关于注入JS
上下文的简单demo;
首先,声明一个NativeMethods
的类,用于定义对外暴露给js
调用的方法,格式如methodName(webview, args, cb)
;
然后定义一个JSBridge
类:其中定义了两个静态方法和一个实例方法;register
用于将nativeMethods
类下的方法转为hashMap格式,便于查询;call
是暴露给js
的供js
统一调用的方法;
最后在webview创建后注册对外方法并将JSBridge
实例通过addJavascriptInterface
注入到JS全局上下文中;
public class MainActivity extends AppCompatActivity {
private WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView = (WebView) findViewById(R.id.mWebView);
...
// 将 NativeMethods 类下面的提供给 js 的方法转换成 hashMap
JSBridge.register("JSBridge", NativeMethods.class);
// 将 JSBridge 的实例对象注入到 js 全局上下文中,名字为 _jsbridge,该实例对象下有 call 方法
mWebView.addJavascriptInterface(new JSBridge(mWebView), "_jsBridge");
}
}
public class NativeMethods {
// 用来供 js 调用的方法
public static void methodName(WebView view, JSONObject arg, CallBack callBack) {
}
}
public class JSBridge {
private WebView mWebView;
public JSBridge(WebView webView) {
this.mWebView = webView;
}
private static Map<String, HashMap<String, Method>> exposeMethods = new HashMap<>();
// 静态方法,用于将传入的第二个参数的类下面用于提供给 javacript 的接口转成 Map,名字为第一个参数
public static void register(String exposeName, Class<?> classz) {
...
if (!exposeMethods.containsKey(exposeName)) {
exposeMethods.put(exposeName, getAllMethod(classz));
}
}
// 实例方法,用于提供给 js 统一调用的方法
@JavascriptInterface
public String call(String methodName, String args) {
...
}
}
根据上述代码可以看到注入到js
全局对象中的实例对象为_jsBridge
;在h5
中的调用如下:
window.callId = 0;
const callNative = (method, params = null, cb) => {
const paramsObj = {
data: params ? JSON.stringify(params) : null,
}
if (typeof cb === 'function') {
const cbName = cb + window.callId++;
window[cbName] = cb;
paramsObj['cbName'] = cbName;
}
if (window._jsBridge) {
window._jsBridge.call(method, JSON.stringify(paramsObj));
} else {
// 兜底方案
const url = `jsbridge://${
method}?${
JSON.stringify(paramsObj)}`;
prompt(url);
}
缺点:安卓4.2以下存在安全漏洞,可能会导致用户信息泄露;
JSBridge
静态方法的call
方法实现:
public static String call(WebView webView, String urlString) {
if (!urlString.equals("") && urlString!=null && urlString.startsWith("jsbridge")) {
Uri uri = Uri.parse(urlString);
String methodName = uri.getHost();
try {
JSONObject args = new JSONObject(uri.getQuery());
JSONObject arg = new JSONObject(args.getString("data"));
String cbName = args.getString("cbName");
if (exposeMethods.containsKey("JSBridge")) {
HashMap<String, Method> methodHashMap = exposeMethods.get("JSBridge");
if (methodHashMap!=null && methodHashMap.size()!=0 && methodHashMap.containsKey(methodName)) {
Method method = methodHashMap.get(methodName);
if (method!=null) {
method.invoke(null, webView, arg, new CallBack(webView, cbName));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
除此之外,js
调用native
方法成功需要给h5
响应的结果反馈,因此native
端需要定义一个Callback
类用于处理js
调用成功的结果反馈;其本质还是native
调用js
方法,以下是安卓端使用evaluatecript
方法实现的Callback
类:
public class CallBack {
private String cbName;
private WebView mWebView;
public CallBack(WebView webView, String cbName) {
this.cbName = cbName;
this.mWebView = webView;
}
public void apply(JSONObject jsonObject) {
if (mWebView!=null) {
mWebView.post(() -> {
mWebView.evaluateJavascript("javascript:" + cbName + "(" + jsonObject.toString() + ")", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
return;
}
});
});
}
}
}
h5
引用h5
引用即使用对应的封装好的JSBridge
的npm
包,内部封装了js
调用native
方法的方法集;该方式能够保证调用时JSBridge
一定存在;缺点是当有变更时,需要native
与h5
同时做变更进行兼容;native
注入JSBridge
实例对象注入到js
全局上下文;该方式有利于保障API与Native的一致性,但是由于注入方法和时机受到限制,h5
调用时总是要判断JSBridge
实例对象是否存在;