目录
- 混合 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 转成为 AnyObjectNative 代码:// 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 转成为 AnyObjectNative 代码:// 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 = selfmodel.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 代码:(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 = selfmodel.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 代码: 复制代码
五. WKWebView 与 JS 交互
WKWebView 的配置//导入 WebKit//创建配置类let confirgure = WKWebViewConfiguration() //WKUserContentController: 内容交互控制器confirgure.userContentController = WKUserContentController() //创建WKWebViewwkWebView = WKWebView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height), configuration: confirgure) //配置代理wkWebView.navigationDelegate = self as WKNavigationDelegatewkWebView.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 回调给 JSNative 代码: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:
WKWebView 和 JS 交互:
WebView 和 JS 交互:
Github地址:
https://github.com/LeeJoey77/WebView_H5Demo.git复制代码
复制代码
https://github.com/LeeJoey77/WebView_H5Demo.gi复制代码