最近整理了一下原生與H5之間的交互方式,簡單的做個總結(jié)。
OC端與JS的交互膝擂,大致有這幾種:攔截協(xié)議、JavaScriptCore庫隙弛、WKWebView架馋、自定義NSURLProtocol攔截、WebViewJavascriptBridge全闷。
- JavaScriptCore一個iOS7引進的標(biāo)準(zhǔn)庫叉寂,iOS7以前也有開發(fā)者自行導(dǎo)入使用。Web端也比較容易統(tǒng)一总珠。
- WebViewJavascriptBridge是一個第三方庫屏鳍,其原理還是使用了對web view的請求攔截,支持WKWebview局服。封裝了完整的OC與JS相互調(diào)用的方法钓瞭,不過需要Web端配合編寫相應(yīng)的方法。安卓方面聽說有一個同名的庫淫奔,如果不是統(tǒng)一使用的話山涡,需要Web端寫2套JS的話,那就有點蛋疼了搏讶。
- WKWebView,iOS8加入的WebKit佳鳖。相對于UIWebView,具有更強大的功能媒惕。提供一個
WKScriptMessageHandler
,可以實現(xiàn)JS對WebView的調(diào)用系吩。 - 協(xié)議的攔截,比較常用的一種方式妒蔚。
- 在自定義NSURLProtocol中穿挨,攔截請求,也可以實現(xiàn)相應(yīng)的方法調(diào)用肴盏。
1. JavaScriptCore
JavaScriptCore中類及協(xié)議:
- JSContext:給JavaScript提供運行的上下文環(huán)境,通過-evaluateScript:方法就可以執(zhí)行一JS代碼
- JSValue:封裝了JS與ObjC中的對應(yīng)的類型科盛,以及調(diào)用JS的API等
- JSManagedValue:管理數(shù)據(jù)和方法的類
- JSVirtualMachine:處理線程相關(guān),使用較少
- JSExport:這是一個協(xié)議菜皂,如果采用協(xié)議的方法交互贞绵,自己定義的協(xié)議必須遵守此協(xié)議,在協(xié)議中聲明的API都會在JS中暴露出來恍飘,才能調(diào)用
對于JSContext和JSValue的更多使用方式可以看下這篇榨崩,介紹的比較完整ios7 JavaScriptCore.framework 本文主要簡單總結(jié)下交互相關(guān)內(nèi)容谴垫。
ObjC調(diào)用JS
在JavaScriptCore中提供的調(diào)用JS的方法
- (JSValue *)evaluateScript:(NSString *)script;
方法就可以執(zhí)行一段JavaScript腳本,并且如果其中有方法母蛛、變量等信息都會被存儲在其中以便在需要的時候使用翩剪。
JSValue提供了- (JSValue *)callWithArguments:(NSArray *)arguments;
方法可以反過來將參數(shù)傳進去來調(diào)用方法
// 一個JSContext對象,就類似于Js中的window彩郊,
// 只需要創(chuàng)建一次即可前弯。
JSContext *context = [[JSContext alloc] init];
// 執(zhí)行一段js
[context evaluateScript:@"function add(a, b) { return a + b; }"];
// 根據(jù)下標(biāo)取出方法
JSValue *add = context[@"add"];
NSLog(@"Func: %@", add);
// 傳入?yún)?shù) 調(diào)用取到的方法
JSValue *sum = [add callWithArguments:@[@(7), @(21)]];
NSLog(@"Sum: %d",[sum toInt32]);
//OutPut:
// Func: function add(a, b) { return a + b; }
// Sum: 28
再來個栗子
self.jsContext = [[JSContext alloc] init];
[self.jsContext evaluateScript:@"var num = 10"];
[self.jsContext evaluateScript:@"var squareFunc = function(value) { return value * 2 }"];
// 調(diào)用 計算面積
JSValue *square = [self.jsContext evaluateScript:@"squareFunc(num)"];
// 可以通過下標(biāo)的方式獲取到方法
JSValue *squareFunc = self.jsContext[@"squareFunc"];
// 傳入?yún)?shù) 調(diào)用取到的方法
JSValue *value = [squareFunc callWithArguments:@[@"20"]];
NSLog(@"%@", square.toNumber);
NSLog(@"%@", value.toNumber);
JS調(diào)用OC
使用JavaScriptCore進行原生與js的交互主要是2種方式,block和注入模型使用協(xié)議代理秫逝。
Block方式
JSContext *context = [[JSContext alloc] init];
// 定義一個block
context[@"log"] = ^() {
NSLog(@"+++++++Begin Log+++++++");
NSArray *args = [JSContext currentArguments];
for (JSValue *jsVal in args) {
NSLog(@"%@", jsVal);
}
JSValue *this = [JSContext currentThis];
NSLog(@"this: %@",this);
NSLog(@"-------End Log-------");
};
// 調(diào)用js執(zhí)行l(wèi)og方法
[context evaluateScript:@"log('ider', [7, 21],
{ hello:'world', js:100 });"];
當(dāng)web端調(diào)用log方法恕出,傳入相關(guān)參數(shù),就能調(diào)用OC端的block筷登。實現(xiàn)交互剃根。
通過注入模型的方式交互
-
OC端要做的事情
首先,我們自定義一個協(xié)議,而且這個協(xié)議必須要遵守JSExport協(xié)議
協(xié)議暴露的方法廉油,是供JS調(diào)用的方法惠险。還可以實現(xiàn)回調(diào)。
@protocol JavaScriptObjectiveCDelegate <JSExport>
// JS調(diào)用此方法來調(diào)用OC的share
- (void)share:(NSDictionary *)params ;
// JS調(diào)用此方法來調(diào)用OC的相機
- (void)callCamera ;
// 在JS中調(diào)用時抒线,多個參數(shù)需要使用駝峰方式
// 這里是多個個參數(shù)的班巩。
- (void)showAlert:(NSString *)title msg:(NSString *)msg;
// 通過JSON傳過來
- (void)callWithDict:(NSDictionary *)params;
// JS調(diào)用Oc,然后在OC中通過調(diào)用JS方法來傳值給JS嘶炭。
- (void)jsCallObjcAndObjcCallJsWithDict:(NSDictionary *)params;
@end
接下來抱慌,我們還需要定義一個模型:
// 此模型用于注入JS的模型,這樣就可以通過模型來調(diào)用方法眨猎。
@interface HYBJsObjCModel : NSObject <JavaScriptObjectiveCDelegate>
@property (nonatomic, weak) JSContext *jsContext;
@property (nonatomic, weak) UIWebView *webView;
@end
模型的實現(xiàn):
@implementation HYBJsObjCModel
- (void)share:(NSDictionary *)params {
NSLog(@"Js調(diào)用了OC的share方法抑进,參數(shù)為:%@", params);
}
- (void)callWithDict:(NSDictionary *)params {
NSLog(@"Js調(diào)用了OC的方法,參數(shù)為:%@", params);
}
// JS調(diào)用了callCamera
- (void)callCamera {
NSLog(@"JS調(diào)用了OC的方法睡陪,調(diào)起系統(tǒng)相冊");
// JS調(diào)用后OC后寺渗,可以傳一個回調(diào)方法的參數(shù),進行回調(diào)JS
JSValue *jsFunc = self.jsContext[@"jsFunc"];
[jsFunc callWithArguments:nil];
}
- (void)jsCallObjcAndObjcCallJsWithDict:(NSDictionary *)params {
NSLog(@"jsCallObjcAndObjcCallJsWithDict was called, params is %@", params);
// 調(diào)用JS的方法
JSValue *jsParamFunc = self.jsContext[@"jsParamFunc"];
[jsParamFunc callWithArguments:@[@{@"age": @10, @"name": @"lili", @"height": @158}]];
}
// 指定參數(shù)的用法
// 在JS中調(diào)用時兰迫,函數(shù)名應(yīng)該為showAlertMsg(arg1, arg2)
- (void)showAlert:(NSString *)title msg:(NSString *)msg {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *a = [[UIAlertView alloc] initWithTitle:title message:msg delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];
[a show];
});
}
@end
JavaScriptCore使用注意
JavaScript調(diào)用本地方法是在子線程中執(zhí)行的信殊,這里要根據(jù)實際情況考慮線程之間的切換。
模型實現(xiàn)完了汁果,在哪里注入呢涡拘。在controller的webView加載完成后
我們是通過webView的valueForKeyPath獲取的,其路徑為documentView.webView.mainFrame.javaScriptContext
据德。
這樣就可以獲取到JS的context鳄乏,然后為這個context注入我們的模型對象跷车。
#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 通過模型調(diào)用方法,這種方式更好些汞窗。
HYBJsObjCModel *model = [[HYBJsObjCModel alloc] init];
// 模型
self.jsContext[@"OCModel"] = model;
model.jsContext = self.jsContext;
model.webView = self.webView;
// 增加異常的處理
self.jsContext.exceptionHandler = ^(JSContext *context,
JSValue *exceptionValue) {
context.exception = exceptionValue;
NSLog(@"異常信息:%@", exceptionValue);
};
}
另外關(guān)于模型姓赤,也可根據(jù)需求直接將模型作為controller,去實現(xiàn)相關(guān)的方法實現(xiàn)仲吏,省去模型
這一層不铆。 如下:
#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
self.jsContext[@"OCModel"] = self;
self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
context.exception = exceptionValue;
NSLog(@"異常信息:%@", exceptionValue);
};
}
-
WEB端
代碼內(nèi)容
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
</head>
<body>
<div style="margin-top: 100px">
<h1>Objective-C和JavaScript交互的那些事</h1>
<input type="button" value="CallCamera" onclick="OCModel.callCamera()">
</div>
<div>
<input type="button" value="Share" onclick="callShare()">
</div>
<script>
var callShare = function() {
OCModel.share({'title': '標(biāo)題', 'desc': '內(nèi)容', 'shareUrl': 'http://www.reibang.com/p/f896d73c670a');
}
</script>
</body>
</html>
注意下 如果調(diào)用的方法是多個參數(shù)的,必須使用駝峰寫法并去掉冒號
- (void)showAlert:(NSString *)title msg:(NSString *)msg;
需要這樣調(diào)用
OCModel. showAlertMsg('title','msg');
以上就是簡單的利用JavaScriptCore framework進行JS交互的用法裹唆,
感謝
iOS與JS交互實戰(zhàn)篇(ObjC)
Objective-C與JavaScript交互的那些事
提供的資料參考誓斥,僅僅是做個總結(jié)
2. WebViewJavascriptBridge
這個第三方庫起先是在UIWebView與JS的深度交互大神文中知悉。其還是使用攔截WebView請求方法许帐,但是做了完整的封裝后劳坑,使用起來還是很簡單的。
1) 導(dǎo)入
#import "WKWebViewJavascriptBridge.h"
2) 初始化
self.bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
// 開啟日志成畦,方便調(diào)試
[WebViewJavascriptBridge enableLogging];
3) Web端setupWebViewJavascriptBridge
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
4)call setupWebViewJavascriptBridge
setupWebViewJavascriptBridge(function(bridge) {
/* Initialize your app here */
bridge.registerHandler('JS Echo', function(data, responseCallback) {
console.log("JS Echo called with:", data)
responseCallback(data)
})
bridge.callHandler('ObjC Echo', function responseCallback(responseData) {
console.log("JS received response:", responseData)
})
})
-
ObjC API
OC端初始化時 默認(rèn)消息處理器
實例化WebViewJavascriptBridge并定義native端的默認(rèn)消息處理器距芬。
JS調(diào)用bridge.send()即可觸發(fā)默認(rèn)處理。
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView handler:^(id data, WVJBResponse *response) {
NSLog(@"ObjC received message from JS: %@", data);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"ObjC got message from Javascript:" message:data delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}];
OC端調(diào)用[self.bridge send];
即可觸發(fā)JS端的默認(rèn)處理循帐。
[self.bridge send:@"Give me a response, will you?" responseCallback:^(id responseData) {
NSLog(@"ObjC got its response! %@", responseData);
}];
OC端registerHandler接收J(rèn)S調(diào)用
在JS中調(diào)用了bridge.callHandler('getScreenHeight')
就會觸發(fā)OC注冊的對應(yīng)的handler框仔,responseCallback中回調(diào)JS傳遞參數(shù)
[self.bridge registerHandler:@"getScreenHeight" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"ObjC Echo called with: %@", data);
responseCallback([NSNumber numberWithInt:[UIScreen
mainScreen].bounds.size.height]);
}];
或者 JS傳遞data給OC,OC打印
[self.bridge registerHandler:@"log" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"Log: %@", data);
}];
OC端callHandler調(diào)用JS
調(diào)用JS showAlert
拄养,傳遞data
[self.bridge callHandler:@"showAlert" data:@"Hi from ObjC to JS!"];
調(diào)用JS getCurrentPageUrl
离斩,在block中獲取參數(shù)
[self.bridge callHandler:@"getCurrentPageUrl" data:nil responseCallback:^(id responseData) {
NSLog(@"Current UIWebView page URL is: %@", responseData);
}];
還可設(shè)置代理監(jiān)聽
[bridge setWebViewDelegate:(UIWebViewDelegate*)webViewDelegate];
-
Javascript API
JS registerHandler接收OC調(diào)用
注冊handle,OC可以通過[bridge callHandler:"handlerName" data:@"Foo"]
和 [bridge callHandler:"handlerName" data:@"Foo" responseCallback:^(id responseData) { ... }]
進行調(diào)用JS
OC傳遞data進行調(diào)用
bridge.registerHandler("showAlert", function(data) { alert(data) })
參數(shù)結(jié)果回傳給OC
bridge.registerHandler("getCurrentPageUrl", function(data, responseCallback) {
responseCallback(document.location.toString())
})
bridge.callHandler("handlerName", data)
JS 調(diào)用OC
JS調(diào)用bridge.callHandler("handlerName", data)
和bridge.callHandler("handlerName", data, function responseCallback(responseData) { ... })
調(diào)用OC端打印
bridge.callHandler("Log", "Foo")
調(diào)用OC端獲取高度瘪匿,在block中使用
bridge.callHandler("getScreenHeight", null, function(response) {
alert('Screen height:' + response)
})
3.WKWebView - iOS8 or Later
iOS8跛梗,蘋果新推出了WebKit,用WKWebView代替UIWebView和WebView棋弥。相關(guān)的使用和特性可以細(xì)讀核偿。
WKWeb?View
iOS 8 WebKit框架概覽(下)
WKWebView特性及使用
-
WKWebView新特性
性能、穩(wěn)定性嘁锯、功能大幅度提升
允許JavaScript的Nitro庫加載并使用(UIWebView中限制)
支持了更多的HTML5特性
高達60fps的滾動刷新率以及內(nèi)置手勢
GPU硬件加速
KVO
重構(gòu)UIWebView成14類與3個協(xié)議宪祥,查看官方文檔
需要注意的是WKWebView貌似不支持NSURLProtocol和NSURLCache。不能做緩存的話家乘,就蛋疼了蝗羊。
關(guān)于WKWebView的代理方法 這篇有比較完整的介紹
http://www.reibang.com/p/1d7a8525ad16
下面是相關(guān)的交互方法
-
app調(diào)js方法
WKWebView調(diào)用js方法和UIWebView類似,一個是evaluateJavaScript
仁锯,一個是stringByEvaluatingJavaScriptFromString
耀找。獲取返回值的方式不同,WKWebView用的是回叫函數(shù)獲取返回值
//直接調(diào)用js
webView.evaluateJavaScript("hi()", completionHandler: nil)
//調(diào)用js帶參數(shù)
webView.evaluateJavaScript("hello('liuyanwei')", completionHandler: nil)
// 調(diào)用js獲取返回值
webView.evaluateJavaScript("getName()") { (any,error) -> Void in
NSLog("%@", any as! String)
}
-
js調(diào)app方法
UIwebView沒有js調(diào)app的方法,而在WKWebView中有了改進野芒。具體步驟分為app注冊handler蓄愁,app處理handler委托,js調(diào)用三個步驟
- 注冊handler需要在webView初始化之前狞悲,如示例撮抓,注冊了一個webViewApp的handler
config = WKWebViewConfiguration()
//注冊js方法
config.userContentController.addScriptMessageHandler(self, name: "webViewApp")
// 初始化
webView = WKWebView(frame: self.webWrap.frame, configuration: config)
- 處理handler委托。ViewController實現(xiàn)WKScriptMessageHandler委托的
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage)
代理方法摇锋。在里面處理事件丹拯。
//實現(xiàn)WKScriptMessageHandler委托
class ViewController:WKScriptMessageHandler
//實現(xiàn)js調(diào)用ios的handle委托
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
//接受傳過來的消息從而決定app調(diào)用的方法
let dict = message.body as! Dictionary<String,String>
let method:String = dict["method"]!
let param1:String = dict["param1"]!
if method=="hello"{
hello(param1)
}
}
-
js調(diào)用 。通過
window.webkit.messageHandlers.webViewApp
找到之前注冊的handler對象荸恕,然后調(diào)用postMessage方法把數(shù)據(jù)傳到app乖酬,app通過上一步的方法解析方法名和參數(shù)。webViewApp
是之前注冊的namevar message = { 'method' : 'hello', 'param1' : 'liuyanwei', }; window.webkit.messageHandlers.webViewApp.postMessage(message);
如果需要app對js的調(diào)用有所響應(yīng)融求,可以通過回叫函數(shù)的方式回應(yīng)js咬像。可以在調(diào)用app的時候增加一個js回叫函數(shù)名
,app在處理完之后可以呼叫回叫函數(shù)并把需要的參數(shù)通過回叫函數(shù)的方式進行傳遞
-
使用用戶腳本來注入 JavaScript
WKUserScript 允許在正文加載之前或之后注入到頁面中生宛。這個強大的功能允許在頁面中以安全且唯一的方式操作網(wǎng)頁內(nèi)容县昂。
一個簡單的例子如下,用戶改變背景的用戶腳本被插入到網(wǎng)頁中:
let source = "document.body.style.background = \"#777\";"
let userScript = WKUserScript(source: source, injectionTime: .AtDocumentEnd, forMainFrameOnly: true)
let userContentController = WKUserContentController()
userContentController.addUserScript(userScript)
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
self.webView = WKWebView(frame: self.view.bounds, configuration: configuration)
WKUserScript 對象可以以 JavaScript 源碼形式初始化陷舅,初始化時還可以傳入是在加載之前還是結(jié)束時注入七芭,以及腳本影響的是這個布局還是僅主要布局。于是用戶腳本被加入到 WKUserContentController 中蔑赘,并且以 WKWebViewConfiguration 屬性傳入到 WKWebView 的初始化過程中。
4. 攔截協(xié)議
最簡單也是最容易想到的一種
UIWebView的代理方法预明,web view發(fā)出請求后攔截缩赛,查看是否為約定的協(xié)議,采取處理撰糠。
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSString *url = request.URL.absoluteString;
if ([url rangeOfString:@"camera://"].location != NSNotFound) {
// url的協(xié)議頭是camera
NSLog(@"callCamera");
return NO;
}
return YES;
}
WKWebView
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
NSString *url = navigationAction.request.URL.absoluteString;
NSLog(@"%@",url);
if (navigationAction.navigationType == WKNavigationTypeLinkActivated && [url rangeOfString:@"camera://"].location != NSNotFound)
{
// url的協(xié)議頭是camera
NSLog(@"callCamera");
decisionHandler(WKNavigationActionPolicyCancel);
// dosomthing酥馍。。阅酪。
}
else
{
decisionHandler(WKNavigationActionPolicyAllow);
}
}
5. NSURLProtocol攔截
這種方式也是最近才看到旨袒,原本利用自定義NSURLProtocol來做緩存處理。相關(guān)的文章可以看:
NSURLProtocol和NSRunLoop的那些坑
iOS中的 NSURLProtocol
在自定義的Protocol的- (void)startLoading
方法中术辐,可以攔截到請求砚尽。一般會在這里做緩存的判斷與讀取處理。在此處辉词,也可以判斷約定的協(xié)議必孤,然后發(fā)送通知,客戶端就可以接收到通知瑞躺,執(zhí)行相應(yīng)的方法敷搪。
- (void)startLoading
{
NSString * url = [[[self request] URL] absoluteString];
if([url hasPrefix:@"LocalActions/"])
{
NSString * actname = [url stringByReplacingOccurrencesOfString:@"LocalActions/" withString:@"LocalAction_"];
// 發(fā)送通知 客戶端就可執(zhí)行方法
[[NSNotificationCenter defaultCenter] postNotificationName:actname object:nil];
}
}
需要注意的是WKWebView貌似不支持NSURLProtocol和NSURLCache兴想。不能做緩存的話,就蛋疼了赡勘。
相關(guān)參考
iOS與JS交互實戰(zhàn)篇(ObjC版)
Objective-C與JavaScript交互的那些事