Hybrid Mobile App 可以理解为通过 Web 网络技术(如 HTML,CSS 和 JavaScript)与 Native 相结合的混合移动应用程序。
H5用于大体界面的编写,如:需要一些基本的输入框、单选按钮、普通按钮、以及下拉选择框等。
CSS3则是主要用于对整体界面细节化的修饰。比如:一个普通按钮,输入框边角默认是直角,那我们可以用CSS来改变其形状。
还可以用来设置不同的样式。
JS主要是要跟服务端打交道,实现数据交互。JS中的数据交互,主要以JSON格式跟XML格式这两种格式实现。
总体来说,H5+CSS3负责界面的搭建,JS负责数据的交互。
下面简述一下 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 页面。
做浏览器首先要选个好的基础。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 代码:
(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 代码:
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:
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