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