前言
當前混合開發模式迎來了前所未有的發展,跨平臺開發、熱更新等優點決定了這種模式的重要地位。雖然前端介面在互動、動效等多方面距離原生應用還有差距,但毫無疑問混合開發只會被越來越多的公司接受。在iOS中,混合開發模式被分為兩個時代,分別是iOS7之前的坑爹時代與之後的黃金時代,其分割代表為JavaScriptCore
框架
坑爹時代
作為完美避開iOS7之前版本的幸運兒,我只能從某位前輩的口中得知那悲慘的歲月。作為那個年代唯一能與前端介面互動的手段就是UIWebView
,先不說它自身的記憶體洩露缺陷,下面是一段前輩寫過的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (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
程式碼加入到執行環境中
12345678910let 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
程式碼執行過程中,可能會出現語法錯誤等多種錯誤,通過下面的程式碼可以對這些錯誤進行處理
123context?.exceptionHandler = { context, exception inprint("Java Script Run Error: \(exception)")} - JSValue
JSValue
是所有JSContext
操作後返回的值,包裝了幾乎所有的資料型別,包括錯誤和IMP
指標等。在JSValue
類結構中存在多個toXXXX
命名的方法轉換成iOS
資料型別以及call
方法來呼叫方法。下面的程式碼從JSContext
環境中獲取已存在的部分變數,並且執行建立一個儲存person
資訊的字典
12345678910let 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
訪問我們程式碼中的物件以及方法有兩種方式:Blocks
和JSExport
。
- Blocks
自定義的block
程式碼可以通過JSContext
轉換成JS
程式碼中的函式指標呼叫,這裡存在一個坑就是Swift
中的閉包無法完成這樣的型別轉換,因此這種方式的操作流程在Swift
中是這樣的:Closure
->block
->function pointer
。在閉包轉成block
的這一過程中,需要使用一個重要的關鍵符@convention
1234567891011let stringConvert: @convention(block) (String)->String = {let pinyin = NSMutableString(string: $0) as CFMutableStringCFStringTransform(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
表示按鈕點選的響應事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<!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
都是一樣的:
1 2 3 4 5 6 |
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
的設定方式如下
1 |
interactionContext?.setObject(self, forKeyedSubscript: "sindrilin") |
響應者已經有了,那麼響應事件也要我們實現程式碼,這裡就需要用到JSExport
協議了。所有這種類似Target-Action
的事件觸發都會通過這個協議獲取方法實現,因此我們需要自定義響應協議以及響應事件。對於有引數的方法我們需要用@objc(name)
的方式給方法起OC
式的方法名,才能保證能被正確呼叫響應:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@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下載