为什么要写这篇文章
大家都知道,移动端混合开发时,少不了jsBridge来进行H5和端的通信。而且,jsBridge的运用时间已经很长了。那么有什么必要再拿出来翻炒呢? 其实我在想,大家知道jsBridge,甚至项目中一直在用。 那么作为前端开发者,知道js跟NA是怎么具体通信的么?比如js是怎么调用NA, NA是怎么执行Js方法的?
我们知道这些事后,脑子思维就会很清晰,自己明白,就不怕被别人忽悠。 开发人员使用最多的词汇可能就是"这个实现不了"、"这个实现成本太大"。。。 所以,明白了后,我们直接可以说: 不难,我来写都行
。 哈哈
下面我以业界用的比较多的 jsBridge 来进行分析。
这是分析的第一篇。
一、从调用说起
1.1 前端html中的调用
我们的在前端项目中,会在需要跟端交互的页面中,会进行如下初始化:
function connectWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
callback(WebViewJavascriptBridge)
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function() {
callback(WebViewJavascriptBridge)
},
false
);
}
}
connectWebViewJavascriptBridge(function(bridge) {
bridge.init(function(message, responseCallback) {
// 当native发送一个没有HandleName的方法时,执行这里的默认方法
console.log('JS got a message', message);
var data = {
'Javascript Responds': '测试中文!'
};
if (responseCallback) {
console.log('JS responding with', data);
responseCallback(data);
}
});
// 监听方法,native中执行
bridge.registerHandler("functionInJs", function(data, responseCallback) {
document.getElementById("show").innerHTML = ("data from Java: = " + data);
if (responseCallback) {
var responseData = "Javascript Says Right back aka!";
responseCallback(responseData);
}
});
})
初始化时, 比较重要的是Bridge.init()
那么,咱们去Native中看看这个init方法是什么。
1.2 Native中init方法
Native中也放了一个js文件:
WebViewJavascriptBridge.js
function init(messageHandler) {
// 进这个逻辑就说明之前init一次了
if (WebViewJavascriptBridge._messageHandler) {
throw new Error('WebViewJavascriptBridge.init called twice');
}
WebViewJavascriptBridge._messageHandler = messageHandler;
var receivedMessages = receiveMessageQueue;
receiveMessageQueue = null;
for (var i = 0; i < receivedMessages.length; i++) {
_dispatchMessageFromNative(receivedMessages[i]);
}
}
目前主要的是在这里: WebViewJavascriptBridge._messageHandler = messageHandler;
把我们传的init回调保存进来。
如果比较困惑Native中怎么也有javascript文件, 不用着急,目前知道有这个js文件就好了。
目前init的调用逻辑大体看到了, 下面再看1.1中的 bridge.registerHandler
1.3 前端中的 bridge.registerHandler
在上面1.1中看到了
bridge.registerHandler("functionInJs", function(data, responseCallback) {}
这里在前端js中注册了一个回调,目的是让NA能在某些时机下执行这个 functionInJs
函数
那么再看看NA中是怎么处理的:
1.4 Native中的registerHandler方法实现
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
比较简单,就是把这个回调放到messageHandlers对象中,以供后面某个时间把这个handle拿出来执行。
目前,我认为有这两个用法暂时就够了, 下面就开始深入到java逻辑中,看看这一切是怎么实现的。
二、Native中怎么实现的(android java代码为例)
需要知道的是,H5和NA之间的通信是使用webview来完成的 所以下面的分析会围绕着webview展开。
webview可以简单的理解为android的一个重要组件, 是android中自带的重要功能, 目的就是创建浏览器页面。
那么看看这个webview的使用。
大家如果不熟悉android, 也不要紧,简单看一看
这是native例子中的 MainActivity.java
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取webview
webView = (BridgeWebView) findViewById(R.id.webView);
button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);
// 为WebView指定一个WebChromeClient对象,WebChromeClient专门用来辅助WebView处理js的对话框,网站title,网站图标,加载进度条等
webView.setWebChromeClient(new WebChromeClient() {
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg, String AcceptType, String capture) {
this.openFileChooser(uploadMsg);
}
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg, String AcceptType) {
this.openFileChooser(uploadMsg);
}
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
mUploadMessage = uploadMsg;
pickFile();
}
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
mUploadMessageArray = filePathCallback;
pickFile();
return true;
}
});
// 通过WebView.addJavascriptInterface接口向Web页面注入Java对象,页面Javascript脚本可直接引用该对象并调用该对象的方法
// 比如: window.WebViewJavascriptBridge.callHandler("submitFromWeb", fn);
webView.addJavascriptInterface(new MainJavascrotInterface(webView.getCallbacks(), webView), "android");
webView.setGson(new Gson());
// 加载具体url
webView.loadUrl("file:///android_asset/demo.html");
// 在js中注册了方法,然后java中可以直接执行js中的这个方法
webView.callHandler("functionInJs", new Gson().toJson(user), new OnBridgeCallback() {
@Override
public void onCallBack(String data) {
Log.d(TAG, "onCallBack: " + data);
}
});
//先把数据保存到队列中,在合适的时候向js发送, bridge.init()能承接
webView.sendToWeb("hello");
}
从上面看,在java中去调用了functionInJs
方法, 结果是把方法名和callback放到队列中,后面会进行发送。
那么放到队列中的逻辑是:
下面的方法,就进入了核心逻辑 BridgeWebview.java
中
1.callHandler的定义:
public void callHandler(String handlerName, String data, OnBridgeCallback callBack) {
doSend(handlerName, data, callBack);
}
2.doSend方法定义:
class JSRequest {
public String callbackId;
public String data;
public String handlerName;
}
private void doSend(String handlerName, Object data, OnBridgeCallback responseCallback) {
if (!(data instanceof String) && mGson == null){
return;
}
JSRequest request = new JSRequest();
if (data != null) {
request.data = data instanceof String ? (String) data : mGson.toJson(data);
}
if (responseCallback != null) {
String callbackId = String.format(BridgeUtil.CALLBACK_ID_FORMAT, (++mUniqueId) + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis()));
mCallbacks.put(callbackId, responseCallback);
request.callbackId = callbackId;
}
if (!TextUtils.isEmpty(handlerName)) {
request.handlerName = handlerName;
}
queueMessage(request);
}
3.queueMessage方法定义:
private void queueMessage(Object message) {
// 如果mMessages是个Arraylist,那么把要请求的message信息先存起来
if (mMessages != null) {
mMessages.add(message);
} else {
// 正式发送
dispatchMessage(message);
}
}
走到这里, 为上面的流程做个小结:
-
我们前端代码js中中,在业务代码中对jsBridge进行init和注册了一个方法的监听, 方法监听用于Native执行方法后,执行js的回调,把数据传递给业务js层。
-
做了监听后,会把业务层的监听函数注册到Na中的
WebViewJavascriptBridge.js
中。 -
Native代码中,通过callHandler执行方法, 这时会把handleName和回调方法先存入mMessages这个ArrayList中; 同时,sendToWeb('hello') 也会执行
doSend
方法,并且存入mMessage中。注意,这里的handleName是null, 也就是说不会定向发送给某个注册监听函数。 也就是说这时并没有执行,只是先存储起来。
三、正式发送的时机来临
BridgeWebview.java
中,因为继承了webview, 而webview有个生命周期钩子是 onLoadFinished
:
@Override
public void onLoadFinished() {
mJSLoaded = true;
if (mMessages != null) {
for (Object message : mMessages) {
dispatchMessage(message);
}
mMessages = null;
}
}
当webview加载完成时, 那么循环mMessages中的数据, 执行 dispatchMessage(message)
这个函数负责执行里面的各个JSRequest实例对象。
private void dispatchMessage(Object message) {
if (mGson == null){
return;
}
String messageJson = mGson.toJson(message);
//escape special characters for json string 为json字符串转义特殊字符
// 系统原生 API 做 Json转义,没必要自己正则替换,而且替换不一定完整
messageJson = JSONObject.quote(messageJson);
// BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA => "javascript:WebViewJavascriptBridge._handleMessageFromNative(%s);"
String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
// 必须要找主线程才会将数据传递出去 --- 划重点
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT&&javascriptCommand.length()>=URL_MAX_CHARACTER_NUM) {
// 把字符串command传进去进行解析
this.evaluateJavascript(javascriptCommand,null);
}else {
this.loadUrl(javascriptCommand);
}
}
}
通过webview组件自带的 evaluateJavascript
解析JavaScript代码 "javascript:WebViewJavascriptBridge._handleMessageFromNative(%s);"
%s已经替换成实际字符串。
这时,就会执行WebViewJavascriptBridge.js
中的_handleMessageFromNative
方法:
function _handleMessageFromNative(messageJSON) {
console.log('handle message: '+ messageJSON);
if (receiveMessageQueue) {
receiveMessageQueue.push(messageJSON);
}
_dispatchMessageFromNative(messageJSON);
}
而后执行 _dispatchMessageFromNative
, 这个方法中去具体执行在我们前端js中存储的init和 registerHandler 中注册的回调函数,并把data传给回调方法。
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
var message = JSON.parse(messageJSON);
var responseCallback;
//java call finished, now need to call js callback function
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
//直接发送
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend('response', responseData, callbackResponseId);
};
}
var handler = WebViewJavascriptBridge._messageHandler;
if (message.handlerName) {
handler = messageHandlers[message.handlerName];
}
//查找指定handler
try {
// 执行回调,这时,我们前端写的js代码回调函数就执行了。
handler(message.data, responseCallback);
} catch (exception) {
if (typeof console != 'undefined') {
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
}
}
}
});
}
对这个阶段再进行一次总结:
-
onLoadFinished的时候,正式循环mMessages,执行dispatchMessage方法,通过evaluateJavascript执行对应的WebViewJavascriptBridge中的_handleMessageFromNative方法,把JSRequest实例传进去作为参数。
-
然后就到了Native中的那个js文件了(注意,这个问题件并不是我们的业务js文件,一定不要弄混了), 这里面去查找业务js文件中的所有注册回调,只要找到对应的handleName了,就去执行对应的回调, 如果没有找到,就去执行默认的回调(init方法中的回调).
-
至此, 我们业务js中的回调函数就执行了, 并且,获取到了端返回的data, 如果有需要,还可以执行JSRequest实例中的端回调函数, 通知端,js已经加载完成了。
至此, 端上主动调用业务js中的方法流程通畅了。