ObjC与JS交互的Native+WebView开发

文章出处,原创于 https://HawkingOuYang.github.io/

我的GitHub


Native + WebView

  • APICloud
  • 微信、QQ、支付宝

观点来源:
http://blog.csdn.net/skymingst/article/details/44968143

优点:

  • 最稳定的Hybrid App开发方式,交互层的效率上由Native的东西解决了,而且架构上基本就是在App内写网页,连App Store都是采用了该种方案;
  • 开发时分工非常明确,底层的由iOS开发人员处理,上层的由Web前端开发人员处理;
  • 有效的在线参数配置方式,以便于及时在线替换界面;

缺点:

  • 团队至少需要三个工程师,一个是Web的,一个是iOS、Android的
  • 运行效率的权衡权衡,多少界面采用Web来渲染,毕竟WebView的效率会相对降低

态度:

另参考:

JS与iOS Native Code互调的优雅实现方案 (开源库的详细实现过程)
http://blog.csdn.net/xunyn/article/details/8802584
或者
http://blog.csdn.net/yanghua_kobe/article/details/8209751


原材料:

WebViewJavascriptBridge 桥接ObjC与JS间的通信

NJKWebViewProgress 网页进度条

WebViewProxy 网页代理


第一部分:WebViewJavascriptBridge

GitHub上WebViewJavascriptBridge开源库的作者分别从ObjC和JS,来说怎么使用,即:Ta是分开来说。
https://github.com/marcuswestin/WebViewJavascriptBridge

我从ObjC与JS对应的相通点,来说怎么使用,即:我是一起来说。

WARNING: WKWebView still has many bugs and missing network APIs. It may not be a simple drop-in replacement.
WebViewJavascriptBridge supports WKWebView for iOS 8 and OSX Yosemite. In order to use WKWebView you need to instantiate the WKWebViewJavascriptBridge. The rest of the WKWebViewJavascriptBridge
API is the same as WebViewJavascriptBridge. 对于WKWebView,为了安全起见,使用iOS自带的API。以下说明 UIWebView如何应用WebViewJavascriptBridge。

关键点:

(一) WebViewJavascriptBridge.js.txt 注入web-view, 在JS那边创建bridge.

这里,WebViewJavascriptBridge开源库已经帮我们实现了,所以我们不用关心。
说明:WebViewJavascriptBridge requires WebViewJavascriptBridge.js.txt file that is injected into web view to create a bridge on JS side.

(二) 桥有两端,一端连接ObjC,另一端连接JS。怎么连接?

一端连接ObjC ——— 这里,在iOS端实现。

1
2
3
4
5
[WebViewJavascriptBridge bridgeForWebView:webView webViewDelegate:self
handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"Received message from javascript: %@", data);
responseCallback(@"Right back atcha");
}];

说明:Optionally, pass in webViewDelegate:(UIWebViewDelegate*)webViewDelegate if you need to respond to the web view’s lifecycle events.
The WVJBResponseCallback will not be nil if the javascript expects a response.

另一端连接JS ——— 这里,在web端实现代码,或者 在iOS端 把这个脚本注入webview.
怎么注入脚本?

对于WKWebView使用

1
2
3
[[WKUserScript alloc] initWithSource: self.jsString
injectionTime: WKUserScriptInjectionTimeAtDocumentEnd
forMainFrameOnly: YES];

对于UIWebView使用

1
[webView stringByEvaluatingJavaScriptFromString: self.jsString];

说明:创建了一个 connectWebViewJavascriptBridge 方法,该方法名是固定的, (iOS7 UIWebView) --- 这里,要么 在web端代码实现,要么 在iOS端注入脚本到web-view,总之,二选一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function connectWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
callback(WebViewJavascriptBridge)
} else {
document.addEventListener('WebViewJavascriptBridgeReady', function() {
callback(WebViewJavascriptBridge)
}, false)
}
}
connectWebViewJavascriptBridge(function(bridge) {
/* Init your app here */
bridge.init(function(message, responseCallback) {
alert('Received message: ' + message)
if (responseCallback) {
responseCallback("Right back atcha")
}
})
bridge.send('Hello from the javascript')
bridge.send('Please respond to this', function responseCallback(responseData) {
console.log("Javascript got its response", responseData)
})
})

(三) 桥,建立起来之后。ObjC 与 JS 通信。

ObjC 发消息给 JS:

1
2
[bridge send:(id)data]
[bridge send:(id)data responseCallback:(WVJBResponseCallback)responseCallback]

说明:Send a message to javascript. Optionally expect a response by giving a responseCallback block.

例如:

1
2
3
4
5
[self.bridge send:@"Hi"];
[self.bridge send:[NSDictionary dictionaryWithObject:@"Foo" forKey:@"Bar"]];
[self.bridge send:@"I expect a response!" responseCallback:^(id responseData) {
NSLog(@"Got response! %@", responseData);
}];

JS 发消息给 ObjC:

1
2
bridge.send({ Foo:"Bar" })
bridge.send(data, function responseCallback(responseData) { ... })

说明:Send a message to ObjC. Optionally expect a response by giving a responseCallback function.

例如:

1
2
3
4
bridge.send("Hi there!")
bridge.send("Hi there!", function(responseData) {
alert("I got a response! "+JSON.stringify(responseData))
})

(四) 桥,建立起来之后。ObjC 与 JS 调用对方代码。

ObjC调用 JS代码:
首先,JS准备好代码,怎么准备?如下:

1
bridge.registerHandler("handlerName", function(responseData) { ... })

说明:Register a handler called handlerName.

例如:

1
2
3
4
bridge.registerHandler("showAlert", function(data) { alert(data) })
bridge.registerHandler("getCurrentPageUrl", function(data, responseCallback) {
responseCallback(document.location.toString())
})

然后,ObjC调用 JS代码,怎么调用?如下:

1
2
[bridge callHandler:(NSString*)handlerName data:(id)data]
[bridge callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)callback]

说明:Call the javascript handler called handlerName. Optionally expect a response by giving a responseCallback block.

JS调用 ObjC代码:
首先,ObjC准备好代码,怎么准备?如下:

1
[bridge registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler]

说明:Register a handler called handlerName.
例如:

1
2
3
[self.bridge registerHandler:@"getScreenHeight" handler:^(id data, WVJBResponseCallback responseCallback) {
responseCallback([NSNumber numberWithInt:[UIScreen mainScreen].bounds.size.height]);
}];

然后,JS调用 ObjC代码,怎么调用?如下:

1
WebViewJavascriptBridge.callHandler("handlerName")


(五) Javascript API 特别说明:
1
document.addEventListener('WebViewJavascriptBridgeReady', function onBridgeReady(event) { ... }, false)

说明:Always wait for the WebViewJavascriptBridgeReady DOM event.

例如:

1
2
3
4
5
6
document.addEventListener('WebViewJavascriptBridgeReady', function(event) {
var bridge = event.bridge
// Start using the bridge
}, false)
bridge.init(function messageHandler(data, response) { ... })

说明:Initialize the bridge. This should be called inside of the ‘WebViewJavascriptBridgeReady’ event handler.

特别注意:The messageHandler function will receive all messages sent from ObjC via

1
[bridge send:(id)data] and [bridge send:(id)data responseCallback:(WVJBResponseCallback)responseCallback]

The response object will be defined if if ObjC sent the message with a WVJBResponseCallback block.
例如:

1
2
3
4
5
6
bridge.init(function(data, responseCallback) {
alert("Got data " + JSON.stringify(data))
if (responseCallback) {
responseCallback("Right back atcha!")
}
})

每次JS收到消息时,都会调用 messageHandler,
所以一般messageHandler不实现任何逻辑,基本就是打印消息。当然,特殊情况,另说。

同理,并且经过验证 可得出:ObjC 在实例化bridge时,即

1
2
3
4
[WebViewJavascriptBridge bridgeForWebView:webView webViewDelegate:self handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"Received message from javascript: %@", data);
responseCallback(@"Right back atcha");
}];

每次ObjC收到消息时, 都会调用handler,所以一般handler不实现任何逻辑,基本就是NSLog打印消息。当然,特殊情况,另说。


第二部分:NJKWebViewProgress

内置app浏览器(In-App browser),效果和safari进度条一样,详见:NJKWebViewProgress
NJKWebViewProgress is a progress interface library for UIWebView. Currently, UIWebView doesn’t have official progress interface. You can implement progress bar for your in-app browser using this module.
NJKWebViewProgress doesn’t use CocoaTouch’s private methods. It’s AppStore safe.


第三部分:参考资料

参考资料JS:

(一) JS事件监听
http://www.w3schools.com/jsref/met_document_addeventlistener.asp

HTML DOM addEventListener() Method
Example
Attach a click event to the document. When the user clicks anywhere in the document, output “Hello World” in a

element with id=”demo”:

1
2
3
document.addEventListener("click", function(){
    document.getElementById("demo").innerHTML = "Hello World";
});

Definition and Usage

The document.addEventListener() method attaches an event handler to the document.

Tip: Use the document.removeEventListener() method to remove an event handler that has been attached with the addEventListener() method.

Tip: Use the element.addEventListener() method to attach an event handler to a specified element.

Syntax
document.addEventListener(event, function, useCapture)

Parameter Values
Parameter
Description
event
Required. A String that specifies the name of the event.

Note: Do not use the “on” prefix. For example, use “click” instead of “onclick”.

For a list of all HTML DOM events, look at our complete HTML DOM Event Object Reference.
function
Required. Specifies the function to run when the event occurs.

When the event occurs, an event object is passed to the function as the first parameter. The type of the event object depends on the specified event. For example, the “click” event belongs to the MouseEvent object.
useCapture
Optional. A Boolean value that specifies whether the event should be executed in the capturing or in the bubbling phase.

Possible values:
• true - The event handler is executed in the capturing phase
• false- Default. The event handler is executed in the bubbling phase

Technical Details
DOM Version:
DOM Level 2 Events
Return Value:
No return value
Changelog:
The useCapture parameter became optional in Firefox 6 and Opera 11.60 (has always been optional for Chrome, IE and Safari)

More Examples
Example
You can also refer to an external “named” function:

1
2
3
4
document.addEventListener("click", myFunction);
function myFunction() {
    document.getElementById("demo").innerHTML = "Hello World";
}

参考资料JS-OC: 通过WKWebView和WebKit, JS与OC交互。

步骤1:
[JavaScript synchronous native communication to WKWebView]
(http://stackoverflow.com/questions/26851630/javascript-synchronous-native-communication-to-wkwebview)

步骤2:
[WKWebView evaluateJavascript return value]
(http://stackoverflow.com/questions/26778955/wkwebview-evaluatejavascript-return-value)

步骤3:
[WKWebView: trying to query javascript synchronously from the main thread]
(http://stackoverflow.com/questions/28388197/wkwebview-trying-to-query-javascript-synchronously-from-the-main-thread)

步骤4:
[How do I wait for an asynchronously dispatched block to finish?]
(http://stackoverflow.com/questions/4326350/how-do-i-wait-for-an-asynchronously-dispatched-block-to-finish/4326754#4326754)

步骤5:
[How to dispatch on main queue synchronously without a deadlock?]
(http://stackoverflow.com/questions/10330679/how-to-dispatch-on-main-queue-synchronously-without-a-deadlock)

参考资料: WKWebView

[Suppress WKWebView from scaling content to render at same magnification as UIWebView does]
(http://stackoverflow.com/questions/26102908/suppress-wkwebview-from-scaling-content-to-render-at-same-magnification-as-uiweb)

[How to determine the content size of a WKWebView?]
(http://stackoverflow.com/questions/27515236/how-to-determine-the-content-size-of-a-wkwebview)

[A Look at the WebKit Framework in iOS 8]
(http://www.appcoda.com/webkit-framework-tutorial/)

[WKWebView的使用心得]
(http://blog.csdn.net/qiang522235670/article/details/44857159)