iOS開發----JavaScriptCore、UIWebView及WKWebView交互的那些事

參與工做時間比較長了,隨着Web前端行業的發展(你們都懂得..),客戶端與Web端的交互也愈來愈頻繁。其實本人不太喜歡依賴第三方,那種看不到摸不着的東西用起來總感受不是很安心,同時也是爲了保證雙方都可以高效完成交互的途中不出現一些意料不到的異常,對此,研究了一下JavaScriptCore這個庫仍是頗有必要的,並分別結合UIWebView以及WKWebView作了一下交互總結。javascript

寫的比較多,若是是第一次接觸這個庫,建議仍是看一看;若是時間比較緊,想直接知道結果的,送你一個捷徑😀傳送門,有幫助能夠Star一下,十分感謝前端

假設一個簡單的場景

  • Web經過一個<input/>輸入一個字符串,經過點擊按鈕設置成導航標題
  • 原生設置完導航標題後,告知Web"以將<#字符串#>"設置成導航Title,並在網頁最底下的label顯示出來。

分別使用UIWebView以及WKWebView實現效果以下:
java

UIWebView.gif

WKWebView.gif

JavaScriptCore

類庫裏面有12個類(還有兩個是負責導入相關類的頭文件以及一個關於WebKit的宏定義);在基本的交互過程當中,其實最常使用的有三個:JSContext、JSValue、JSExportios

JSContext

簡單的理解爲執行JavaScript的一個環境,就好像咱們在繪製View時候須要獲取的CGContext同樣,JS的執行須要在此環境之下。git

JSValue

能夠理解成 一種供iOS數據結構與JS數據結構相互轉換的包裝,也能夠當作一種橋接關係,咱們執行JS獲取的結果就是經過JSValue對象進行包裝傳給客戶端進行處理的,類型轉換官方文檔描述以下:github

Objective-C type  |   JavaScript type
 --------------------+---------------------
         nil         |     undefined
        NSNull       |        null
       NSString      |       string
       NSNumber      |   number, boolean
     NSDictionary    |   Object object
       NSArray       |    Array object
        NSDate       |     Date object
       NSBlock (1)   |   Function object (1)
          id (2)     |   Wrapper object (2)
        Class (3)    | Constructor object (3)複製代碼

JavaScriptType返回的JSValue數據可經過JSValue.toXXX()轉成客戶端相應的數據結構;反之,客戶端對象也能夠經過JSValue()的構造方法將相應的數據結構封裝成JSValue。web

JSExport

這是一個協議,官方文檔沒有暴露出任何的open協議方法,能夠理解爲一個空協議。bash

一般用法是自定義一個CustomExport : JSExport,裏面將JS能夠調用的屬性或者方法進行暴露,JS就能夠直接使用暴露的屬性與方法了。微信

ObjC方法定義樣式是很是特殊的,但官方文檔給出了轉換後JS調用的樣式:數據結構

//Objective-C
- (void)doFoo:(id)foo withBar:(id)bar;

//JS
doFooWithBar(foo,bar)複製代碼

但這樣會有一個缺點,萬一,方法有不少個參,拼接起來的JS方法名簡直就是日了X;不過這點Apple已經幫咱們想到了,使用JSExportAs宏,能夠將方法名簡化,就像Swift中的typealias以及ObjC中的typedef

//這樣在JS中直接調用doFoo(foo,bar)便可
 JSExportAs(doFoo,
  - (void)doFoo:(id)foo withBar:(id)bar
  );複製代碼

以上三個文件就算理解完了,下面來一段小應用😀。

客戶端調用JavaScript

執行簡單的JavaScript

let context = JSContext()

//方法函數定義採用的是ES6語法,由於最近正在學習RN,習慣這麼寫了呢😀
let _ = context?.evaluateScript("var textnumber = 1")
let _ = context?.evaluateScript("var names = ['Yue','Xiao','Wen']")
let _ = context?.evaluateScript("var triple = (value) => value + 3")
let returnValue = context?.evaluateScript("triple(3)") //由於有返回值,須要接收一下

//打印結果:returnValue = Optional(6)
print("__testValueInContext --- returnValue = \(returnValue?.toNumber())")複製代碼

獲取定義的JavaScript變量

//經過變量名字獲取對象
let names = context?.objectForKeyedSubscript("names")

//經過定義順序的下標獲取對象,就是取['Yue','Xiao','Wen']的第0個元素
let firstName = names?.objectAtIndexedSubscript(0) //Yue

//打印結果:names = Optional([Yue, Xiao, Wen]) firstName = Optional(Yue)
print("__testValueInContext --- names = \(names?.toArray())\nfirstName = \(firstName)")

/// 得到context建立的函數變量
let function = context?.objectForKeyedSubscript("triple") //運行 let result = function?.call(withArguments: [3]) //打印結果:context-function's result = Optional(6) print("__testValueInContext --- context-function's result = \(result?.toNumber())")複製代碼

捕獲執行異常

/// 捕獲JS運行錯誤
context?.exceptionHandler = {(context,exception) in
    print("__testValueInContext --- JS error = \(exception)\n")//打印錯誤
}

/** 執行一個錯誤的js,由於沒有函數Triple(上面的方法名第一字母是小寫的),會調用上面的exceptionHandler 打印結果: JS error = Optional(ReferenceError: Can't find variable: Triple) */
let _ = context?.evaluateScript("Triple(3)")複製代碼

JavaScript 調用客戶端

仔細看看JSValue的類型轉換,就能夠知道,JS中方法就是客戶端中的閉包,不過這裏樓主採用了Swift和ObjC混編模式,至於緣由下面會說一下:

//得到處理完畢的數據
let result = RITLJSCoreObject.textJavaScriptUseiOS(inObjC: "Hello")

//結果 I am Objc, result = Optional("Hello I am append String")
print("I am Objc, result = \(result?.toString())\n")複製代碼

實現方法:

+(JSValue *)textJavaScriptUseiOSInObjC:(NSString *)value
{
    JSContext * context = [JSContext new];
    
    //設置block
    context[@"stringHandler"] = ^(NSString * oldValue){
        NSMutableString * valueHandler = [[NSMutableString alloc]initWithString:oldValue];
        [valueHandler appendString:@" I am append String"];
        return valueHandler;
    };
    
    NSString * js = [NSString stringWithFormat:@"stringHandler('%@')",value];
    //注入
    return [context evaluateScript:js];
}複製代碼

Swift版本以下,功能實如今本人看來應該是同樣的,但在進行注入的時候出現了問題,致使執行方法出現了undefined

多是`Swift`的一個bug,也多是我使用不當
若是是我使用錯了,還請知道緣由的小夥伴私信一下,十分感謝。複製代碼
let context = JSContext()

//初始化一個閉包
let stringHandler : (String) -> String = { (value) in
    var value = value
    value.append(" I am appending word with closure!")
    return value
}

//封裝成JSValue
let handerValue = JSValue(object: stringHandler, in: context)

//問題語句$$$$,我懷疑是注入失敗..見鬼了
context?.setObject(handerValue, forKeyedSubscript: "stringHandler" as NSString)
let result = context?.evaluateScript("stringHandler('Hello')")

// 結果:I am Swift ,result = Optional("undefined") - - 很無解有沒有!!!!
print("I am Swift ,result = \(result?.toString())\n")複製代碼

實現場景

終於能夠運用上面的一些方法來實現功能啦。

JavaScript中的邏輯以下:

  • 確認當前使用的是UIWebView仍是WKWebView,並經過變量ritl_type肯定
  • 點擊按鈕,根據類型執行不一樣的操做
  • 客戶端經過執行iosTellSomething方法告知Web,修改當前label的值
// 默認爲WKWebView
var ritl_tyle = "WKWebView";

// 肯定是webView仍是WKWebView
function sureType(value){
  ritl_tyle = value;
};

// 按鈕點擊
function buttonDidTap (){
  var inputValue = $('#input').val()

  if (ritl_tyle == "UIWebView"){//若是是UIWebView
        RITLExportObject.say(inputValue)//經過注入的對象進行通知客戶端
  }

  else if (ritl_tyle == "WKWebView"){//若是是WKWebView
        alert("WKWebView");
        window.webkit.messageHandlers.ChangedMessage.postMessage(inputValue);
    }
};

function iosTellSomething(value){
    //document.getElementById("label").value = "收到啦";//設置給label
    $('#label').text(value);
}複製代碼

UIWebView

JSExport

定義一個自定義的協議RITLJSExport,這裏仍然採用混編模式,由於我仍是Swfit注入失敗了...

@protocol RITLJSExport <NSObject,JSExport>

// 相似typedef 將saySomething定義爲say,便於JS調用
JSExportAs(say,
- (void)saySomething:(NSString *)thing
);
@end

@interface RITLExportObject : NSObject

/// 進行的回調
@property (nonatomic, copy) void(^dosomething)(NSString *);

/// 將本身註冊到JSContext
- (void)registerSelfToContext:(JSContext *)context;

@end

@interface RITLExportObject (RITLJSExport)<RITLJSExport>
    
@end複製代碼

UIWebViewDelegate

UIWebViewDelegate中的webViewDidFinishLoad()方法中對JSContext進行截取,並執行操做:

// MARK: UIWebView-Delegate 系列
extension RITLJSWebViewController : UIWebViewDelegate {
    
    func webViewDidFinishLoad(_ webView: UIWebView) {
        
        //得到JSContent對象
        guard  let context : JSContext = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as! JSContext? else {
            return
        }
        
        //告訴web,這裏是UIWebView
        webView.stringByEvaluatingJavaScript(from: "sureType('UIWebView')")
        
        /* 使用的ObjC的Export對象 */
        let exportObject = RITLExportObject()
        exportObject.dosomething = { [weak self](value) in
            
            guard let value = value else { return }
            self?.navigationItem.title = value //設置導航欄
            
            //執行js告知,修改導航欄完畢
            webView.stringByEvaluatingJavaScript(from: "iosTellSomething('已將\(value)設置成導航Title')")//迴應
        }
        
        //進行注入
        exportObject.registerSelf(to: context)
    }
}
複製代碼

WKWebView

首先有一點,WKWebView是獲取不到JSContext的,那咋辦?不要緊,WKWebView提供給了咱們很是便利的交互,不詳細說了,以前寫的一篇博文已經介紹了,有興趣能夠看看iOS開發-------基於WKWebView的原生與JavaScript數據交互

添加JavaScript交互

// 使用WkWebView
lazy var wkWebView : WKWebView = {
    
    let webView: WKWebView = WKWebView(frame: self.view.bounds)
    
    webView.navigationDelegate = self
    webView.uiDelegate = self
    webView.configuration.userContentController.add(RITLSciptMessageHandler(self), name: "ChangedMessage")// 添加處理
    
    return webView
}()複製代碼

在WKNavigationDelegate中告知web當前使用webView的類型:

// 是爲了使用JS確認一下類型,實際開發不須要在這個代理下進行以下操做
extension RITLJSWebViewController : WKNavigationDelegate {
    
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){
        
        //確認類型
        webView.evaluateJavaScript("sureType('WKWebView')", completionHandler: nil)
    }
}複製代碼

履行WKScriptMessageHandler協議,完成交互操做便可

// MARK: WKWebView-Delegate 系列
extension RITLJSWebViewController : WKScriptMessageHandler {
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
    {
        //若是body體是約定好的字符串,而且經過標誌ChangedMessage傳遞而且存在body體
        guard message.body is String,message.name ==  "ChangedMessage",let body:String = message.body as? String else { return }
        
        navigationItem.title = body//設置導航
        
        //執行通知HTML
        wkWebView.evaluateJavaScript("iosTellSomething('已將\(body)設置成導航Title')") { (_, error) in
            print("error = \(error?.localizedDescription)")
        }
    }
}複製代碼

最後記得移除哦

deinit {
        print("\(type(of: self)) deinit")
        if ritl_useWkWebView {
            wkWebView.configuration.userContentController.removeAllUserScripts()
        }
    }複製代碼

這樣子,基於JavaScriptCore的UIWebView以及WKWebView交互就算圓滿完成啦,歡迎前去Start



做者:RITL
連接:https://www.jianshu.com/p/d8f7c5e237e8

此文章來源於第三方轉載!!

 

小編這呢,給你們推薦一個優秀的iOS交流平臺,平臺裏的夥伴們都是很是優秀的iOS開發人員,咱們專一於技術的分享與技巧的交流,你們能夠在平臺上討論技術,交流學習。歡迎你們的加入(想要進入的可加小編微信)。

微信號13142121176

相關文章
相關標籤/搜索