1. 程式人生 > >IOS原聲與JS互動

IOS原聲與JS互動

跟原生開發相比,H5的開發相對來一個成熟的框架和團隊來講在開發速度和開發效率上有著比原生很大的優勢,至少不用等待稽核。那麼問題來了,H5與本地原生程式碼勢必要有互動的,比如本地上傳一些資訊,H5開啟本地的頁面,開啟本地進行微信等第三方分享等,今天就簡單講一下iOS中本地UIWebView,WKWebView與H5的互動。

DEMO地址:點選下載

UIWebView的互動

stringByEvaluatingJavaScriptFromString的使用

UIWebView在2.0時代就有的類,一直到現在(目前9.x)都可以使用的WEB容器,它的方法很簡單,在iOS7.0之前JS互動的方法只有一個stringByEvaluatingJavaScriptFromString:

  • (nullable NSString )stringByEvaluatingJavaScriptFromString:(NSString )script;
    使用stringByEvaluatingJavaScriptFromString方法,需要等UIWebView中的頁面載入完成之後去呼叫.

以下是簡單的使用場景:

1、獲取當前頁面的url。

  • (void)webViewDidFinishLoad:(UIWebView *)webView {
    NSString *currentURL = [webView stringByEvaluatingJavaScriptFromString:@”document.location.href”];
    }
    2、獲取頁面title:

  • (void)webViewDidFinishLoad:(UIWebView *)webView {
    NSString *currentURL = [webView stringByEvaluatingJavaScriptFromString:@”document.location.href”];
    NSString *title = [webview stringByEvaluatingJavaScriptFromString:@”document.title”];
    }
    3、修改介面元素的值。

NSString *js_result = [webView stringByEvaluatingJavaScriptFromString:@”document.getElementsByName(‘q’)[0].value=’iOS’;”];
4、表單提交:

NSString *js_result2 = [webView stringByEvaluatingJavaScriptFromString:@”document.forms[0].submit(); “];
這樣就實現了在google搜尋關鍵字:“iOS”的功能。
5、插入js程式碼
上面的功能我們可以封裝到一個js函式中,將這個函式插入到頁面上執行,程式碼如下:

"script.type = 'text/javascript';"            
"script.text = \"function myFunction() { "            
"var field = document.getElementsByName('q')[0];"             
"field.value='iOS';"            
"document.forms[0].submit();"   
"}\";"     
"document.getElementsByTagName('head')[0].appendChild(script);"];    

[webView stringByEvaluatingJavaScriptFromString:@"myFunction();"];   

看上面的程式碼:

a、首先通過js建立一個script的標籤,type為’text/javascript’。
b、然後在這個標籤中插入一段字串,這段字串就是一個函式:myFunction,這個函式實現google自動搜尋關鍵字的功能。
c、然後使用stringByEvaluatingJavaScriptFromString執行myFunction函式。

6、直接呼叫JS函式

上面的函式呼叫是本地注入到H5中,然後本地呼叫的,那麼如果H5中就有原生的JS函式:myFunction();,那麼我們就可以直接執行:

[webView stringByEvaluatingJavaScriptFromString:@”myFunction();”];
JavaScriptCore框架的使用

我們會發現stringByEvaluatingJavaScriptFromString的方法呼叫太笨拙,在iOS7.0中蘋果公司增加了JS利器JavaScriptCore框架,框架讓Objective-C和JavaScript程式碼直接的互動變得更加的簡單方便。該框架其實只是基於webkit中以C/C++實現的JavaScriptCore的一個包裝。其本身是可以單獨作為一個開發庫來使用,框架中有完整的資料計算邏輯,今天只講H5與本地互動,所以不作涉及,有興趣可以參考:iOS7新JavaScriptCore框架入門介紹。

JavaScriptCore提供了很多靈活的本地OC與JS的互動方式,通過JSContext和JSValue來完成的,JSContext是一個WebView中js程式碼執行環境,所有的JS互動都要通過- (JSValue )evaluateScript:(NSString )script;方法就可以執行一段JavaScript指令碼。

JSValue則可以說是JavaScript和Object-C之間互換的橋樑,它提供了多種方法可以方便地把JavaScript資料型別轉換成Objective-C.

具體如何互動我們先來看一段H5程式碼:




測試iOS與JS之前的互調

* {
font-size: 40px;
}


function showAppAlertMsg(message){
alert(message);
}




Test how to use objective-c call js




    <div>
        <input type="button" value="Call ObjC func with JSON " onclick="callWithDict({'name': 'testname', 'age': 10, 'height': 170})">
            <input type="button" value="Call ObjC func with JSON and ObjC call js func to pass args." onclick="jsCallObjcAndObjcCallJsWithDict({'name': 'testname', 'age': 10, 'height': 170})">
    </div>

    <div>
        <span id="jsParamFuncSpan" style="color: red; font-size: 50px;"></span>
    </div>
</body>


我們可以看出其中H5實現的JS程式碼如下:

function showAppAlertMsg(message){ alert(message); }

函式showAppAlertMsg是H5實現可以由我們本地主動呼叫的。那麼相對應的H5主動呼叫的方法是:


其中callSystemCamera()是H5呼叫的函式,我們可以看到在HTML程式碼中是找不到這個函式的實現的,因為這個函式是需要我們本地去實現。

接下來我們就講一下我們本地如何呼叫由H5實現的函式showAppAlertMsg,本地如何實現能夠右H5端呼叫的方法:

1.在頁面載入完成之後獲取JS執行環境JSContext

  • (void)webViewDidFinishLoad:(UIWebView *)webView {
    JSContext *jsContext = [webView valueForKeyPath:@”documentView.webView.mainFrame.javaScriptContext”];
    }
    2.利用evaluateScript宣告函式並傳遞引數執行程式碼

    JSValue *jsValue = [jsContext evaluateScript:@”showAppAlertMsg”];
    [jsValue callWithArguments:@[@”這是app本地互動文案”]];
    第一行程式碼是宣告一個函式showAppAlertMsg,第二行是傳遞引數並執行程式碼callWithArguments.

以上是主動呼叫H5實現的函式,也可以稱之為傳遞資料給H5。

本地實現能夠讓H5呼叫的函式:

// 也可以通過下標的方式獲取到方法
self.jsContext[@"callSystemCamera"] = ^(){
    NSLog(@"callSystemCamera");

};

self.jsContext[@"showAlertMsg"] = ^(NSString *title, NSString *message){
    NSLog(@"callSystemCamera");

};

self.jsContext[@"callWithDict"] = ^(id jsonDic){
    NSLog(@"callWithDict%@",jsonDic);

};

一目瞭然,通過Block傳遞引數和方法。

上面是利用JavaScriptCore的最基本的方法實現JS呼叫,還有另外一種方案利用JSExport協議進行互動.其實可以理解為通過JSExport協議實現一種把本地的例項繫結為H5中的一個物件,通過這個物件呼叫本地例項方法的一種互動設計。
該設計在H5端與上面的是不一樣的:



我們會發現 與之前的H5程式碼相比 多了一個 OCModel,這個在JS中可以理解為一個物件,點選按鈕之後呼叫物件OCModel中的函式OCModel.callSystemCamera,那麼該物件如何由本地繫結呢。

1.首先宣告一個JSExport協議,該協議需要宣告剛才H5中的函式:

import

import “NLJsObjCModel.h”

@implementation NLJsObjCModel

  • (void)callWithDict:(NSDictionary *)params {
    NSLog(@”Js呼叫了OC的方法,引數為:%@”, params);
    }

// Js呼叫了callSystemCamera
- (void)callSystemCamera {
NSLog(@”JS呼叫了OC的方法,調起系統相簿”);

// JS呼叫後OC後,又通過OC呼叫JS,但是這個是沒有傳引數的
JSValue *jsFunc = self.jsContext[@"jsFunc"];
[jsFunc callWithArguments:nil];

}

  • (void)jsCallObjcAndObjcCallJsWithDict:(NSDictionary *)params {
    NSLog(@”jsCallObjcAndObjcCallJsWithDict was called, params is %@”, params);

    // 呼叫JS的方法
    JSValue *jsParamFunc = self.jsContext[@”jsParamFunc”];
    [jsParamFunc callWithArguments:@[@{@”age”: @10, @”name”: @”lili”, @”height”: @158}]];
    }

  • (void)showAlert:(NSString )title msg:(NSString )msg {
    dispatch_async(dispatch_get_main_queue(), ^{
    UIAlertView *a = [[UIAlertView alloc] initWithTitle:title message:msg delegate:nil cancelButtonTitle:@”Ok” otherButtonTitles:nil, nil];
    [a show];
    });
    }
    @end
    3.將NLJsObjCModel例項物件繫結到JS中:

pragma mark - UIWebViewDelegate

  • (void)webViewDidFinishLoad:(UIWebView *)webView {
    self.jsContext = [webView valueForKeyPath:@”documentView.webView.mainFrame.javaScriptContext”];
    // 通過模型呼叫方法,這種方式更好些。
    NLJsObjCModel *model = [[NLJsObjCModel alloc] init];
    self.jsContext[@”OCModel”] = model;
    model.jsContext = self.jsContext;
    model.webView = self.webView;

    self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
    context.exception = exceptionValue;
    NSLog(@”異常資訊:%@”, exceptionValue);
    };
    }
    到此結束,H5按鈕點選之後就可以通過JSExport協議傳遞跟本地物件,本地物件就能收到相應。

WKWebView的互動使用

iOS8以後,蘋果推出了新框架Wekkit,提供了替換UIWebView的元件WKWebView。

先看下 WKWebView的特性:
1. 在效能、穩定性、功能方面有很大提升(最直觀的體現就是載入網頁是佔用的記憶體,模擬器載入百度與開源中國網站時,WKWebView佔用23M,而UIWebView佔用85M);
2. 允許JavaScript的Nitro庫載入並使用(UIWebView中限制);
3. 支援了更多的HTML5特性;
4. 高達60fps的滾動重新整理率以及內建手勢;
5. 將UIWebViewDelegate與UIWebView重構成了14類與3個協議(檢視蘋果官方文件);

WKWebView 的使用呢與UIWebView相同,不同的是回撥和代理方法.這裡簡單介紹一下.

WKNavigationDelegate

// 頁面開始載入時呼叫
- (void)webView:(WKWebView )webView didStartProvisionalNavigation:(WKNavigation )navigation;
// 當內容開始返回時呼叫
- (void)webView:(WKWebView )webView didCommitNavigation:(WKNavigation )navigation;
// 頁面載入完成之後呼叫
- (void)webView:(WKWebView )webView didFinishNavigation:(WKNavigation )navigation;
// 頁面載入失敗時呼叫
- (void)webView:(WKWebView )webView didFailProvisionalNavigation:(WKNavigation )navigation;
頁面跳轉的代理方法:

// 接收到伺服器跳轉請求之後呼叫
- (void)webView:(WKWebView )webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation )navigation;
// 在收到響應後,決定是否跳轉
- (void)webView:(WKWebView )webView decidePolicyForNavigationResponse:(WKNavigationResponse )navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
// 在傳送請求之前,決定是否跳轉
- (void)webView:(WKWebView )webView decidePolicyForNavigationAction:(WKNavigationAction )navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
新的 WKUIDelegate協議

這個協議主要用於WKWebView處理web介面的三種提示框(警告框、確認框、輸入框),下面是警告框的例子:

/**
* web介面中有彈出警告框時呼叫
*
* @param webView 實現該代理的webview
* @param message 警告框中的內容
* @param frame 主視窗
* @param completionHandler 警告框消失呼叫
*/
- (void)webView:(WKWebView )webView runJavaScriptAlertPanelWithMessage:(NSString )message initiatedByFrame:(void (^)())completionHandler;
動態載入並執行JS程式碼(WKUserScript)

用於在客戶端內部加入JS程式碼,並執行,示例如下

// 圖片縮放的js程式碼
NSString *js = @”var count = document.images.length;for (var i = 0; i < count; i++) {var image = document.images[i];image.style.width=320;};window.alert(‘找到’ + count + ‘張圖’);”;
// 根據JS字串初始化WKUserScript物件
WKUserScript *script = [[WKUserScript alloc] initWithSource:js injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
// 根據生成的WKUserScript物件,初始化WKWebViewConfiguration
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
[config.userContentController addUserScript:script];
_webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
[_webView loadHTMLString:@”http://www.nsu.edu.cn/v/2014v3/img/background/3.jpg’ />”baseURL:nil];
[self.view addSubview:_webView];
五、webView 執行JS程式碼
使用者呼叫用JS寫過的程式碼,一般指服務端開發的:

//javaScriptString是JS方法名,completionHandler是非同步回撥block
[self.webView evaluateJavaScript:javaScriptString completionHandler:completionHandler];

六、JS呼叫App註冊過的方法
在WKWebView裡面註冊供JS呼叫的方法,是通過WKUserContentController類下面的方法:

  • (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;
    scriptMessageHandler是代理回撥,JS呼叫name方法後,OC會呼叫scriptMessageHandler指定的物件。

JS在呼叫OC註冊方法的時候要用下面的方式:

window.webkit.messageHandlers..postMessage()
注意,name(方法名)是放在中間的,在這裡個人理解該name可以理解為一個標識,該標識將OC本地的例項物件繫結到JS中,將messageBody只能是一個物件,如果要傳多個值,需要封裝成陣列,或者字典。整個示例如下:

//OC註冊供JS呼叫的方法
[[_webView configuration].userContentController addScriptMessageHandler:self name:@”app”];

//OC在JS呼叫方法做的處理
- (void)userContentController:(WKUserContentController )userContentController didReceiveScriptMessage:(WKScriptMessage )message
{
NSLog(@”JS 呼叫了 %@ 方法,傳回引數 %@”,message.name,message.body);
}

//JS呼叫
window.webkit.messageHandlers.app.postMessage(null);
最後:

簡單做一個總結,在iOS7以前我們只能使用UIWebView,所以在js互動中只能通過stringByEvaluatingJavaScriptFromString進行本地呼叫H5,而H5端呼叫本地多采用URLSchemes方式(解耦—Hybrid H5跨平臺性思考),

iOS7之後使用UIWebView則可以使用JavaScriptCore框架,通過JSContext進行JS互動。
iOS8之後可以使用WKWebView本身的框架,畢竟WKWebView本身已經優化很多,並且提供了更多的方法和協議,不過注意一點的是在WKWebView中鑑於一些執行緒和程序的問題是無法獲取JSContext的。