Hybrid开发即 原生与前端的混合开发,常指原生+H5的混合开发。在此之前,我们来梳理下,原生与H5交互的最原始做法(这里基于android)。
android与js交互
android与js交互的核心思想是通过向页面注入javascript代码间接调用H5脚本中定义的方法。WebView为我们提供了两种注入方式,一种有回调,一种无回调。
- SDK<19,android提供的无回调方法:loadUrl(“javascript:xxx”)
- SDK>=19,android新增了有回调的方法:evaluateJavascript(String,ValueCallback)
比如H5页面中定义了这样一个方法:
<script type="">
function show() {
alert("show()---");
return "js接收到了消息---";
}
</script>
那么用上面的方法可以这样调用:
String scrpit = "show()";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webView.evaluateJavascript(script, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
}
});
} else {
webView.loadUrl("javascript:" + script);
}
##js与android交互
js与android交互的核心思想是 android向页面注入一个对象后,js就可以拿到android注入进来的对象,通过这个对象调用其方法。
核心代码:
WebSettings webSettings = wv.getSettings();
webSettings.setJavaScriptEnabled(true);
wv.addJavascriptInterface(new JsApp(),"app");
class JsApp{
public JsApp(){}
@JavascriptInterface
public void call(Object obj){
}
}
这里,我们向页面注入了JsApp对象,命名为app,这样H5就可以通过app来调用JsApp中的方法了,H5中调用方式:
app.call(obj);
我们发现了JsApp中call()加了@JavascriptInterface注解,这是android4.2之后引入的。因为webview允许JavaScript 控制宿主应用程序,这是个很强大的特性,但同时,在4.2的版本前存在重大安全隐患,因为JavaScript 可以使用反射访问注入webview的java对象的public fields,在一个包含不信任内容的WebView中使用这个方法,会允许攻击者去篡改宿主应用程序,使用宿主应用程序的权限执行java代码。因此4.2以后,任何为JS暴露的接口,都需要加@@JavascriptInterface注解,这样可以避免Java对象的fields 被JS访问。
##Hybrid开发框架-DsBridge
基于前面我们讲到的android与js的交互,一些方便我们进行hybrid开发的框架应运而生,比如下面我们要分析的DsBridge框架。
###使用方式
1.添加 JitPack repository 到gradle脚本中
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
2.添加依赖
dependencies {
//compile 'com.github.wendux:DSBridge-Android:3.0-SNAPSHOT'
//support the x5 browser core of tencent
//compile 'com.github.wendux:DSBridge-Android:x5-3.0-SNAPSHOT'
}
3.新建一个java类,实现API
public class JsApi{
//同步API
@JavascriptInterface
public String testSyn(Object msg) {
return msg + "[syn call]";
}
//异步API
@JavascriptInterface
public void testAsyn(Object msg, CompletionHandler<String> handler) {
handler.complete(msg+" [ asyn call]");
}
}
可以看到,DSBridge正式通过类的方式集中、统一地管理API。由于安全原因,所有Java API 必须有"@JavascriptInterface" 标注。
4.添加API类实例到 DWebView
DWebView dwebView= (DWebView) findViewById(R.id.dwebview);
dwebView.addJavascriptObject(new JsApi(), null);
5.在Javascript中调用原生 (Java/Object-c/swift) API ,并注册一个 javascript API供原生调用
js调原生:
//同步调用
dsBridge.call("testSyn","testSyn");
//异步调用
dsBridge.call("testAsyn","testAsyn", function (v) {
alert(v);
})
//注册 javascript API
dsBridge.register('addValue',function(l,r){
return l+r;
})
6.在Java中调用 Javascript API
dwebView.callHandler("addValue",new Object[]{3,4},new OnReturnValue<Integer>(){
@Override
public void onValue(Integer retValue) {
Log.d("jsbridge","call succeed,return value is "+retValue);
}
});
以上使用方法来自github,接下来,我们就针对js调原生、原生调js两个方面来疏通其流程。
###js调原生
打开github上提供的DSBridge-Android工程,在/src/main/assets下存放着前端需要用到的资源,其中我们主要关注js-call-native.html、dsbridge.js。前者是js调原生的H5页面,后者是Node.js写的js模块,主要是提供给前者调用的功能封装。
js-call-native.html:
<!DOCTYPE html>
<html>
<head lang="zh-cmn-Hans">
<meta charset="UTF-8">
<title>DSBridge Test</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
<meta name="viewport" content="width=device-width,initial-scale=0.5,user-scalable=no"/>
<!--require dsbridge init js-->
<script src="./dsbridge.js"> </script>
</head>
<body>
<div class="btn" onclick="callSyn()">Synchronous call</div>
<div class="btn" onclick="callAsyn()">Asynchronous call</div>
...
<script>
function callSyn() {
alert(dsBridge.call("testSyn", "testSyn"))
}
function callAsyn() {
dsBridge.call("testAsyn","testAsyn", function (v) {
alert(v)
})
}
function callAsyn_() {
for (var i = 0; i < 2000; i++) {
dsBridge.call("testAsyn", "js+" + i, function (v) {
if (v == "js+1999 [ asyn call]") {
alert("All tasks completed!")
}
})
}
}
//省略了其他的代码,我们只看这两个方法
...
</script>
</body>
</html>
JsApi.java:
public class JsApi{
@JavascriptInterface
public String testSyn(Object msg) {
return msg + "[syn call]";
}
@JavascriptInterface
public void testAsyn(Object msg, CompletionHandler<String> handler){
handler.complete(msg+" [ asyn call]");
}
//省略了其他代码,我们只看着两个方法
...
}
我们看这里:
function callSyn() {
alert(dsBridge.call("testSyn", "testSyn"))
}
callSyn();
通过调用callSyn()方法,js就可以调用到android中定义在JsApi.java中testSyn()方法。
callSyn()方法中,走了dsBridge.call(),它是哪里来的呢?这时我们就要关注前面提到的dsBridge.js了,通过<script src="./dsbridge.js"> </script>
将dsbridge.js注入到当前页面,这样就可以调用dsbridge模块了。接下来,我们来看dsbridge.js:
var bridge = {
default:this,// for typescript
call: function (method, args, cb) {
var ret = '';
if (typeof args == 'function') {
cb = args;
args = {};
}
var arg={data:args===undefined?null:args}//定义arg对象
if (typeof cb == 'function') { //如果cb参数是一个方法
var cbName = 'dscb' + window.dscb++; //cbName = dscb1、dscb2、dscb3...
window[cbName] = cb; //window.dscb1 = cb,因此可以调用 dscb1(v)、dscb2(v)等函数,对应 function callAsyn() {
// dsBridge.call("testAsyn","testAsyn", function (v) {
// alert(v)
// })
// } 中的 function(v){}
arg['_dscbstub'] = cbName; //arg对象中添加一个属性 _dscbstub = cbName
}
arg = JSON.stringify(arg) //arg转为json
//if in webview that dsBridge provided, call!
if(window._dsbridge){//是否注入过 _dsbridge对象(android注入的对象)
ret= _dsbridge.call(method, arg) //调用android对象的call()
}else if(window._dswk||navigator.userAgent.indexOf("_dsbridge")!=-1){//如果注入过_dswk对象,或者 userAgent的最后是_dsbridge
ret = prompt("_dsbridge=" + method, arg); //走android prompt
}
return JSON.parse(ret||'{}').data
},
register: function (name, fun, asyn) {
var q = asyn ? window._dsaf : window._dsf
if (!window._dsInit) {
window._dsInit = true;
//notify native that js apis register successfully on next event loop
setTimeout(function () {
bridge.call("_dsb.dsinit");
}, 0)
}
if (typeof fun == "object") {
q._obs[name] = fun;
} else {
q[name] = fun
}
},
registerAsyn: function (name, fun) {
this.register(name, fun, true);
},
hasNativeMethod: function (name, type) {
return this.call("_dsb.hasNativeMethod", {name: name, type:type||"all"});
},
disableJavascriptDialogBlock: function (disable) {
this.call("_dsb.disableJavascriptDialogBlock", {
disable: disable !== false
})
}
};
!function () {
if (window._dsf) return;
var _close=window.close;
var ob = {
//保存JS同步方法
_dsf: {
_obs: {}
},
//保存JS异步方法
_dsaf: {
_obs: {}
},
dscb: 0,
dsBridge: bridge,
close: function () {
if(bridge.hasNativeMethod('_dsb.closePage')){
bridge.call("_dsb.closePage")
}else{
_close.cal