iOS開發-javaScript交互

前言

當前混合開發模式迎來了前所未有的發展,跨平臺開發、熱更新等優點決定了這種模式的重要地位。雖然前端界面在交互、動效等多方面距離原生應用還有差距,但毫無疑問混合開發只會被越來越多的公司接受。在iOS中,混合開發模式被分為兩個時代,分別是iOS7之前的坑爹時代與之后的黃金時代,其分割代表為JavaScriptCore框架

坑爹時代

作為完美避開iOS7之前版本的幸運兒,我只能從某位前輩的口中得知那悲慘的歲月。作為那個年代唯一能與前端界面交互的手段就是UIWebView,先不說它自身的內存泄露缺陷,下面是一段前輩寫過的代碼:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSString * address = request.URL.absoluteString;
    for (NSString * black in _blackList) {
        if ([address containsString: black]) {
            return NO;
        }
    }
    for (NSString * event in _eventList) {
        if ([address containsString: event]) {
            SEL callback = NSSelectorFromString(_callbacks[event]);
            [self performSelector: callback];
            return [event containsString: @"shouldOpen=1"];
        }
    }
    return YES;
}

在那個年代,前輩的小伙伴們把前端事件的觸發條件設置為鏈接跳轉,然后通過鏈接中的關鍵字符來判斷處理操作。為此,需要定義好些個數據集合來存儲這些關鍵字符的處理操作。如果遇到應用和前端交換交互數據的時候,那一長串的參數字符全部拼接在請求地址里,想想也是醉了。另外的交互方法就是通過stringByEvaluatingJavaScriptFromString方法來執行js代碼。

JavaScriptCore

JavaScriptCore是一套用來對JS代碼進行解析和提供執行環境的開源框架,極大的簡化了我們的交互過程。下面從項目和JS代碼相互調用的兩個不同操作介紹其中相對應的方法

項目調用JS代碼

  • JSContext
    一個JSContext對象是JavaScript運行的全局環境對象,它提供了代碼運行和注冊方法接口的服務。下面的代碼就創建了一個JSContext對象,并且定義了一部分的JS代碼加入到執行環境中
    let context = JSContext()
    context.evaluateScript(" var age = 22 ")
    context.evaluateScript(" var name = 'SindriLin' ")
    context.evaluateScript(" var birth = 1993-01-01 ")
    context.evaluateScript(" var createPerson =
    function(age, name, birth)
    {
    return {'age': age, 'name': name, 'birth': birth}
    } ")
    context.evaluateScript(" var codeDescription = 'The code create three value and a function to create a dictionary stored person information' ")
    此外,在JS代碼執行過程中,可能會出現語法錯誤等多種錯誤,通過下面的代碼可以對這些錯誤進行處理
    context?.exceptionHandler = { context, exception in
    print("Java Script Run Error: (exception)")
    }

  • JSValue
    JSValue是所有JSContext操作后返回的值,包裝了幾乎所有的數據類型,包括錯誤和IMP指針等。在JSValue類結構中存在多個toXXXX命名的方法轉換成iOS數據類型以及call方法來調用方法。下面的代碼從JSContext環境中獲取已存在的部分變量,并且執行創建一個存儲person信息的字典
    let age = context?.objectForKeyedSubscript("age")
    let name = context?.objectForKeyedSubscript("name")
    let birth = context?.objectForKeyedSubscript("birth")
    let createFunction = context?.objectForKeyedSubscript("createPerson")
    let codeDescription = context?.objectForKeyedSubscript("codeDescription")
    let person = createFunction.call(withArguments: [age.toInt32(), name.toString(), birth.toString()])

    let personInfo = "name: \(person["name"]) age: \(person["age"] and birth: \(person["birth"])"
    print("The javaScript code description: \(codeDescription.toString())")
    print("The created person \(personInfo) ")
    

通過上面的例子,我們可以看到,只要了解到JS代碼中我們需要調用的方法信息,通過JSContext + JSValue的方式我們就能輕松的在項目中調用前端界面的方法,而不再需要拼接長串參數字符通過鏈接地址傳遞給前端界面

JS調用項目代碼

JavaScript訪問我們代碼中的對象以及方法有兩種方式:BlocksJSExport

  • Blocks
    自定義的block代碼可以通過JSContext轉換成JS代碼中的函數指針調用,這里存在一個坑就是Swift中的閉包無法完成這樣的類型轉換,因此這種方式的操作流程在Swift中是這樣的:Closure -> block -> function pointer。在閉包轉成block的這一過程中,需要使用一個重要的關鍵符@convention
    let stringConvert: @convention(block) (String)->String = {
    let pinyin = NSMutableString(string: $0) as CFMutableString
    CFStringTransform(pinyin, nil, kCFStringTransformToLatin, false)
    CFStringTransform(pinyin, nil, kCFStringTransformStripCombiningMarks, false)
    return pinyin as String
    }

    let convertObjc = unsafeBitCast(stringConvert, to: AnyObject.self)
    context?.setObject(convertObjc, forKeyedSubscript: "convertFunc")
    let convertFunc = context?.objectForKeyedSubscript("convertFunc")
    print("林欣達的拼音是\(convertFunc.call(withArguments: ["林欣達"]).toString())")
    

    這時候,只要前端在JS的按鈕點擊代碼中調用convertFunc()這句代碼就會執行這個closure中的代碼。使用這種方式要注意由于閉包的捕獲特性,有可能會導致你的JSContext對象被引用而無法被釋放,使用JSContext.current()獲取當前上下文來解決引用問題

  • JSExport
    JS中調用iOS方法的時候,通過調用JSExport的派生協議方法來實現。所有派生協議的方法會自動提供給JavaScript代碼使用,這個在下面的demo中可以看到

實戰

在本文demo中我寫了一段JS代碼,下面放出這段代碼以及運行效果。其中要注意的是按鈕的onclik表示按鈕點擊的響應事件:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <div style="margin-top: 20px">
            <h2 align="center" style="color:#ff0000">JS與iOS交互</h2>
            <input type="button" value="點擊后切換控制器的背景顏色" onclick="sindrilin.call()">
        </div>
        <div style="color:#7BBDE5">
            <br />
            <br />
            賬戶:
            <input id="account" type="text">
            <br />
            密碼:
            <input id="password" type="password">
        </div>
        <div>
            <input type="button" value="登錄" onclick="login()">
        </div>
    
        <script>
        
            var login = function()
            {
                account = document.getElementById("account")
                password = document.getElementById("password")
                var accountInfo = JSON.stringify({"account": account.value, "password": password.value});
                sindrilin.login(accountInfo);
            }
    
            var alertFromIOS = function(message)
            {
                    alert(message)
            }
    
        </script>
    </body>
</html>


首先我們需要加載這個HTML文件,然后獲取代碼運行的全局環境對象。基本上在所有的HTML格式文件中,獲取環境對象的keyPath都是一樣的:

let jsPath = Bundle.main().pathForResource("interaction", ofType: "html")
webView.loadRequest(URLRequest(url: URL(fileURLWithPath: jsPath!)))
interactionContext = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as? JSContext
interactionContext?.exceptionHandler = {
    print("Interaction Error: \($1?.toString())")
}

對照HTML代碼,最上面的按鈕點擊之后會調用一個sindrilin.call()的方法,這個方法最終要由我們的控制器來進行處理。我們可以把這個字符串分成類似Target-Action機制的兩部分,前者sindrilin表示響應者,后面call()表示響應事件。其中Target的設置方式如下

interactionContext?.setObject(self, forKeyedSubscript: "sindrilin")

響應者已經有了,那么響應事件也要我們實現代碼,這里就需要用到JSExport協議了。所有這種類似Target-Action的事件觸發都會通過這個協議獲取方法實現,因此我們需要自定義響應協議以及響應事件。對于有參數的方法我們需要用@objc(name)的方式給方法起OC式的方法名,才能保證能被正確調用響應:

@objc protocol LXDInteractionExport: JSExport {
    func call()                                    ///響應sindrilin.call()
    @objc(login:) func login(accountInfo: String)  ///響應sindrilin.login(accountInfo)
}

extension ViewController: LXDInteractionExport {
    func call() {
        print("call from html button clicked")
        view.backgroundColor = UIColor(red: CGFloat(arc4random() % 256) / 255, green: CGFloat(arc4random() % 256) / 255, blue: CGFloat(arc4random() % 256) / 255, alpha: 1)
    }

    func login(accountInfo: String) {
        do {
            if let JSON: [String: String] = try JSONSerialization.jsonObject(with: accountInfo.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [String: String] {
                print("JSON: \(JSON)")
                let alert = interactionContext?.objectForKeyedSubscript("alertFromIOS")
                let message = "The alert from javascript call\naccount: \(JSON["account"]) and password: \(JSON["password"])"
                _ = alert?.call(withArguments: [message])
            }
        } catch {
            print("Error: \(error)")
        }      
    }
}

用戶在前端界面輸入賬戶和密碼信息之后點擊登錄就會調用login(accountInfo: String)方法,將用戶名和密碼拼湊成JSON字符串傳遞過來。在響應方法中我解析獲取對應字段的用戶信息,并且組轉成新的字符串調用JS的彈窗函數彈出響應。demo下載

關注iOS開發文集收看更多文章
轉載請注明原文作者和地址

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本教程中所涉及到的幾種類型: JSContext, JSContext是代表JS的執行環境,通過-evaluate...
    貝勒老爺閱讀 888評論 0 5
  • 跟原生開發相比,H5的開發相對來一個成熟的框架和團隊來講在開發速度和開發效率上有著比原生很大的優勢,至少不用等待審...
    大沖哥閱讀 1,865評論 0 7
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,923評論 18 139
  • 注:JavaScriptCore API也可以用Swift來調用,本文用Objective-C來介紹。 在iOS7...
    JW_T閱讀 562評論 0 0
  • -1- 眼前盤曲的公路延伸著,消失在拐角處,通往一個于我而言完全陌生的城市。 百無聊賴的坐著,耳邊是謝春花清脆透亮...
    荷默閱讀 303評論 10 8