1. 程式人生 > >【iOS開發】H5與Native互動之JSBridge技術

【iOS開發】H5與Native互動之JSBridge技術

做過混合開發的很多人都知道Ionic和PhoneGap之類的框架,這些框架在web基礎上包了一層Native,然後通過Bridge技術使得js可以呼叫視訊、位置、音訊等功能。本文就是介紹這層Bridge的互動原理,通過閱讀本文你可以瞭解到js與ios及android底層的通訊原理及JSBridge的封裝技術及除錯方法。

一、原理篇

下面分別介紹iOS和Android與Javascript的底層互動原理

iOS

在講解原理之前,首先來了解下iOS的UIWebView元件,先來看一下蘋果官方的介紹:

You can use the UIWebView class to embed web content in your application. To do so, you simply create a UIWebView object, attach it to a window, and send it a request to load web content. You can also use this class to move back and forward in the history of webpages, and you can even set some web content properties programmatically.

上面的意思是說UIWebView是一個可載入網頁的物件,它有瀏覽記錄功能,且對載入的網頁內容是可程式設計的。說白了UIWebView有類似瀏覽器的功能,我們使用可以它來開啟頁面,並做一些定製化的功能,如可以讓js調某個方法可以取到手機的GPS資訊。

但需要注意的是,Safari瀏覽器使用的瀏覽器控制元件和UIwebView元件並不是同一個,兩者在效能上有很大的差距。幸運的是,蘋果釋出iOS8的時候,新增了一個WKWebView元件,如果你的APP只考慮支援iOS8及以上版本,那麼你就可以使用這個新的瀏覽器控制元件了。

原生的UIWebView類提供了下面一些屬性和方法

屬性:

  • loading:是否處於載入中
  • canGoBack:A Boolean value indicating whether the receiver can move backward. (只讀)
  • canGoForward:A Boolean value indicating whether the receiver can move forward. (只讀)
  • request:The URL request identifying the location of the content to load. (read-only)

方法:

  • loadData:Sets the main page contents, MIME type, content encoding, and base URL.
  • loadRequest:載入網路內容
  • loadHTMLString:載入本地HTML檔案
  • stopLoading:停止載入
  • goBack:後退
  • goForward:前進
  • reload:重新載入
  • stringByEvaluatingJavaScriptFromString:執行一段js指令碼,並且返回執行結果

Native(Objective-C或Swift)呼叫Javascript方法

Native呼叫Javascript語言,是通過UIWebView元件的stringByEvaluatingJavaScriptFromString方法來實現的,該方法返回js指令碼的執行結果。

// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")  
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];

從上面程式碼可以看出它其實就是呼叫了window下的一個物件,如果我們要讓native來呼叫我們js寫的方法,那這個方法就要在window下能訪問到。但從全域性考慮,我們只要暴露一個物件如JSBridge對native呼叫就好了,所以在這裡可以對native的程式碼做一個簡單的封裝:

//下面為虛擬碼
webview.setDataToJs(somedata);  
webview.setDataToJs = function(data) {  
 webview.stringByEvaluatingJavaScriptFromString("JSBridge.trigger(event, data)")
}

Javascript呼叫Native(Objective-C或Swift)方法

反過來,Javascript呼叫Native,並沒有現成的API可以直接拿來用,而是需要間接地通過一些方法來實現。UIWebView有個特性:在UIWebView內發起的所有網路請求,都可以通過delegate函式在Native層得到通知。這樣,我們就可以在UIWebView內發起一個自定義的網路請求,通常是這樣的格式:jsbridge://methodName?param1=value1&param2=value2

於是在UIWebView的delegate函式中,我們只要發現是jsbridge://開頭的地址,就不進行內容的載入,轉而執行相應的呼叫邏輯。

發起這樣一個網路請求有兩種方式:1. 通過localtion.href;2. 通過iframe方式; 通過location.href有個問題,就是如果我們連續多次修改window.location.href的值,在Native層只能接收到最後一次請求,前面的請求都會被忽略掉。

使用iframe方式,以喚起Native APP的分享元件為例,簡單的封閉如下:

var url = 'jsbridge://doAction?title=分享標題&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';  
var iframe = document.createElement('iframe');  
iframe.style.width = '1px';  
iframe.style.height = '1px';  
iframe.style.display = 'none';  
iframe.src = url;  
document.body.appendChild(iframe);  
setTimeout(function() {  
    iframe.remove();
}, 100);

然後Webview就可以攔截這個請求,並且解析出相應的方法和引數。如下程式碼所示:

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {  
        print("shouldStartLoadWithRequest")
        let url = request.URL
        let scheme = url?.scheme
        let method = url?.host
        let query = url?.query

        if url != nil && scheme == "jsbridge" {
            print("scheme == \(scheme)")
            print("method == \(method)")
            print("query == \(query)")

            switch method! {
                case "getData":
                    self.getData()
                case "putData":
                    self.putData()
                case "gotoWebview":
                    self.gotoWebview()
                case "gotoNative":
                    self.gotoNative()
                case "doAction":
                    self.doAction()
                case "configNative":
                    self.configNative()
                default:
                    print("default")
            }

            return false
        } else {
            return true
        }
    }

Android

在android中,native與js的通訊方式與ios類似,ios中的通過schema方式在android中也是支援的。

javascript呼叫native方式

目前在android中有三種呼叫native的方式:

1.通過schema方式,使用shouldOverrideUrlLoading方法對url協議進行解析。這種js的呼叫方式與ios的一樣,使用iframe來呼叫native程式碼。
2.通過在webview頁面裡直接注入原生js程式碼方式,使用addJavascriptInterface方法來實現。
在android裡實現如下:

class JSInterface {  
    @JavascriptInterface //注意這個程式碼一定要加上
    public String getUserData() {
        return "UserData";
    }
}
webView.addJavascriptInterface(new JSInterface(), "AndroidJS");  

上面的程式碼就是在頁面的window物件裡注入了AndroidJS物件。在js裡可以直接呼叫

alert(AndroidJS.getUserData()) //UserDate  

3.使用prompt,console.log,alert方式,這三個方法對js裡是屬性原生的,在android webview這一層是可以重寫這三個方法的。一般我們使用prompt,因為這個在js裡使用的不多,用來和native通訊副作用比較少。

class YouzanWebChromeClient extends WebChromeClient {  
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 這裡就可以對js的prompt進行處理,通過result返回結果
    }
    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {

    }
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

    }

}

Native呼叫javascript方式

在android裡是使用webview的loadUrl進行呼叫的,如:

// 呼叫js中的JSBridge.trigger方法
webView.loadUrl("javascript:JSBridge.trigger('webviewReady')");  

二、庫的封裝

js呼叫native的封裝

上面我們瞭解了js與native通訊的底層原理,所以我們可以封裝一個基礎的通訊方法doCall來遮蔽android與ios的差異。

YouzanJsBridge = {  
    doCall: function(functionName, data, callback) {
        var _this = this;
        // 解決連續呼叫問題
        if (this.lastCallTime && (Date.now() - this.lastCallTime) < 100) {
            setTimeout(function() {
                _this.doCall(functionName, data, callback);
            }, 100);
            return;
        }
        this.lastCallTime = Date.now();

        data = data || {};
        if (callback) {
            $.extend(data, { callback: callback });
        }

        if (UA.isIOS()) {
            $.each(data, function(key, value) {
                if ($.isPlainObject(value) || $.isArray(value)) {
                    data[key] = JSON.stringify(value);
                }
            });
            var url = Args.addParameter('youzanjs://' + functionName, data);
            var iframe = document.createElement('iframe');
            iframe.style.width = '1px';
            iframe.style.height = '1px';
            iframe.style.display = 'none';
            iframe.src = url;
            document.body.appendChild(iframe);
            setTimeout(function() {
                iframe.remove();
            }, 100);
        } else if (UA.isAndroid()) {
            window.androidJS && window.androidJS[functionName] && window.androidJS[functionName](JSON.stringify(data));
        } else {
            console.error('未獲取platform資訊,調取api失敗');
        }
    }
}

上面android端我們使用了addJavascriptInterface方法來注入一個AndroidJS物件。

專案通用方法抽象

在專案的實踐中,我們逐漸抽象出一些通用的方法,這些方法基本上都是可以滿足專案的需求。如下所示:

1.getData(datatype, callback, extra) H5從Native APP獲取資料

使用場景:H5需要從Native APP獲取某些資料的時候,可以呼叫這個方法。

引數 型別 是否必須 示例值 說明
datatype String userInfo 資料型別
callback Function 回撥函式
extra Object 傳遞給Native APP的資料物件

示例程式碼:

JSBridge.getData('userInfo',function(data) {  
    console.log(data);
});

2.putData(datatype, data) H5告訴Native APP一些資料

使用場景:H5告訴Native APP一些資料,可以呼叫這個方法。

引數 型別 是否必須 示例值 說明
datatype String userInfo 資料型別
data Object { username: ‘zhangsan’, age: 20 } 傳遞給Native APP的資料物件

示例程式碼:

JSBridge.putData('userInfo', {  
    username: 'zhangsan',
    age: 20
});

3.gotoWebview(url, page, data) Native APP新開一個Webview視窗,並開啟相應網頁

引數 型別 是否必須 示例值 說明
url String 網頁連結地址,一般都只要傳遞URL引數就可以了
page String web 網頁page型別,預設為web
data Object 額外引數物件

示例程式碼:

// 示例1:開啟一個網頁
JSBridge.gotoWebview('http://www.youzan.com');

// 示例2:開啟一個網頁,並且傳遞額外的引數給Native APP
JSBridge.gotoWebview('http://www.youzan.com', 'goodsDetail', {  
    goods_id: 10000,
    title: '這是商品的標題',
    desc: '這是商品的描述'
});

4.gotoNative(page, data) 從H5頁面跳轉到Native APP的某個原生介面

引數 型別 是否必須 示例值 說明
page String loginPage Native頁面標示符,例如loginPage
data Object { username: ‘zhangsan’, age: 20 } 額外引數物件

示例程式碼:

// 示例1:開啟Native APP登入頁面
JSBridge.gotoNative('loginPage');

// 示例2:開啟Native APP登入頁面,並且傳遞使用者名稱給Native APP
JSBridge.gotoNative('loginPage', {  
    username: '張三'
});

5.doAction(action, data) 功能上的一些操作

引數 型別 是否必須 示例值 說明
action String copy 操作功能型別,例如分享、複製
data Object { content: ‘這是要複製的內容’ } 額外引數

示例程式碼:

// 示例1:呼叫Native APP複製一段文字到剪下板
JSBridge.doAction('copy', {  
    content: '這是要複製的內容'
});

// 示例2:呼叫Native APP的分享元件,分享當前網頁到微信
JSBridge.doAction('share', {  
    title: '分享標題',
    desc: '分享描述',
    link: 'http://www.youzan.com',
    imgs_url: 'http://wap.koudaitong.com/v2/common/url/create?type=homepage&index%2Findex=&kdt_id=63077&alias=63077'
});

三、除錯篇

使用Safari進行UIWebView的除錯

(1)首先需要開啟Safari的除錯模式,在Safari的選單中,選擇“Safari”→“Preference”→“Advanced”,勾選上“Show Develop menu in menu bar”選項,如下圖所示。 2-1

(2)開啟真機或iPhone模擬器的除錯模式,在真機或iPhone模擬器中開啟設定介面,選擇“Safari”→“高階”→“Web檢查器”,選擇開啟即可,如下圖所示。

2-2

(3)將真機通過USB連上電腦,或者開啟模擬器,Safari的“Develop”選單下便會多出相應的選單項,如圖所示。

Paste_Image.png

(4)Safari連線上UIWebView之後,我們就可以直接在Safari中直接修改HTML、CSS,以及除錯Javascript。

Paste_Image.png

四、轉載地址連結