Hybrid Mobile App 能夠理解爲經過 Web 網絡技術(如 HTML,CSS 和 JavaScript)與 Native 相結合的混合移動應用程序。html
H5用於大致界面的編寫,如:須要一些基本的輸入框、單選按鈕、普通按鈕、以及下拉選擇框等。前端
CSS3則是主要用於對總體界面細節化的修飾。好比:一個普通按鈕,輸入框邊角默認是直角,那咱們能夠用CSS來改變其形狀。java
還能夠用來設置不一樣的樣式。ios
JS主要是要跟服務端打交道,實現數據交互。JS中的數據交互,主要以JSON格式跟XML格式這兩種格式實現。git
整體來講,H5+CSS3負責界面的搭建,JS負責數據的交互。github
下面簡述一下 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 頁面。
作瀏覽器首先要選個好的基礎。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文件的支持也很差等。
JavaScriptCore介紹
JavaScriptCore 這個庫是 Apple 在 iOS 7 以後加入到標準庫的,它對 iOS Native 與 JS 作交互調用產生了劃時代的影響。
JavaScriptCore 大致是由 4 個類以及 1 個協議組成的:
Native 調用 JS:
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 請求
用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 的配置
//導入 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
不一樣於 UIWebView,WKWebView 注入並執行 JS 的方法不會阻塞當前線程。由於考慮到 webview 加載的 web content 內 JS 代碼不必定通過驗證,若是阻塞線程可能會掛起 App。
self.wkWebView.evaluateJavaScript(「jsFuncName()") { (result, error) in print(result, error) } 注意: 方法不會阻塞線程,並且它的回調代碼塊老是在主線程中運行。注意: 方法不會阻塞線程,並且它的回調代碼塊老是在主線程中運行。複製代碼
JS 調用 Native
攔截 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'});
}
複製代碼
再由 Native 調用本地硬件, 具體實現看 demo , 這裏再也不贅述.
參考連接:
攔截 URL:
WKWebView 和 JS 交互:
www.cocoachina.com/ios/2017102…
WebView 和 JS 交互:
Github地址: 點擊打開連接
https://github.com/LeeJoey77/WebView_H5Demo.git複製代碼
複製代碼
https://github.com/LeeJoey77/WebView_H5Demo.gi複製代碼