移動端混合開發(1):和H5的javascript互動
最近公司專案開發中涉及到了大量的混合開發,這裡開一個系列,把開發中的經驗和遇到的問題和大家分享下
講到移動端的混合開發,繞不開的一個話題就是原生和Js的互動,關於iOS、Android怎麼和js互動,網上的資料很多,這裡先簡單介紹幾個方法。
js部分
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<script>
function iOSShowDialog()
{
alert("iOS btn");
//showDialog 為iOS內定義好的方法
var params = {"title":"js 呼叫 iOS", "type":0, "callBack":"nativeCallBack"};
var paramsStr = JSON.stringify(params);
alert("iOS params:" + paramsStr);
window.webkit.messageHandlers.showDialog.postMessage(paramsStr);
}
function androidShowDialog()
{
alert("Android btn");
var params = {"title":"js 呼叫 Android", "type":1, "callBack":"nativeCallBack"};
var paramsStr = JSON.stringify(params);
alert("iOS params:" + paramsStr);
androidProxy.showDialog(paramsStr);
}
function nativeCallBack(paramsStr)
{
// var str:String = paramsStr;
var params = JSON.parse(paramsStr);
document.getElementById("info").innerHTML = params.message;
}
</script>
<body>
<body>
<div>
<button id="iOSBtn" onclick="iOSShowDialog()">click me call iOS show dialog</button>
</div>
<div>
<button id="AndroidBtn" onclick="androidShowDialog()">click me call Android show dialog</button>
</div>
<div>
<textarea id="info">...</textarea>
</div>
</body>
</body>
</html>
簡單說一下,js裡一共定義了三個方法,前兩個iOSShowDialog,androidShowDialog
看名字應該就能猜出來,是分別用來呼叫iOS和Android原生的方法的,第三個方法nativeCallBack
是用來給原生回撥的。
這裡稍微多說一點,雖然js呼叫原生的方法是可以有返回值的,但是在設計這個構架時,還是建議使用非同步回撥的方式。因為在js和原生的通訊場景中,有很多是非同步流程,最典型的就是js向原生要一個數據用來在頁面中展示,但是這個資料原生是需要通過伺服器請求返回的。這個場景裡,很明顯函式返回值是沒有辦法滿足需求的。
頁面刷出來是這個樣子:
js準備完畢
iOS部分
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// 建立配置
let config = WKWebViewConfiguration()
// 建立UserContentController(提供JavaScript向webView傳送訊息的方法)
let userContent = WKUserContentController()
// 新增訊息處理,注意:self指代的物件需要遵守WKScriptMessageHandler協議,結束時需要移除
userContent.add(self, name: "showDialog");
// 將UserConttentController設定到配置檔案
config.userContentController = userContent;
webView = WKWebView(frame: CGRect(x:0, y:0, width:self.view.bounds.width, height:300), configuration: config);
// let url = Bundle.main.url(forResource: "demo", withExtension: "html");
let url = URL(string: "http://你的域名/demo.html");
webView?.navigationDelegate = self;
webView?.uiDelegate = self;
// webView?.loadFileURL(url!, allowingReadAccessTo: Bundle.main.bundleURL);
webView?.load(URLRequest(url: url!));
self.view.addSubview(webView!);
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if(message.name == "showDialog")
{
let paramsStr = message.body as! String;
let paramsData = paramsStr.data(using: .utf8);
let params = try! JSONSerialization.jsonObject(with: paramsData!, options: .allowFragments) as! Dictionary<String, Any>;
let title = params["title"] as! String;
let type = params["type"] as! Int;
let callBack = params["callBack"] as! String;
let alert = UIAlertController(title: title, message: "type is "+type.description, preferredStyle: .alert);
alert.addAction(UIAlertAction(title: "確定", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil);
let callBackParams = ["message":"iOS的回撥來了"];
let callBackParamsStrData = try! JSONSerialization.data(withJSONObject: callBackParams, options: []);
let callBackParamsStr = String(data: callBackParamsStrData, encoding: .utf8);
let callBackFunc = callBack+"("+"'\(callBackParamsStr!)'"+")";
webView?.evaluateJavaScript(callBackFunc, completionHandler: { (item, error) in
print(item, error);
})
}
}
這裡只上兩個核心的方法,iOS我沒有用UIWebView+JavascriptCore,而是直接用了WKWebView,畢竟WKWebView效率和記憶體消耗好很多。但是WKWebView貌似不支援JavascriptCore,它有自己的js互動方式,並且也很簡單:
// 建立配置
let config = WKWebViewConfiguration()
// 建立UserContentController(提供JavaScript向webView傳送訊息的方法)
let userContent = WKUserContentController()
// 新增訊息處理,注意:self指代的物件需要遵守WKScriptMessageHandler協議,結束時需要移除
userContent.add(self, name: "showDialog");
// 將UserConttentController設定到配置檔案
config.userContentController = userContent;
這裡注意一下userContent.add(self, name: "showDialog");
這裡的是向H5注入方法的過程,因此showDialog必須和js裡呼叫的方法名一致。並且,第一個引數填入的物件必須遵守WKScriptMessageHandler
介面。
再來看響應,
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
剛才說了,userContent
傳入的物件必須遵守WKScriptMessageHandler
介面,上面的函式就是接口裡必須實現的方法。
if(message.name == "showDialog")
message物件的name屬性是方法名
let paramsStr = message.body as! String;
message 物件的body是方法的引數
再來看回調,我在js傳給iOS的json裡添加了一個欄位callBack,因此,回撥只需要將callBack欄位裡的方法名取出,拼裝成一個函式呼叫語句,通過WKWebView呼叫js的方法,調一下就可以了。
let callBackParams = ["message":"iOS的回撥來了"];
let callBackParamsStrData = try! JSONSerialization.data(withJSONObject: callBackParams, options: []);
let callBackParamsStr = String(data: callBackParamsStrData, encoding: .utf8);
let callBackFunc = callBack+"("+"'\(callBackParamsStr!)'"+")";
webView?.evaluateJavaScript(callBackFunc, completionHandler: { (item, error) in
print(item, error);
})
注意這句,let callBackFunc = callBack+"("+"'\(callBackParamsStr!)'"+")";
剛才說了,拼裝成一句函式呼叫語句。剛開始,我拼出來的是這樣的:"nativeCallBack(["message":"iOS的回撥來了"])"
乍一看,沒毛病,但始終報錯,後來才意識到,我在js的回撥方法裡定義的引數是一個String,也就是說,我呼叫的語句引數必須有引號包圍。所以,正確的應該是這樣的:"nativeCallBack( ' ["message":"iOS的回撥來了"] ' )"
嗯,沒毛病!
Android部分
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = mWebView.getSettings();
// 設定與Js互動的許可權
webSettings.setJavaScriptEnabled(true);
// 通過addJavascriptInterface()將Java物件對映到JS物件
//引數1:Javascript物件名
//引數2:Java物件名
// 載入JS程式碼
// 格式規定為:file:///android_asset/檔名.html
mWebView.post(new Runnable() {
@Override
public void run() {
// mWebView.loadUrl("file:///android_asset/demo.html");
mWebView.loadUrl("http://你的域名/demo.html");
mWebView.addJavascriptInterface(new AndroidtoJs(mWebView), "androidProxy");//AndroidtoJS類物件對映到js的test物件
}
});
}
@JavascriptInterface
public void showDialog(String dataStr)
{
JSONObject jsonObject = null;
try {
jsonObject = new JSONObject(dataStr);
String title = jsonObject.getString("title");
int type = jsonObject.getInt("type");
Toast.makeText(AppApplication.getInstance(), title + "type:" + type, Toast.LENGTH_LONG).show();
final String callBack = jsonObject.getString("callBack");
Map<String,String> callBackParams = new HashMap<>();
callBackParams.put("message", "這是從Android原生返回的資料");
JSONObject paramsJsonObjct = new JSONObject(callBackParams);
final String paramsJsonObjctStr = paramsJsonObjct.toString();
Log.d("AndroidtoJs",paramsJsonObjctStr);
wView.post(new Runnable() {
@Override
public void run() {
wView.evaluateJavascript("javascript:" + callBack + "('" + paramsJsonObjctStr + "')", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
}
});
}
});
} catch (JSONException e) {
e.printStackTrace();
}
}
Android相對簡單點,直接用WebView就行,方式其實和iOS類似,向H5注入一個物件,物件中定義的方法就可以背js呼叫。mWebView.addJavascriptInterface(new AndroidtoJs(mWebView), "androidProxy");//AndroidtoJS類物件對映到js的test物件
注意,androidProxy就是你在js裡呼叫的物件名,AndroidtoJs是在Android專案裡定義的物件,裡面封裝了需要暴露給js的方法public void showDialog(String dataStr)
。這個方法前一定要加上@JavascriptInterface
回撥也很簡單,
wView.post(new Runnable() {
@Override
public void run() {
wView.evaluateJavascript("javascript:" + callBack + "('" + paramsJsonObjctStr + "')", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
}
});
這裡有一點注意的地方,Android規定呼叫webUiew物件的方法必須在同一個執行緒,不然會報錯。因此,在呼叫webView.loadUrl和webView.evaluateJavascript時,都加上了webView.post(new Runnable() {}
尾聲
第一篇就寫這些,有什麼問題望大家多多指教。