混合 APP 开发(Hybrid App)

4,362 阅读11分钟

目录

  • 混合 App
  • Html5简介
  • UIWebView 和 WKWebView
  • UIWebView 和 JS 交互
  • WKWebView 和 JS 交互
  • JS 调用 Native 相机


一. 混合 APP

Hybrid Mobile App 可以理解为通过 Web 网络技术(如 HTML,CSS 和 JavaScript)与 Native 相结合的混合移动应用程序。

H5用于大体界面的编写,如:需要一些基本的输入框、单选按钮、普通按钮、以及下拉选择框等。

CSS3则是主要用于对整体界面细节化的修饰。比如:一个普通按钮,输入框边角默认是直角,那我们可以用CSS来改变其形状。

还可以用来设置不同的样式。

JS主要是要跟服务端打交道,实现数据交互。JS中的数据交互,主要以JSON格式跟XML格式这两种格式实现。

总体来说,H5+CSS3负责界面的搭建,JS负责数据的交互。


二. HTML5简介


下面简述一下 Hybrid 的发展史:


1.H5 发布


Html5 是在 2014 年 9 月份正式发布的,这一次的发布做了一个最大的改变就是“从以前的 XML 子集升级成为一个独立集合”。




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