iOS開(kāi)發(fā)----JavaScriptCore犀变、UIWebView及WKWebView交互的那些事

參與工作時(shí)間比較長(zhǎng)了,隨著Web前端行業(yè)的發(fā)展(大家都懂得..)罗标,客戶端與Web端的交互也越來(lái)越頻繁庸队。其實(shí)本人不太喜歡依賴第三方,那種看不到摸不著的東西用起來(lái)總感覺(jué)不是很安心闯割,同時(shí)也是為了保證雙方都能夠高效完成交互的途中不出現(xiàn)一些意料不到的異常彻消,對(duì)此,研究了一下JavaScriptCore這個(gè)庫(kù)還是很有必要的宙拉,并分別結(jié)合UIWebView以及WKWebView做了一下交互總結(jié)宾尚。

寫(xiě)的比較多,如果是第一次接觸這個(gè)庫(kù)谢澈,建議還是看一看煌贴;如果時(shí)間比較緊,想直接知道結(jié)果的锥忿,送你一個(gè)捷徑??傳送門(mén)牛郑,有幫助可以Star一下,十分感謝

假設(shè)一個(gè)簡(jiǎn)單的場(chǎng)景

  • Web通過(guò)一個(gè)<input/>輸入一個(gè)字符串敬鬓,通過(guò)點(diǎn)擊按鈕設(shè)置成導(dǎo)航標(biāo)題
  • 原生設(shè)置完導(dǎo)航標(biāo)題后淹朋,告知Web"以將<#字符串#>"設(shè)置成導(dǎo)航Title笙各,并在網(wǎng)頁(yè)最底下的label顯示出來(lái)。

分別使用UIWebView以及WKWebView實(shí)現(xiàn)效果如下:

UIWebView.gif

WKWebView.gif

JavaScriptCore

類庫(kù)里面有12個(gè)類(還有兩個(gè)是負(fù)責(zé)導(dǎo)入相關(guān)類的頭文件以及一個(gè)關(guān)于WebKit的宏定義)础芍;在基本的交互過(guò)程中杈抢,其實(shí)最常使用的有三個(gè):JSContext、JSValue者甲、JSExport

JSContext

簡(jiǎn)單的理解為執(zhí)行JavaScript的一個(gè)環(huán)境春感,就好像我們?cè)诶L制View時(shí)候需要獲取的CGContext一樣,JS的執(zhí)行需要在此環(huán)境之下虏缸。

JSValue

可以理解成 一種供iOS數(shù)據(jù)結(jié)構(gòu)與JS數(shù)據(jù)結(jié)構(gòu)相互轉(zhuǎn)換的包裝鲫懒,也可以看成一種橋接關(guān)系,我們執(zhí)行JS獲取的結(jié)果就是通過(guò)JSValue對(duì)象進(jìn)行包裝傳給客戶端進(jìn)行處理的刽辙,類型轉(zhuǎn)換官方文檔描述如下:

   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數(shù)據(jù)可通過(guò)JSValue.toXXX()轉(zhuǎn)成客戶端相應(yīng)的數(shù)據(jù)結(jié)構(gòu)窥岩;反之,客戶端對(duì)象也可以通過(guò)JSValue()的構(gòu)造方法將相應(yīng)的數(shù)據(jù)結(jié)構(gòu)封裝成JSValue宰缤。

JSExport

這是一個(gè)協(xié)議颂翼,官方文檔沒(méi)有暴露出任何的open協(xié)議方法,可以理解為一個(gè)空協(xié)議慨灭。

通常用法是自定義一個(gè)CustomExport : JSExport朦乏,里面將JS可以調(diào)用的屬性或者方法進(jìn)行暴露,JS就可以直接使用暴露的屬性與方法了氧骤。

ObjC方法定義樣式是非常特殊的呻疹,但官方文檔給出了轉(zhuǎn)換后JS調(diào)用的樣式:

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

//JS
doFooWithBar(foo,bar)

但這樣會(huì)有一個(gè)缺點(diǎn),萬(wàn)一筹陵,方法有很多個(gè)參刽锤,拼接起來(lái)的JS方法名簡(jiǎn)直就是日了X;不過(guò)這點(diǎn)Apple已經(jīng)幫我們想到了朦佩,使用JSExportAs宏并思,可以將方法名簡(jiǎn)化,就像Swift中的typealias以及ObjC中的typedef语稠。

//這樣在JS中直接調(diào)用doFoo(foo,bar)即可
 JSExportAs(doFoo,
  - (void)doFoo:(id)foo withBar:(id)bar
  );

以上三個(gè)文件就算理解完了宋彼,下面來(lái)一段小應(yīng)用??。

客戶端調(diào)用JavaScript

執(zhí)行簡(jiǎn)單的JavaScript

let context = JSContext()

//方法函數(shù)定義采用的是ES6語(yǔ)法仙畦,因?yàn)樽罱趯W(xué)習(xí)RN输涕,習(xí)慣這么寫(xiě)了呢??
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)") //因?yàn)橛蟹祷刂担枰邮找幌?
//打印結(jié)果:returnValue = Optional(6)
print("__testValueInContext --- returnValue = \(returnValue?.toNumber())")

獲取定義的JavaScript變量

//通過(guò)變量名字獲取對(duì)象
let names = context?.objectForKeyedSubscript("names")

//通過(guò)定義順序的下標(biāo)獲取對(duì)象议泵,就是取['Yue','Xiao','Wen']的第0個(gè)元素
let firstName = names?.objectAtIndexedSubscript(0) //Yue

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

/// 獲得context創(chuàng)建的函數(shù)變量
let function = context?.objectForKeyedSubscript("triple")

//運(yùn)行
let result = function?.call(withArguments: [3])

//打印結(jié)果:context-function's result = Optional(6)
print("__testValueInContext --- context-function's result = \(result?.toNumber())")

捕獲執(zhí)行異常

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

/**
 執(zhí)行一個(gè)錯(cuò)誤的js,因?yàn)闆](méi)有函數(shù)Triple(上面的方法名第一字母是小寫(xiě)的),會(huì)調(diào)用上面的exceptionHandler
 打印結(jié)果: JS error = Optional(ReferenceError: Can't find variable: Triple)
 */
let _ = context?.evaluateScript("Triple(3)")

JavaScript 調(diào)用客戶端

仔細(xì)看看JSValue的類型轉(zhuǎn)換占贫,就可以知道桃熄,JS中方法就是客戶端中的閉包先口,不過(guò)這里樓主采用了Swift和ObjC混編模式型奥,至于原因下面會(huì)說(shuō)一下:(用法相似,但是真正的結(jié)構(gòu)并不一樣)

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

//結(jié)果 I am Objc, result = Optional("Hello I am append String")
print("I am Objc, result = \(result?.toString())\n")

實(shí)現(xiàn)方法:

+(JSValue *)textJavaScriptUseiOSInObjC:(NSString *)value
{
    JSContext * context = [JSContext new];
    
    //設(shè)置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版本如下碉京,功能實(shí)現(xiàn)在本人看來(lái)應(yīng)該是一樣的厢汹,但在進(jìn)行注入的時(shí)候出現(xiàn)了問(wèn)題,導(dǎo)致執(zhí)行方法出現(xiàn)了undefined谐宙。 多謝評(píng)論區(qū)我只是個(gè)仙的提示

可能是Swift的一個(gè)bug烫葬,也可能是我使用不當(dāng)
如果是我使用錯(cuò)了嚼锄,還請(qǐng)知道原因的小伙伴私信一下命黔,十分感謝。

let context = JSContext()

//初始化一個(gè)閉包
//由于OC中block與Swift中的closure結(jié)構(gòu)并不一樣站辉,需要使用`@convention(block) `關(guān)鍵詞聲明一下
let stringHandler : @convention(block)  (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)

// ~~問(wèn)題語(yǔ)句$$$$划栓,我懷疑是注入失敗..見(jiàn)鬼了~~
context?.setObject(handerValue, forKeyedSubscript: "stringHandler" as NSString)
let result = context?.evaluateScript("stringHandler('Hello')")

// ~~結(jié)果:I am Swift ,result = Optional("undefined") - - 很無(wú)解有沒(méi)有6医怼!V臆瘛蒋歌!(之前)~~
// 結(jié)果:  I am Swift ,result = Optional("Hello I am appending word with closure!")
print("I am Swift ,result = \(result?.toString())\n")

實(shí)現(xiàn)場(chǎng)景

終于可以運(yùn)用上面的一些方法來(lái)實(shí)現(xiàn)功能啦。

JavaScript中的邏輯如下:

  • 確認(rèn)當(dāng)前使用的是UIWebView還是WKWebView,并通過(guò)變量ritl_type確定
  • 點(diǎn)擊按鈕委煤,根據(jù)類型執(zhí)行不同的操作
  • 客戶端通過(guò)執(zhí)行iosTellSomething方法告知Web堂油,修改當(dāng)前l(fā)abel的值
// 默認(rèn)為WKWebView
var ritl_tyle = "WKWebView";

// 確定是webView還是WKWebView
function sureType(value){
  ritl_tyle = value;
};

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

  if (ritl_tyle == "UIWebView"){//如果是UIWebView
        RITLExportObject.say(inputValue)//通過(guò)注入的對(duì)象進(jìn)行通知客戶端
  }

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

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

UIWebView

JSExport

定義一個(gè)自定義的協(xié)議RITLJSExport,這里仍然采用混編模式,因?yàn)槲疫€是Swfit注入失敗了...

Objective

@protocol RITLJSExport <NSObject,JSExport>

// 類似typedef 將saySomething定義為say,便于JS調(diào)用
JSExportAs(say,
- (void)saySomething:(NSString *)thing
);
@end

@interface RITLExportObject : NSObject

/// 進(jìn)行的回調(diào)
@property (nonatomic, copy) void(^dosomething)(NSString *);

/// 將自己注冊(cè)到JSContext
- (void)registerSelfToContext:(JSContext *)context;

@end

@interface RITLExportObject (RITLJSExport)<RITLJSExport>
    
@end

Swift

import UIKit

/// 必須追加@objc
@objc protocol RITLJSSwiftExport: JSExport {
    
    /// 方法的標(biāo)簽一定記得去掉
    func say(_ something: String)
}

/// 必須追加@objc
@objc class RITLExportSwiftObject: NSObject {

    var doSomething: ((String?) -> Void)?
    override init() {
        super.init()
    }
}

extension RITLExportSwiftObject : RITLJSSwiftExport {
    
    func say(_ something: String) {
        doSomething?(something)
    }
}

UIWebViewDelegate

UIWebViewDelegate中的webViewDidFinishLoad()方法中對(duì)JSContext進(jìn)行截取碧绞,并執(zhí)行操作:

// MARK: UIWebView-Delegate 系列
extension RITLJSWebViewController : UIWebViewDelegate {
    
    func webViewDidFinishLoad(_ webView: UIWebView) {
        
        //獲得JSContent對(duì)象
        guard  let context : JSContext = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as! JSContext? else {
            return
        }
        
        //告訴web府框,這里是UIWebView
        webView.stringByEvaluatingJavaScript(from: "sureType('UIWebView')")
        
        /* 使用的ObjC的Export對(duì)象 */
        let exportObject = RITLExportObject()
        exportObject.dosomething = { [weak self](value) in
            
            guard let value = value else { return }
            self?.navigationItem.title = value //設(shè)置導(dǎo)航欄
            
            //執(zhí)行js告知,修改導(dǎo)航欄完畢
            webView.stringByEvaluatingJavaScript(from: "iosTellSomething('已將\(value)設(shè)置成導(dǎo)航Title')")//回應(yīng)
        }
        
        //進(jìn)行注入
        exportObject.registerSelf(to: context)


        // 使用Swift的Export對(duì)象
        let exportObject = RITLExportSwiftObject()
        
        exportObject.doSomething =  { [weak self](value) in
            guard let value = value else { return }
            DispatchQueue.main.async {
                //設(shè)置導(dǎo)航欄
                self?.navigationItem.title = value
                
                //執(zhí)行js告知头遭,修改導(dǎo)航欄完畢
                webView.stringByEvaluatingJavaScript(from: "iosTellSomething('已將\(value)設(shè)置成導(dǎo)航Title')")//回應(yīng)
            }
        }
                
        context.setObject(exportObject, forKeyedSubscript: "RITLExportObject" as NSString)
    }
}

WKWebView

首先有一點(diǎn)寓免,WKWebView是獲取不到JSContext的,那咋辦计维?沒(méi)關(guān)系袜香,WKWebView提供給了我們非常便利的交互,不詳細(xì)說(shuō)了鲫惶,之前寫(xiě)的一篇博文已經(jīng)介紹了蜈首,有興趣可以看看iOS開(kāi)發(fā)-------基于WKWebView的原生與JavaScript數(shù)據(jù)交互

添加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當(dāng)前使用webView的類型:

// 是為了使用JS確認(rèn)一下類型,實(shí)際開(kāi)發(fā)不需要在這個(gè)代理下進(jìn)行如下操作
extension RITLJSWebViewController : WKNavigationDelegate {
    
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){
        
        //確認(rèn)類型
        webView.evaluateJavaScript("sureType('WKWebView')", completionHandler: nil)
    }
}

履行WKScriptMessageHandler協(xié)議欠母,完成交互操作即可

// MARK: WKWebView-Delegate 系列
extension RITLJSWebViewController : WKScriptMessageHandler {
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
    {
        //如果body體是約定好的字符串欢策,并且通過(guò)標(biāo)志ChangedMessage傳遞并且存在body體
        guard message.body is String,message.name ==  "ChangedMessage",let body:String = message.body as? String else { return }
        
        navigationItem.title = body//設(shè)置導(dǎo)航
        
        //執(zhí)行通知HTML
        wkWebView.evaluateJavaScript("iosTellSomething('已將\(body)設(shè)置成導(dǎo)航Title')") { (_, error) in
            print("error = \(error?.localizedDescription)")
        }
    }
}

最后記得移除哦

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末踩寇,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子六水,更是在濱河造成了極大的恐慌俺孙,老刑警劉巖辣卒,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異睛榄,居然都是意外死亡荣茫,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)场靴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)啡莉,“玉大人,你說(shuō)我怎么就攤上這事旨剥∵中溃” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵轨帜,是天一觀的道長(zhǎng)该押。 經(jīng)常有香客問(wèn)我,道長(zhǎng)阵谚,這世上最難降的妖魔是什么蚕礼? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮梢什,結(jié)果婚禮上奠蹬,老公的妹妹穿的比我還像新娘。我一直安慰自己嗡午,他們只是感情好囤躁,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著荔睹,像睡著了一般狸演。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上僻他,一...
    開(kāi)封第一講書(shū)人閱讀 52,337評(píng)論 1 310
  • 那天宵距,我揣著相機(jī)與錄音,去河邊找鬼吨拗。 笑死满哪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的劝篷。 我是一名探鬼主播哨鸭,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼娇妓!你這毒婦竟也來(lái)了像鸡?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤哈恰,失蹤者是張志新(化名)和其女友劉穎只估,沒(méi)想到半個(gè)月后华望,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡仅乓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蓬戚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片夸楣。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖子漩,靈堂內(nèi)的尸體忽然破棺而出豫喧,到底是詐尸還是另有隱情,我是刑警寧澤幢泼,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布紧显,位于F島的核電站,受9級(jí)特大地震影響缕棵,放射性物質(zhì)發(fā)生泄漏孵班。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一招驴、第九天 我趴在偏房一處隱蔽的房頂上張望篙程。 院中可真熱鬧,春花似錦别厘、人聲如沸虱饿。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)氮发。三九已至,卻和暖如春冗懦,著一層夾襖步出監(jiān)牢的瞬間爽冕,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工披蕉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扇售,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓嚣艇,卻偏偏與公主長(zhǎng)得像承冰,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子食零,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 跟原生開(kāi)發(fā)相比困乒,H5的開(kāi)發(fā)相對(duì)來(lái)一個(gè)成熟的框架和團(tuán)隊(duì)來(lái)講在開(kāi)發(fā)速度和開(kāi)發(fā)效率上有著比原生很大的優(yōu)勢(shì),至少不用等待審...
    大沖哥閱讀 1,847評(píng)論 0 7
  • 前言 Web 頁(yè)面中的 JS 與 iOS Native 如何交互是每個(gè) iOS 猿必須掌握的技能贰谣。而說(shuō)到 Nati...
    幽城88閱讀 2,208評(píng)論 1 8
  • 隨著H5技術(shù)的興起娜搂,在iOS開(kāi)發(fā)過(guò)程中迁霎,難免會(huì)遇到原生應(yīng)用需要和H5頁(yè)面交互的問(wèn)題。其中會(huì)涉及方法調(diào)用及參數(shù)傳值等...
    Chris_js閱讀 3,088評(píng)論 1 8
  • JavaScriptCore框架主要是用來(lái)實(shí)現(xiàn)iOS與H5的交互百宇。由于現(xiàn)在混合編程越來(lái)越多考廉,H5的相對(duì)講多,所以研...
    水靈芳蕥閱讀 1,409評(píng)論 1 8
  • 每次看完一部劇携御,無(wú)論結(jié)果是好是壞昌粤,是悲是喜,心中總是涌起一股傷感啄刹。剛看完戰(zhàn)爭(zhēng)劇戰(zhàn)長(zhǎng)沙涮坐,長(zhǎng)沙這個(gè)我今后將生活與學(xué)習(xí)的...
    柚柚柚子青茶閱讀 269評(píng)論 0 0