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复制代码