native與Javascript的三種交互方式
1. native對(duì)Javascript執(zhí)行代碼注入
// 點(diǎn)擊圖片預(yù)覽
NSString * LJJSInjectClickImage(void) {
#define __wvjb_js_func__(x) #x
static NSString * JSCode = @__wvjb_js_func__(
function getImages() {
var objs = document.getElementsByTagName("img");
var imgScr = '';
for (var i = 0; i < objs.length; i++) {
imgScr = imgScr + objs[i].src + '+';
};
return imgScr;
};
function registerImagesClickAction() {
var imgs = document.getElementsByTagName('img');
var length = imgs.length;
for (var i = 0; i < length; i++) {
img = imgs[i];
img.onclick = function () {
window.location.href = 'lj-js-clickimage:' + this.src
}
}
});
#undef __wvjb_js_func__
return JSCode;
}
// 執(zhí)行注入
[webView stringByEvaluatingJavaScriptFromString:LJJSInjectClickImage()];
2. native調(diào)用Javascript
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[_webView evaluateJavaScript:@"getImages()" completionHandler:^(NSString *imageString, NSError * _Nullable error) {
NSLog(@"%@", imageString);
}];
}
3. Javascript調(diào)用native
- 1: 攔截URL
WebViewJavascriptBridge乃沙,Cordova阻塑,EasyJSWebView都是基于攔截URL的原理來實(shí)現(xiàn)抑进。 - 2: JavaScriptCore
iOS7之后蘋果推出JavaScriptCore框架抱冷,從而讓web頁面和本地原生應(yīng)用炮温,交互起來非常方便巴碗,而且使用此框架可以做Android那邊和iOS相對(duì)統(tǒng)一,web前端寫一套代碼就可以適配客戶端的兩個(gè)平臺(tái),從而減少了web前端的工作量议蟆,但只適用于UIWebView。 - 3: MessageHandler
iOS8以后出現(xiàn)萎战,web前端需要對(duì)iOS Android不同處理咐容,不允許跨域,無法發(fā)送POST參數(shù)蚂维。只適用于WKWebView戳粒。
基于攔截URL實(shí)現(xiàn)的插件化JSBridge SDK
以下是一個(gè)設(shè)備信息插件的聲明文件路狮,前端工程師可通過引入該聲明文件獲取設(shè)備信息插件的相關(guān)能力。
當(dāng)收到plusready
事件名稱時(shí)蔚约,代表插件初始化代碼已經(jīng)成功注入奄妨,此時(shí)注冊該插件。
/* ************************ 設(shè)備信息插件 ************************ */
document.addEventListener("plusready",
function registerPluginFunction() {
var _pluginName = 'DeviceInfoPlugin',
p = window.plus;
p[_pluginName] = {
appVersion: function (param, callback) {
p.requestNative(_pluginName, "appVersion", param, callback)
},
uniqueId: function (param, callback) {
p.requestNative(_pluginName, "uniqueId", param, callback)
}
};
document.removeEventListener("plusready", registerPluginFunction, true);
},
true);
調(diào)用插件代碼:
<html>
<head>
<script type="text/javascript" src="DeviceInfoPlugin.js"></script>
<script>
function appVersion(){
window.plus.DeviceInfoPlugin.appVersion(null, function (responseData) {
alert(JSON.stringify(responseData));
})
}
</script>
</head>
<body>
<img src="eg.jpg" width=100 height=100 /><br/>
<img src="zq.jpg" width=100 height=100 /><br/>
<input type="button" id="enter32" value="appVersion" onclick="appVersion();" /><br/>
</body>
</html>
可以看到對(duì)插件的調(diào)用最終都會(huì)觸發(fā) window.plus.requestNative(插件名苹祟,方法名砸抛,參數(shù),回調(diào))
树枫。
window.plus.requestNative(...)
的實(shí)現(xiàn)是在native加載webView時(shí)注入到WebView中直焙。
注入的關(guān)鍵代碼如下:
(function(){
if(window.plus){
return;
}
window.plus = {};
// messageMap存儲(chǔ)callbackId與回調(diào)的對(duì)應(yīng)關(guān)系
var messageMap = new Map();
var uniqueId = 1;
window.LJJSBridge = {
requestNative : function(scheme, plugin, func, param, callback) {
var message = {};
message.plugin = plugin;
message.param = param;
if (!!callback) {
var callbackId ='cb_'+ (uniqueId++) + '_' + new Date().getTime();
message.callbackId = callbackId;
message.callback = callback;
messageMap.set(callbackId,message);
}
window.LJJSBridge.openNativeURL(scheme,plugin,func,JSON.stringify(message));
},
openNativeURL : function (scheme, plugin, func, args) {
var formattedArgs = (args.length > 0 ? encodeURIComponent(args):"");
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", scheme + ":" + plugin + ":" + encodeURIComponent(func) +":" + formattedArgs);
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
},
_handleMessageFromNative : function(nativeMessage) {
setTimeout(function() {
var message;
if(nativeMessage.callbackId){
message = messageMap.get(nativeMessage.callbackId);
console.log(nativeMessage.responseData);
if (!message||(!message.responseCallback)) {
return;
}
message.responseCallback(nativeMessage.responseData);
messageMap.delete(message.callbackId);
}
},0)
},
}
})();
window.plus.requestNative
帶上scheme
轉(zhuǎn)換為window.LJJSBridge.requestNative
茄蚯,并在結(jié)尾發(fā)送plusready
事件具被。
(function(){
if (window.plus.requestNative) {
return;
}
function requestNative(plugin,func,param,callback) {
window.LJJSBridge.requestNative("lj-js-plugin",plugin, func, param,callback);
}
var plus = {
requestNative:requestNative,
};
window.plus = plus;
var readyEvent = document.createEvent('Events');
readyEvent.initEvent('plusready');
readyEvent.bridge = plus;
document.dispatchEvent(readyEvent);
})();
js調(diào)用native的方法執(zhí)行順序如下。
window.LJJSBridge.openNativeURL()
內(nèi)將方法調(diào)用轉(zhuǎn)換為URL跳轉(zhuǎn)顽分,對(duì)參數(shù)進(jìn)行URI編碼舔清。在native進(jìn)行URL攔截丝里。
window.plus.DeviceInfoPlugin.appVersion()
window.plus.requestNative()
window.LJJSBridge.requestNative()
window.LJJSBridge.openNativeURL()
iOS native對(duì)URL進(jìn)行攔截:
// 插件Bridge處理
@property (nonatomic, strong) LJPluginsJSBridge *pluginsBridge;
// URL攔截
NSURL *url = [navigationAction.request URL];
NSString *urlString = [url absoluteString];
NSArray *components = [urlString componentsSeparatedByString:@":"];
NSString *scheme = components[0];
if ([scheme isEqualToString:[@"lj-js-plugin" copy]] && [components count] > 3) {
NSString *pluginName = components[1];
NSString *funcName = components[2];
NSString *argsString = [components[3] length] ? [components[3] stringByRemovingPercentEncoding] : nil;
[self.pluginsBridge execPlugWithPlugName:pluginName Function:funcName Message:argsString];
}
LJPluginsJSBridge負(fù)責(zé)對(duì)單個(gè)webView中所有的插件進(jìn)行管理曲初,插件采用反射+懶加載方式創(chuàng)建体谒。
/**
Js調(diào)用native方法
@param plugName 插件名稱
@param functionName native方法名
@param msg 屬性json
*/
- (void)execPlugWithPlugName:(NSString *)plugName Function:(NSString *)functionName Message:(NSString *)msg
{
if (plugName == nil) {
return;
}
// 從插件列表中找到該插件
NSMutableArray<LJBaseJSPlugin *> *plugsAr = [self plugsAr];
__block LJBaseJSPlugin *plug = nil;
[plugsAr enumerateObjectsUsingBlock:^(LJBaseJSPlugin * tempPlug, NSUInteger idx, BOOL *stop) {
if ([tempPlug.name isEqualToString:plugName]) {
plug = tempPlug;
*stop = YES;
}
}];
// 創(chuàng)建插件并執(zhí)行插件方法
void (^createAndExecPlugBlock)(void) = ^() {
LJBaseJSPlugin *baseJSPlugin = [self createJSPluginWithPlugName:plugName];
if (baseJSPlugin != nil) {
// 找到并創(chuàng)建該插件,將該插件加入插件列表
[plugsAr addObject:baseJSPlugin];
// 執(zhí)行該插件的native方法
[self callFunctionWithObj:baseJSPlugin functionName:functionName message:[LJBridgeUtil convertStringToMessage:msg]];
}
};
if (plug == nil) {
// 未找到該插件臼婆,創(chuàng)建該插件
createAndExecPlugBlock();
}
else {
// 找到該插件
if (plug.webView != nil && plug.vc != nil) {
// 執(zhí)行該插件的native方法
[self callFunctionWithObj:plug functionName:function message:[LJBridgeUtil convertStringToMessage:msg]];
}
else {
// 若插件不可用抒痒,則移除該插件并重新創(chuàng)建它
[plugsAr removeObject:plug];
createAndExecPlugBlock();
}
}
}
使用反射創(chuàng)建插件:
/**
創(chuàng)建插件
@param plugNameStr 插件名稱
*/
- (LJBaseJSPlugin *)createJSPluginWithPlugName:(NSString *)plugName
{
// 創(chuàng)建該插件
// Class plugClass = [[LJAbilityConfig sharedInstance] classOfPlugName:plugName];
Class plugClass = NSClassFromString(plugName);
if (plugClass == nil) {
return nil;
}
LJBaseJSPlugin *baseJSPlugin = nil;
if (plugClass != nil) {
baseJSPlugin = [[plugClass alloc] init];
baseJSPlugin.name = plugName;
baseJSPlugin.nativeCalled = NO;
baseJSPlugin.webView = self.webView;
baseJSPlugin.vc = self.vc;
}
return baseJSPlugin;
}
如下url被分解為四部分:
lj-js-plugin:DeviceInfoPlugin:appVersion:%7B%22plugin%22%3A%22DeviceInfoPlugin%22%2C%22param%22%3Anull%2C%22callbackId%22%3A%22cb_1_1631588855957%22%7D
- scheme:lj-js-plugin,
- plugin:DeviceInfoPlugin,
- function:appVersion,
- args:%7B%22plugin%22%3A%22DeviceInfoPlugin%22%2C%22param%22%3Anull%2C%22callbackId%22%3A%22cb_1_1631588855957%22%7D
在屬于webView的插件列表中查找DeviceInfoPlugin插件,若未找到則動(dòng)態(tài)創(chuàng)建它颁褂。
對(duì)DeviceInfoPlugin插件調(diào)用appVersion方法故响。
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
NSString *ocFunctionName = [NSString stringWithFormat:@"%@:",functionName];
if ([obj respondsToSelector:NSSelectorFromString(ocFunctionName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
dispatch_main_async_safe(^{
[obj performSelector:NSSelectorFromString(ocFunctionName) withObject:msg];
})
#pragma clang diagnostic pop
}
設(shè)備信息插件實(shí)現(xiàn)如下:
@interface DeviceInfoPlugin : LJBaseJSPlugin
- (void)appVersion:(LJMessage *)msg;
- (void)uniqueId:(LJMessage *)msg;
@end
@implementation DeviceInfoPlugin
- (void)appVersion:(LJMessage *)msg
{
NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary];
NSString *versionStr = [infoDict objectForKey:@"CFBundleShortVersionString"];
NSString *buildStr = [infoDict objectForKey:@"CFBundleVersion"];
msg.responseDic = @{
@"version":ACNotNilStr(versionStr),
@"build":ACNotNilStr(buildStr),
};
[self respondJSWithMsg:msg];
}
@end
基礎(chǔ)插件實(shí)現(xiàn)如下:
nativeCalled屬性提供插件給native調(diào)用的能力,默認(rèn)值為true颁独。插件不僅支持js調(diào)用彩届,也支持本地native調(diào)用。
/**
插件基類(公開).
*/
@interface LJBaseJSPlugin : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, weak) id webView;
@property (nonatomic, weak) UIViewController *vc;
// 是否是native調(diào)用誓酒,默認(rèn)yes
@property (nonatomic, assign, getter=isNativeCalled) BOOL nativeCalled;
- (void)respondJSWithMsg:(LJMessage *)resMsg;
@end
@implementation LJBaseJSPlugin
- (id)init
{
self = [super init];
if (self) {
_nativeCalled = YES;
}
return self;
}
- (void)respondJSWithMsg:(LJMessage *)resMsg
{
if (!resMsg) {
return;
}
// 回調(diào)native
if (self.isNativeCalled) {
if (resMsg.nativeResponseBlock) {
resMsg.nativeResponseBlock(resMsg);
}
}
else {
if (self.webView) {
// native回調(diào)JS樟蠕,回傳返回值
[LJBridgeUtil webView:self.webView callJSWithMessage:resMsg];
}
}
}
@end
callJSWithMessage()
負(fù)責(zé)native回調(diào)js,將version信息回傳靠柑。
// js處理native調(diào)用
static const NSString *kLJJSHandleMessageFormat = @"window.LJJSBridge._handleMessageFromNative(%@);";
+ (void)webView:(id)webView callJSWithMessage:(LJMessage *)msg
{
NSString *messageStr = [msg json];
NSString *formatStr = [kLJJSHandleMessageFormat copy];
NSString *jsStr = [NSString stringWithFormat:formatStr,messageStr];
[self webView:webView callJsWithString:jsStr];
}
_handleMessageFromNative
通過callbackId找到Map中的message寨辩,并執(zhí)行回調(diào)message.callback(nativeMessage.responseData);
_handleMessageFromNative : function(nativeMessage) {
setTimeout(function() {
var message;
if(nativeMessage.callbackId){
message = messageMap.get(nativeMessage.callbackId);
console.log(nativeMessage.responseData);
if (!message||(!message.callback)) {
return;
}
message.callback(nativeMessage.responseData);
messageMap.delete(message.callbackId);
}
},0)
},
成功執(zhí)行回調(diào)。