混合开发框架整理:使用Crosswalk + WebViewJavascriptBridge进行混合开发
作者:hwj3747
转载请注明
Crosswalk介绍
目前APP的开发模式大多基于H5+原生壳的开发模式,这时候使用到的WebView的性能就至关重要。我们知道,Android平台上,系统的碎片化比较严重,同Android版本的WebView的H5解析能力也有较大差异,导致相应的HTML5应用一致性难以保证。所以在做混合开发的时候,对Android系统的适配是一个比较麻烦的问题。
这个时候,如果能在我们的APP嵌入一个第三方,不使用系统自带浏览器的话,这些问题就都迎刃而解了。Crosswalk就是这样一个第三方浏览器,其具有较好的H5性、功能支持,较好的平台一致性,以及近似原生应用的系统整合体验。
Crosswalk项目具有以下优势:
最大限度降低Android碎片化的影响,得到一致的,可预测的行为。
使用最新的Web技术及API。在Android 4.0+版本上提供丰富的功能。
使用Chrome DevTools轻松调试。(很方便的功能,可以直接在谷歌浏览器上进行调试,只不过需要*)
提升应用中HTML,CSS和JavaScript的性能。
WebViewJavascriptBridge介绍
WebViewJavascriptBridge 是盛名已久的 JSBridge 库,其主要作用是作为原生native与web JS沟通的“桥梁”,讲人话就是,能实现JS调用原生代码,以及原生调用JS的这么一个功能。目前该库已经累计获得1W+的start,并且已经有很多成熟的项目在使用这个库了,由此可见,该库是被广大开发者所认可的。
美中不足的是,目前官方只支持IOS版本的库。万幸它是开源的,许多人大神都基于它实现了支持Android版本的WebViewJavascriptBridge。比如说我使用到的是“大头鬼”开发的一个库:WebViewJavascriptBridge。
准备工作
首先,先从crosswalk官网下载aar包
我用到的是这个包:crosswalk-23.53.589.4.aar
然后从github上down下WebViewJavascriptBridge库,导入Android studio
然后在该项目的基础上导入crosswalk的aar包:
集成方法:
- 设置grade外部库为libs,拷贝aar文件到libs**
repositories {
flatDir {
dirs 'libs'
}
}
- 关联crosswalk库
compile(name: 'crosswalk-23.53.589.4', ext: 'aar')
然后同步一下代码就准备就绪了
使用Crosswalk和WebViewJavascriptBridge开发应用
打开项目,发现原本的项目用的还是系统自带的webview以及WebViewClient
public class BridgeWebView extends WebView implements WebViewJavascriptBridge
public class BridgeWebViewClient extends WebViewClient
我们要做的就是把这两个替换成相应的crosswalk的XWalkView和XWalkResourceClient
所以新建一个类,模仿BridgeWebView 的写法,把BridgeWebView 相关的注册和处理handle,消息分发,等操作搬到自定义的XWalkView上。
@SuppressLint("SetJavaScriptEnabled")
public class MyXWalkView extends XWalkView implements WebViewJavascriptBridge {
private final String TAG = "BridgeWebView";
public static final String toLoadJs = "WebViewJavascriptBridge.js";
Map<String, CallBackFunction> responseCallbacks = new HashMap<String, CallBackFunction>();
Map<String, BridgeHandler> messageHandlers = new HashMap<String, BridgeHandler>();
BridgeHandler defaultHandler = new DefaultHandler();
private List<Message> startupMessage = new ArrayList<Message>();
public List<Message> getStartupMessage() {
return startupMessage;
}
public void setStartupMessage(List<Message> startupMessage) {
this.startupMessage = startupMessage;
}
private long uniqueId = 0;
public MyXWalkView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyXWalkView(Context context) {
super(context);
init();
}
/**
*
* @param handler
* default handler,handle messages send by js without assigned handler name,
* if js message has handler name, it will be handled by named handlers registered by native
*/
public void setDefaultHandler(BridgeHandler handler) {
this.defaultHandler = handler;
}
private void init() {
this.setVerticalScrollBarEnabled(false);
this.setHorizontalScrollBarEnabled(false);
this.getSettings().setJavaScriptEnabled(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
this.setResourceClient(new MyXWalkResourceClient(this));
}
/**
* 获取到CallBackFunction data执行调用并且从数据集移除
* @param url
*/
void handlerReturnData(String url) {
String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
CallBackFunction f = responseCallbacks.get(functionName);
String data = BridgeUtil.getDataFromReturnUrl(url);
if (f != null) {
f.onCallBack(data);
responseCallbacks.remove(functionName);
return;
}
}
@Override
public void send(String data) {
send(data, null);
}
@Override
public void send(String data, CallBackFunction responseCallback) {
doSend(null, data, responseCallback);
}
/**
* 保存message到消息队列
* @param handlerName handlerName
* @param data data
* @param responseCallback CallBackFunction
*/
private void doSend(String handlerName, String data, CallBackFunction responseCallback) {
Message m = new Message();
if (!TextUtils.isEmpty(data)) {
m.setData(data);
}
if (responseCallback != null) {
String callbackStr = String.format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis()));
responseCallbacks.put(callbackStr, responseCallback);
m.setCallbackId(callbackStr);
}
if (!TextUtils.isEmpty(handlerName)) {
m.setHandlerName(handlerName);
}
queueMessage(m);
}
/**
* list<message> != null 添加到消息集合否则分发消息
* @param m Message
*/
private void queueMessage(Message m) {
if (startupMessage != null) {
startupMessage.add(m);
} else {
dispatchMessage(m);
}
}
/**
* 分发message 必须在主线程才分发成功
* @param m Message
*/
void dispatchMessage(Message m) {
String messageJson = m.toJson();
//escape special characters for json string 为json字符串转义特殊字符
messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
// 必须要找主线程才会将数据传递出去 --- 划重点
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
this.loadUrl(javascriptCommand);
}
}
/**
* 刷新消息队列
*/
void flushMessageQueue() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
@Override
public void onCallBack(String data) {
// deserializeMessage 反序列化消息
List<Message> list = null;
try {
list = Message.toArrayList(data);
} catch (Exception e) {
e.printStackTrace();
return;
}
if (list == null || list.size() == 0) {
return;
}
for (int i = 0; i < list.size(); i++) {
Message m = list.get(i);
String responseId = m.getResponseId();
// 是否是response CallBackFunction
if (!TextUtils.isEmpty(responseId)) {
CallBackFunction function = responseCallbacks.get(responseId);
String responseData = m.getResponseData();
function.onCallBack(responseData);
responseCallbacks.remove(responseId);
} else {
CallBackFunction responseFunction = null;
// if had callbackId 如果有回调Id
final String callbackId = m.getCallbackId();
if (!TextUtils.isEmpty(callbackId)) {
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
Message responseMsg = new Message();
responseMsg.setResponseId(callbackId);
responseMsg.setResponseData(data);
queueMessage(responseMsg);
}
};
} else {
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
// do nothing
}
};
}
// BridgeHandler执行
BridgeHandler handler;
if (!TextUtils.isEmpty(m.getHandlerName())) {
handler = messageHandlers.get(m.getHandlerName());
} else {
handler = defaultHandler;
}
if (handler != null){
handler.handler(m.getData(), responseFunction);
}
}
}
}
});
}
}
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
this.loadUrl(jsUrl);
// 添加至 Map<String, CallBackFunction>
responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}
/**
* register handler,so that javascript can call it
* 注册处理程序,以便javascript调用它
* @param handlerName handlerName
* @param handler BridgeHandler
*/
public void registerHandler(String handlerName, BridgeHandler handler) {
if (handler != null) {
// 添加至 Map<String, BridgeHandler>
messageHandlers.put(handlerName, handler);
}
}
/**
* unregister handler
*
* @param handlerName
*/
public void unregisterHandler(String handlerName) {
if (handlerName != null) {
messageHandlers.remove(handlerName);
}
}
/**
* call javascript registered handler
* 调用javascript处理程序注册
* @param handlerName handlerName
* @param data data
* @param callBack CallBackFunction
*/
public void callHandler(String handlerName, String data, CallBackFunction callBack) {
doSend(handlerName, data, callBack);
}
接下来,同样新建一个类,模仿BridgeWebViewClient 的写法,拦截XWalkview的url,对消息进行处理,并且关键的是在加载完成的事件里,再加载一次WebViewJavascriptBridge.js这个文件,所以需要把WebViewJavascriptBridge.js这个文件拷贝到asset目录下。
public class MyXWalkResourceClient extends XWalkResourceClient {
private MyXWalkView webView;
public MyXWalkResourceClient(MyXWalkView view) {
super(view);
this.webView = view;
}
@Override
public boolean shouldOverrideUrlLoading(XWalkView view, String url) {
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据
webView.handlerReturnData(url);
return true;
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
webView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
}
}
@Override
public void onLoadStarted(XWalkView view, String url) {
super.onLoadStarted(view, url);
}
@Override
public void onLoadFinished(XWalkView view, String url) {
super.onLoadFinished(view, url);
if (BridgeWebView.toLoadJs != null) {
webViewLoadLocalJs(view, MyXWalkView.toLoadJs);
}
//
if (webView.getStartupMessage() != null) {
for (Message m : webView.getStartupMessage()) {
webView.dispatchMessage(m);
}
webView.setStartupMessage(null);
}
}
/**
* 这里只是加载lib包中assets中的 WebViewJavascriptBridge.js
* @param view webview
* @param path 路径
*/
public static void webViewLoadLocalJs(XWalkView view, String path){
String jsContent = assetFile2Str(view.getContext(), path);
view.loadUrl("javascript:" + jsContent);
}
/**
* 解析assets文件夹里面的代码,去除注释,取可执行的代码
* @param c context
* @param urlStr 路径
* @return 可执行代码
*/
public static String assetFile2Str(Context c, String urlStr){
InputStream in = null;
try{
in = c.getAssets().open(urlStr);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
String line = null;
StringBuilder sb = new StringBuilder();
do {
line = bufferedReader.readLine();
if (line != null && !line.matches("^\\s*\\/\\/.*")) { // 去除注释
sb.append(line);
}
} while (line != null);
bufferedReader.close();
in.close();
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(in != null) {
try {
in.close();
} catch (IOException e) {
}
}
}
return null;
}
}
使用方法
接下来,看一下调用代码
- web调用原生层:原生层使用registerHandler方法,注册一个js与原生交互的插件名称为“submitFromWeb”,js端只要调用callHandler方法并指定名称为“submitFromWeb”就能调用原生的代码,原生层在handler方法里面取得js传过来的值data,并且使用CallBackFunction 回调给原生层,大部分的场景都可以使用这个方法。
- 原生层调用web,方法跟上面雷同,直接在原生层使用callHandler定义名称为“functionInJs”,在JS层注册一个名为functionInJs的handler就能接收到原生的消息了。
原生层:
webView.loadUrl("file:///android_asset/demo.html");
webView.registerHandler("submitFromWeb", new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
Log.i(TAG, "handler = submitFromWeb, data from web = " + data);
function.onCallBack("submitFromWeb exe, response data 中文 from Java");
}
});
User user = new User();
Location location = new Location();
location.address = "SDU";
user.location = location;
user.name = "大头鬼";
webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() {
@Override
public void onCallBack(String data) {
}
});
webView.send("hello");
h5层
<script>
function testDiv() {
document.getElementById("show").innerHTML = document.getElementsByTagName("html")[0].innerHTML;
}
function testClick() {
var str1 = document.getElementById("text1").value;
var str2 = document.getElementById("text2").value;
//send message to native
var data = {id: 1, content: "这是一个图片 <img src=\"a.png\"/> test\r\nhahaha"};
window.WebViewJavascriptBridge.send(
data
, function(responseData) {
document.getElementById("show").innerHTML = "repsonseData from java, data = " + responseData
}
);
}
function testClick1() {
var str1 = document.getElementById("text1").value;
var str2 = document.getElementById("text2").value;
//call native method
window.WebViewJavascriptBridge.callHandler(
'submitFromWeb'
, {'param': '中文测试'}
, function(responseData) {
document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
}
);
}
function bridgeLog(logContent) {
document.getElementById("show").innerHTML = logContent;
}
function connectWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
callback(WebViewJavascriptBridge)
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function() {
callback(WebViewJavascriptBridge)
},
false
);
}
}
connectWebViewJavascriptBridge(function(bridge) {
bridge.init(function(message, responseCallback) {
console.log('JS got a message', message);
var data = {
'Javascript Responds': '测试中文!'
};
if (responseCallback) {
console.log('JS responding with', data);
responseCallback(data);
}
});
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);
}
});
})
</script>
本文地址:https://blog.csdn.net/hwj3747/article/details/108572122