混合 APP 開發(Hybrid App)

目錄

  • 混合 App
  • Html5簡介
  • UIWebView 和 WKWebView
  • UIWebView 和 JS 交互
  • WKWebView 和 JS 交互
  • JS 調用 Native 相機


一. 混合 APP

Hybrid Mobile App 能夠理解爲經過 Web 網絡技術(如 HTML,CSS 和 JavaScript)與 Native 相結合的混合移動應用程序。html

H5用於大致界面的編寫,如:須要一些基本的輸入框、單選按鈕、普通按鈕、以及下拉選擇框等。前端

CSS3則是主要用於對總體界面細節化的修飾。好比:一個普通按鈕,輸入框邊角默認是直角,那咱們能夠用CSS來改變其形狀。java

還能夠用來設置不一樣的樣式。ios

JS主要是要跟服務端打交道,實現數據交互。JS中的數據交互,主要以JSON格式跟XML格式這兩種格式實現。git

整體來講,H5+CSS3負責界面的搭建,JS負責數據的交互。github


二. HTML5簡介


下面簡述一下 Hybrid 的發展史:web


1.H5 發佈swift


Html5 是在 2014 年 9 月份正式發佈的,這一次的發佈作了一個最大的改變就是「從之前的 XML 子集升級成爲一個獨立集合」。api




2.H5 滲入 Mobile App 開發瀏覽器


Native APP 開發中有一個 webview 的組件(Android 中是 webview,iOS 有 UIWebview和 WKWebview),這個組件能夠加載 Html 文件。

在 H5 大行其道以前,webview 加載的 web 頁面很單調(由於只能加載一些靜態資源),自從 H5 火了以後,前端猿們開發的 H5 頁面在 webview 中的表現不俗使得 H5 開發慢慢滲透到了 Mobile App 開發中來。



3.Hybrid 現狀


雖然目前已經出現了 RN 和 Weex 這些使用 JS 寫 Native App 的技術,可是 Hybrid 仍然沒有被淘汰,市面上大多數應用都不一樣程度的引入了 Web 頁面。


三. UIWebView 和 WKWebView

作瀏覽器首先要選個好的基礎。iOS8提供兩類瀏覽組件:UIWebView和WKWebView。

UIWebView是iOS傳統的瀏覽控件,絕大多數瀏覽器都採用這個控件做爲基礎, 如Chrome,Firefox,Safari。UIWebView比較封閉,不少API都不開放,但卻一度是惟一的選擇。好處是,這個控件使用時間比較長,有不少方案能夠參考。

WKWebView是蘋果在iOS8和 OS X Yosemite 中新推出的WebKit中的一個組件。

它代替了 UIKit 中的UIWebView和AppKit中的WebView,提供了統一的跨雙平臺 API。支持HTML5的特性, 佔用內存可能只有UIWebView的1/3 ~ 1/4, 擁有 60fps 滾動刷新率、內置手勢、高效的app和web信息交換通道、和Safari相同的JavaScript引擎, 增長了加載進度屬性, 比UIWebView性能更增強大。

但WKWebView也不是那麼完美:如沒有控制Cookie的API, 對讀取本地html文件的支持也很差等。


四. UIWebView 和 JS 交互


JavaScriptCore介紹


JavaScriptCore 這個庫是 Apple 在 iOS 7 以後加入到標準庫的,它對 iOS Native 與 JS 作交互調用產生了劃時代的影響。

JavaScriptCore 大致是由 4 個類以及 1 個協議組成的:



  • JSContext 是 JS 執行上下文,你能夠把它理解成 JavaScriptCore 包裝出來的 JS 運行的環境。
  • JSValue 是對 JavaScript 值的引用,任何 JS 中的值均可以被包裝爲一個 JSValue。
  • JSManagedValue 是對 JSValue 的包裝,加入了「conditional retain」。
  • JSVirtualMachine 能夠理解爲JS 虛擬機, 在JSVirtualMachine中能夠建立多個 JSContext 實例, 他們都是能夠獨立運行的 JavaScript 執行環境。
  • JSExport 協議:咱們可使用這個協議暴露原生對象,實例方法,類方法,和屬性給JavaScript,這樣JavaScript就能夠調用相關暴露的方法和屬性。


Native 調用 JS:


  • WebView 直接注入 JS 並執行
  • JavaScriptCore 方法
WebView 直接注入 JS 並執行

self.webView.stringByEvaluatingJavaScript(from: 「jsFuncName()」)

注意:
這個方法會返回運行 JS 的結果(nullable NSString *),它是一個同步方法,會阻塞當前線程!儘管此方法不被棄用,但最佳作法是使用 WKWebView 類的 evaluateJavaScript:completionHandler:method。注意:
這個方法會返回運行 JS 的結果(nullable NSString *),它是一個同步方法,會阻塞當前線程!儘管此方法不被棄用,但最佳作法是使用 WKWebView 類的 evaluateJavaScript:completionHandler:method。複製代碼


JavaScriptCore 方法
// 導入 JavaScriptCore 庫

JavaScriptCore 庫提供的 JSValue 類,是對 JavaScript 值的引用。 您可使用 JSValue 類來轉換 JavaScript 和 Objective-C 或 Swift 之間的基本值(如數字和字符串),以便在本機代碼和 JavaScript 代碼之間傳遞數據。

Native 代碼: 
self.context = webView.value(forKeyPath: 「documentView.webView.mainFrame.javaScriptContext") let jsValue: JSValue = self.context.objectForKeyedSubscript(「jsFuncName()」) jsValue.call(withArguments: ["param1" ,"param2"]) JS 代碼: function jsFuncName(param1, param2){ } 複製代碼



JS 調用 Native :


  • 攔截 URL 請求
  • Block 方法
  • 模型注入(JavaScriptCore 的 JSExport 協議)
攔截 URL 請求

用JS 發起一個假的 URL 請求, 而後在 shouldStartLoadWith 代理方法中攔截此次請求, 作出相應處理.
注意: 
這裏在JS 中自定義一個loadURL 方法發起請求,而不是直接使用 window.location.href
若是要傳遞參數, 能夠拼接在 URL 上

Native 代碼:
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        if request.url?.scheme == "haleyAction" {
            // to do something
            return false
        }
       return true
 }
        
JS 代碼:
function loadURL(url) {
    var iFrame;
    iFrame = document.createElement("iframe");
            iFrame.setAttribute("src", url);
            iFrame.setAttribute("style", "display:none;");
            iFrame.setAttribute("height", "0px");
            iFrame.setAttribute("width", "0px");
            iFrame.setAttribute("frameborder", "0");
            document.body.appendChild(iFrame);
            // 發起請求後這個 iFrame 就沒用了,因此把它從 dom 上移除掉
            iFrame.parentNode.removeChild(iFrame);
            iFrame = null;
        }
    
        function firstClick() {
            //要傳遞參數時, 能夠拼接在url上
            loadURL("haleyAction://shareClick?title=測試分享的標題&content=測試分享的內容&url=http://www.baidu.com");
        }複製代碼


Block 方法

使用 block 在js中運行原生代碼, 將自動與JavaScript方法創建橋樑
注意: 這種方法僅僅適用於 OC 的 block, 並不適用於swift中的閉包, 爲了公開閉包,      
咱們將進行以下兩步操做:
(1)使用 @convention(block) 屬性標記閉包,來創建橋樑成爲 OC 中的 block
(2)在映射 block 到 JavaScript方法調用以前,咱們須要 unsafeBitCast 函數將block 轉成爲 AnyObject

Native 代碼:
// JS調用了無參數swift方法
let closure1: @convention(block) () ->() = {
            
}
self.context.setObject(unsafeBitCast(closure1, to: AnyObject.self),   
forKeyedSubscript: "test1" as NSCopying & NSObjectProtocol)

// JS調用了有參數swift方法
let closure2: @convention(block) () ->() = {
            
}
self.context.setObject(unsafeBitCast(closure2, to: AnyObject.self), 
forKeyedSubscript: "test2" as NSCopying & NSObjectProtocol)

JS 代碼:
function JS_Swift1(){
    test1();
}
function JS_Swift2(){
    test2('oc','swift');
}注意: 這種方法僅僅適用於 OC 的 block, 並不適用於swift中的閉包, 爲了公開閉包,      
咱們將進行以下兩步操做:
(1)使用 @convention(block) 屬性標記閉包,來創建橋樑成爲 OC 中的 block
(2)在映射 block 到 JavaScript方法調用以前,咱們須要 unsafeBitCast 函數將block 轉成爲 AnyObject

Native 代碼:
// JS調用了無參數swift方法
let closure1: @convention(block) () ->() = {
            
}
self.context.setObject(unsafeBitCast(closure1, to: AnyObject.self),   
forKeyedSubscript: "test1" as NSCopying & NSObjectProtocol)

// JS調用了有參數swift方法
let closure2: @convention(block) () ->() = {
            
}
self.context.setObject(unsafeBitCast(closure2, to: AnyObject.self), 
forKeyedSubscript: "test2" as NSCopying & NSObjectProtocol)

JS 代碼:
function JS_Swift1(){
    test1();
}
function JS_Swift2(){
    test2('oc','swift');
}複製代碼



模型注入(JavaScriptCore 的 JSExport 協議)

步驟一: 自定義協議服從 JSExport協議
可使用該協議暴露原生對象,實例方法,類方法,和屬性給JavaScript,這樣JavaScript就能夠調用相關暴露的方法和屬性。遵照JSExport協議,就能夠定義咱們本身的協議,在協議中聲明的API都會在JS中暴露出來

注意:
若是js是多個參數的話  咱們代理方法的全部變量前的名字連起來要和js的方法名字同樣好比: js方法爲  OCModel.showAlertMsg('js title', 'js message’),他有兩個參數 那麼咱們的代理方法 就是把js的方法名 showAlertMsg 任意拆分紅兩段做爲代理方法名 第一個參數的 argumentLabel 用 "_" 隱藏 @objc protocol JavaScriptSwiftDelegate: JSExport { func callNoParam() func showAlert(_ title: String, msg: String) } 步驟二: 自定義模型服從自定義協議, 實現協議方法 @objc class JSObjCModel: NSObject, JavaScriptSwiftDelegate { weak var controller: UIViewController? weak var jsContext: JSContext? func callNoParam() { let jsFunc = self.jsContext?.objectForKeyedSubscript("jsFunc"); _ = jsFunc?.call(withArguments: []); } func showAlert(_ title: String, msg: String) { let alert = UIAlertController(title: title, message: msg, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "肯定", style: .default, handler: nil)) self.controller?.present(alert, animated: true, completion: nil) } } 步驟三: 將模型對象注入 JS // 模型注入 let model = JSObjCModel() model.controller = self model.jsContext = context // 這一步是將OCModel這個模型注入到JS中,在JS就能夠經過OCModel調用咱們暴露的方法了 context.setObject(model, forKeyedSubscript: "OCModel" as NSCopying & NSObjectProtocol) let url = Bundle.main.url(forResource: "WebView", withExtension: "html") context.evaluateScript(try? String.init(contentsOf: url!, encoding: .utf8)) context.exceptionHandler = { [unowned self](con, except) in self.context.exception = except } JS 代碼: <div class='btn-button' onclick="OCModel.callNoParam()">JS調用Native方式三無參</div> <div class='btn-button' onclick="OCModel.showAlertMsg('js title', 'js message’)">JS調用Native方式三有參</div>(JavaScriptCore 的 JSExport 協議) 步驟一: 自定義協議服從 JSExport協議 可使用該協議暴露原生對象,實例方法,類方法,和屬性給JavaScript,這樣JavaScript就能夠調用相關暴露的方法和屬性。遵照JSExport協議,就能夠定義咱們本身的協議,在協議中聲明的API都會在JS中暴露出來 注意: 若是js是多個參數的話 咱們代理方法的全部變量前的名字連起來要和js的方法名字同樣好比: js方法爲 OCModel.showAlertMsg('js title', 'js message’),他有兩個參數 那麼咱們的代理方法 就是把js的方法名 showAlertMsg 任意拆分紅兩段做爲代理方法名 第一個參數的 argumentLabel 用 "_" 隱藏 @objc protocol JavaScriptSwiftDelegate: JSExport { func callNoParam() func showAlert(_ title: String, msg: String) } 步驟二: 自定義模型服從自定義協議, 實現協議方法 @objc class JSObjCModel: NSObject, JavaScriptSwiftDelegate { weak var controller: UIViewController? weak var jsContext: JSContext? func callNoParam() { let jsFunc = self.jsContext?.objectForKeyedSubscript("jsFunc"); _ = jsFunc?.call(withArguments: []); } func showAlert(_ title: String, msg: String) { let alert = UIAlertController(title: title, message: msg, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "肯定", style: .default, handler: nil)) self.controller?.present(alert, animated: true, completion: nil) } } 步驟三: 將模型對象注入 JS // 模型注入 let model = JSObjCModel() model.controller = self model.jsContext = context // 這一步是將OCModel這個模型注入到JS中,在JS就能夠經過OCModel調用咱們暴露的方法了 context.setObject(model, forKeyedSubscript: "OCModel" as NSCopying & NSObjectProtocol) let url = Bundle.main.url(forResource: "WebView", withExtension: "html") context.evaluateScript(try? String.init(contentsOf: url!, encoding: .utf8)) context.exceptionHandler = { [unowned self](con, except) in self.context.exception = except } JS 代碼: <div class='btn-button' onclick="OCModel.callNoParam()">JS調用Native方式三無參</div> <div class='btn-button' onclick="OCModel.showAlertMsg('js title', 'js message’)">JS調用Native方式三有參</div>複製代碼


五. WKWebView 與 JS 交互


WKWebView 的配置

//導入 WebKit
//建立配置類
let confirgure = WKWebViewConfiguration()
             
//WKUserContentController: 內容交互控制器
confirgure.userContentController = WKUserContentController()
        
//建立WKWebView
wkWebView = WKWebView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height), configuration: confirgure)
        
//配置代理
wkWebView.navigationDelegate = self as WKNavigationDelegate
wkWebView.uiDelegate = self as WKUIDelegate複製代碼


Native 調用 JS


  • WebView 直接注入 JS 並執行


不一樣於 UIWebView,WKWebView 注入並執行 JS 的方法不會阻塞當前線程。由於考慮到 webview 加載的 web content 內 JS 代碼不必定通過驗證,若是阻塞線程可能會掛起 App。

self.wkWebView.evaluateJavaScript(「jsFuncName()") { (result, error) in print(result, error) } 注意: 方法不會阻塞線程,並且它的回調代碼塊老是在主線程中運行。注意: 方法不會阻塞線程,並且它的回調代碼塊老是在主線程中運行。複製代碼


JS 調用 Native


  • 攔截 URL 請求
  • Webkit 的 WKUIDelegate協議
  • 模型注入(Webkit 的 WKScriptMessageHandler協議)

攔截 URL 請求
攔截請求的代理方法爲 WebKit 中 WKNavigationDelegate 協議的

 func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: ) 方法

, 其它同 WebView複製代碼


Webkit 的 WKUIDelegate協議

WKUIDelegate 協議包含一些函數用來監聽 web JS 想要顯示 alert 或 confirm 時觸發。咱們若是在 WKWebView 中加載一個 web 而且想要 web JS 的 alert 或 confirm 正常彈出,就須要實現對應的代理方法。

以JS 彈出Confirm 爲例, 下面是在 WKUIDelegate 監聽 web 要顯示 confirm 的代理方法中用 Native UIAlertController 替代 JS 中的 confirm 顯示的 例子: 

//經過 message 獲得JS 端所傳的數據,在 ios 端顯示原生 alert 獲得 true/false 後經過 completionHandler 回調給 JS

Native 代碼:
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
        let alert = UIAlertController(title: "Confirm", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) -> Void in
            completionHandler(true)
        }))
        alert.addAction(UIAlertAction(title: "cancel", style: .cancel, handler: { (_) -> Void in
            completionHandler(false)
        }))
        self.present(alert, animated: true, completion: nil)
}

JS 代碼:
function callJsConfirm() {
        if (confirm('confirm', 'Objective-C call js to show confirm')) {
            d ocument.getElementById('jsParamFuncSpan').innerHTML = 'true';
        }else {
             document.getElementById('jsParamFuncSpan').innerHTML = 'false';
        }
}
複製代碼


模型注入(Webkit 的 WKScriptMessageHandler協議)

注意: 
對象注入寫在 viewWillAppear 中, 防止循環引用

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        //注入對象名稱 APPModel, 當 JS 經過 APPModel 調用時, 能夠在 WKScriptMessageHandler 代理方法中接收到
        wkWebView.configuration.userContentController.add(self, name: "APPModel")
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        wkWebView.configuration.userContentController.removeScriptMessageHandler(forName: "APPModel")
          }

JS 經過 AppModel 給 Native 發送數據,會在該方法中收到
JS調用iOS的部分, 都只能在此處使用, 咱們也能夠注入多個名稱(JS對象), 用於區分功能

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "APPModel" {
            //傳遞的參數只支持NSNumber, NSString, NSDate, NSArray,NSDictionary, and NSNull類型
            let alert = UIAlertController(title: "MessageHandler", message: message.name, preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) -> Void in
                
            }))
            self.present(alert, animated: true, completion: nil)
        }
    }

JS 代碼:
function messageHandlers() {
        //APPModel 是咱們注入的對象
        window.webkit.messageHandlers.APPModel.postMessage({body: 'messageHandlers'});
}
注意: 
對象注入寫在 viewWillAppear 中, 防止循環引用

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        //注入對象名稱 APPModel, 當 JS 經過 APPModel 調用時, 能夠在 WKScriptMessageHandler 代理方法中接收到
        wkWebView.configuration.userContentController.add(self, name: "APPModel")
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        wkWebView.configuration.userContentController.removeScriptMessageHandler(forName: "APPModel")
          }

JS 經過 AppModel 給 Native 發送數據,會在該方法中收到
JS調用iOS的部分, 都只能在此處使用, 咱們也能夠注入多個名稱(JS對象), 用於區分功能

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "APPModel" {
            //傳遞的參數只支持NSNumber, NSString, NSDate, NSArray,NSDictionary, and NSNull類型
            let alert = UIAlertController(title: "MessageHandler", message: message.name, preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) -> Void in
                
            }))
            self.present(alert, animated: true, completion: nil)
        }
    }

JS 代碼:
function messageHandlers() {
        //APPModel 是咱們注入的對象
        window.webkit.messageHandlers.APPModel.postMessage({body: 'messageHandlers'});
}
複製代碼


六. JS 經過 Native 調用iOS 硬件(相機)

JS 調用 iOS 硬件, 本質上仍是經過以上介紹的 JS 調用 Native 方法調用 Native接口,

再由 Native 調用本地硬件, 具體實現看 demo , 這裏再也不贅述.



參考連接:


攔截 URL:

www.jianshu.com/p/d19689e0e…

blog.csdn.net/wanglei0918…

WKWebView 和 JS 交互:

github.com/marcuswesti…

www.cocoachina.com/ios/2017102…

blog.csdn.net/baihuaxiu12…

WebView 和 JS 交互:

www.jianshu.com/p/c11f9766f…

www.jianshu.com/p/8f3c47c24…

blog.csdn.net/longshihua/…


Github地址: 點擊打開連接

https://github.com/LeeJoey77/WebView_H5Demo.git複製代碼
複製代碼
https://github.com/LeeJoey77/WebView_H5Demo.gi複製代碼
相關文章
相關標籤/搜索