混合 APP 开发(Hybrid App)

目录

  • 混合 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 中是 webviewiOS UIWebview WKWebview),这个组件可以加载 Html 文件。

H5 大行其道之前,webview 加载的 web 页面很单调(因为只能加载一些静态资源),自从 H5 火了之后,前端猿们开发的 H5 页面在 webview 中的表现不俗使得 H5 开发慢慢渗透到了 Mobile App 开发中来。



3.Hybrid 现状


虽然目前已经出现了 RN Weex 这些使用 JS Native App 的技术,但是 Hybrid 仍然没有被淘汰,市面上大多数应用都不同程度的引入了 Web 页面。


三. UIWebView 和 WKWebView

做浏览器首先要选个好的基础。iOS8提供两类浏览组件:UIWebViewWKWebView

UIWebViewiOS传统的浏览控件,绝大多数浏览器都采用这个控件作为基础, ChromeFirefoxSafariUIWebView比较封闭,很多API都不开放,但却一度是唯一的选择。好处是,这个控件使用时间比较长,有很多方案可以参考。

WKWebView是苹果在iOS8 OS X Yosemite 中新推出的WebKit中的一个组件。

它代替了 UIKit 中的UIWebViewAppKit中的WebView,提供了统一的跨双平台 API支持HTML5的特性, 占用内存可能只有UIWebView1/3 ~ 1/4, 拥有 60fps 滚动刷新率、内置手势、高效的appweb信息交换通道、和Safari相同的JavaScript引擎, 增加了加载进度属性, UIWebView性能更加强大。

WKWebView也不是那么完美:如没有控制CookieAPI,  对读取本地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。

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');
}


模型注入(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'});
}


. JS 通过 Native 调用iOS 硬件(相机)

JS 调用 iOS 硬件, 本质上还是通过以上介绍的 JS 调用 Native 方法调用 Native接口,

再由 Native 调用本地硬件, 具体实现看 demo , 这里不再赘述.


参考链接:


拦截 URL:

https://www.jianshu.com/p/d19689e0ed83

https://blog.csdn.net/wanglei0918/article/details/78141890

WKWebView JS 交互:

https://github.com/marcuswestin/WebViewJavascriptBridge

http://www.cocoachina.com/ios/20171024/20895.html

https://blog.csdn.net/baihuaxiu123/article/details/51674726

WebView JS 交互:

https://www.jianshu.com/p/c11f9766f8d5

https://www.jianshu.com/p/8f3c47c24e29

https://blog.csdn.net/longshihua/article/details/51645575


Github地址: 点击打开链接

https://github.com/LeeJoey77/WebView_H5Demo.git
 
 
https://github.com/LeeJoey77/WebView_H5Demo.gi



猜你喜欢

转载自blog.csdn.net/leecsdn77/article/details/80534999