博客搬迁
2020.1.3
iOS UIWebView逐步被淘汰, WKWebView成为支流. 本文封装了WKJSWebView
(代码见第三节),参考EasyJSWebView
的交互方式, 对其进行了批改和减少. 能够实现原生调用JS, 也能够JS调用原生。
一. 应用办法
JS调原生
- 创立一个交互类, 定义给js的交互接口
// OC#import <Foundation/Foundation.h>#import "WKJSWebView.h"@interface JSInterface : NSObject- (void)testWithParams:(NSString*)_params callback:(WKJSDataFunction*)_callback;@end#import "JSInterface.h"#import "MJExtension.h"@implementation JSInterface- (void)testWithParams:(NSString*)_params callback:(WKJSDataFunction*)_callback{ //接管h5 参数 NSLog(@"H5 调 native, 参数 : %@", _params); NSString *letter = [NSString stringWithFormat:@"%C", (unichar)(arc4random_uniform(26) + 'A')]; NSDictionary* p1 = @{@"letter": letter, @"b": @"bb", @"c": @"cc"}; NSString* p2 = @"param_p2"; NSString* p3 = @"param_p3"; NSArray* nativeParams = @[p1, p2, p3]; //执行h5回调函数 [_callback executeWithParams:nativeParams completionHandler:^(id response, NSError *error) { NSLog(@"completionHandler"); }];}@end
- 初始化webView
// OC- (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor lightGrayColor]; CGRect rect = CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height-150); self.webView = [[WKJSWebView alloc] initWithFrame:rect configuration:[WKWebViewConfiguration new] scripts:nil withJavascriptInterfaces:@{@"native":[JSInterface new]}]; self.webView.navigationDelegate = self; [self.view addSubview:self.webView]; NSString* _urlStr = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"]; NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:_urlStr]]; [self.webView loadRequest:request];}
- JS调原生接口
<script> // js window.native.testWithParamscallback('abc', (p1, p2, p3) => { console.log(p1, p2, p3); var obj1 = JSON.parse(p1); let div = document.getElementById("op"); div.innerHTML = obj1.letter; });</script>
原生调JS
- js注册办法
<script> // js function changeColor(param) { let div = document.getElementById("oi"); div.style.backgroundColor = param.color; }; window.EasyJS.mount("divChangeColor", changeColor);</script>
- 原生调用JS
// OCNSDictionary* args = @{@"color": [self Ox_randomColor]};[self.webView invokeJSFunction:@"divChangeColor" params:args completionHandler:^(id response, NSError *error) { NSLog(@"原生调用JS办法实现.");}];
二. 原理解析
根本思维就是将须要交互的接口挂载到浏览器的window上, 而后通过js代码调用.
原生将js代码编译成字符串, 再通过上面的办法执行js:
// OC- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
js调原生
注入桥接js
首先看上面的js代码:
// js!function () { if (window.EasyJS) { return; } window.EasyJS = { /** * 寄存JS的回调函数 */ __callbacks: {}, /** * 寄存JS注册给native的办法 */ __events: {}, /** * JS执行此办法,将JS函数挂载到__events供原生调用 * @param {String} funcName js办法名 * @param {Function} handler js办法 */ mount: function (funcName, handler) { EasyJS.__events[funcName] = handler; }, /** * 原生执行此办法 调用JS函数 * @param {String} funcID js办法名 * @param {JSON} paramsJson 参数 */ invokeJS: function (funcID, paramsJson) { let handler = EasyJS.__events[funcID]; if (handler && typeof (handler) === 'function') { let args = ''; try { if (typeof JSON.parse(paramsJson) == 'object') { args = JSON.parse(paramsJson); } else { args = paramsJson; } return handler(args); } catch (error) { console.log(error); args = paramsJson; return handler(args); } } else { console.log(funcID + '函数未定义'); } }, /** * native通过此办法执行JS回调函数 * @param {String} cbID 函数ID * @param {Boolean} removeAfterExecute 执行后是否从__callbacks中否移除此回调函数 */ invokeCallback: function (cbID, removeAfterExecute) { let args = Array.prototype.slice.call(arguments); args.shift(); // __cb1577786915804 args.shift(); // false for (let i = 0, l = args.length; i < l; i++) { args[i] = decodeURIComponent(args[i]); } let cb = EasyJS.__callbacks[cbID]; if (removeAfterExecute) { EasyJS.__callbacks[cbID] = undefined; } return cb.apply(null, args); }, /** * 调用原生obj对象的办法 * @param {String} obj * @param {String} functionName * @param {Array} args */ call: function (obj, functionName, args) { let formattedArgs = []; for (let i = 0, l = args.length; i < l; i++) { if (typeof args[i] == 'function') { formattedArgs.push('f'); let cbID = '__cb' + (+new Date) + Math.random(); EasyJS.__callbacks[cbID] = args[i]; formattedArgs.push(cbID); } else { formattedArgs.push('s'); formattedArgs.push(encodeURIComponent(args[i])); } } let argStr = (formattedArgs.length > 0 ? ':' + encodeURIComponent(formattedArgs.join(':')) : ''); /** NativeListener 要与原生中addScriptMessageHandler的name保持一致 */ window.webkit.messageHandlers.NativeListener.postMessage(obj + ':' + encodeURIComponent(functionName) + argStr); let ret = EasyJS.retValue; EasyJS.retValue = undefined; if (ret) { return decodeURIComponent(ret); } }, /** * native用来给window增加obj的对象与办法 * @param {String} obj 增加到window上的对象 * @param {Array<String>} methods 增加到obj上的办法数组 */ inject: function (obj, methods) { window[obj] = {}; let jsObj = window[obj]; for (let i = 0, l = methods.length; i < l; i++) { (function () { let method = methods[i]; let jsMethod = method.replace(new RegExp(':', 'g'), ''); jsObj[jsMethod] = function () { return EasyJS.call(obj, method, Array.prototype.slice.call(arguments)); }; })(); } } };}()
这段js在webView初始化时注入到浏览器, 在window上减少一个EasyJS对象, 为交互搭建桥梁.
// OC//EASY_JS_INJECT_STRING是下面的js代码串[configuration.userContentController addUserScript:[[WKUserScript alloc] initWithSource:EASY_JS_INJECT_STRING injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]];
注入原生交互办法
而后, 持续注入原生交互类:
// OC// interfaces : @{@"native":[JSInterface new]} NSMutableString* injectString = [[NSMutableString alloc] init]; for(NSString *key in [interfaces allKeys]) { [injectString appendString:@"EasyJS.inject(\""]; [injectString appendString:key]; [injectString appendString:@"\", ["]; NSObject* interfaceObj = [interfaces objectForKey:key]; if ([interfaceObj isKindOfClass:[NSObject class]]) { Class cls = object_getClass(interfaceObj); while (cls != [NSObject class]) { unsigned int mc = 0; Method * mlist = class_copyMethodList(cls, &mc); for (int i = 0; i < mc; i++) { [injectString appendString:@"\""]; [injectString appendString:[NSString stringWithUTF8String:sel_getName(method_getName(mlist[i]))]]; [injectString appendString:@"\""]; if ((i != mc - 1) || (cls.superclass != [NSObject class])) { [injectString appendString:@", "]; } } free(mlist); cls = cls.superclass; } } [injectString appendString:@"]);"]; //@"EasyJS.inject(\"native\", [\"testWithParams:callback:\"]);" }#ifdef DEBUG NSLog(@"injectString :\n%@", injectString);#endif [configuration.userContentController addUserScript:[[WKUserScript alloc] initWithSource:injectString injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]];
下面代码调用了EasyJS.inject()
办法:
// jsinject: function (obj, methods) { window[obj] = {}; let jsObj = window[obj]; for (let i = 0, l = methods.length; i < l; i++) { (function () { let method = methods[i]; let jsMethod = method.replace(new RegExp(':', 'g'), ''); jsObj[jsMethod] = function () { return EasyJS.call(obj, method, Array.prototype.slice.call(arguments)); }; })(); }}
在window减少native对象,并且把JSInterface的交互办法都加到native对象.这里的native相当于JSInterface在h5中的镜像, 通过native,就能够调用原生办法:
// jswindow.native.testWithParamscallback('abc', (p1, p2, p3) => { // h5回调函数});
发送音讯给原生
然而, native.testWithParamscallback长这样的:
// jsfunction() { return EasyJS.call(obj, method, Array.prototype.slice.call(arguments));};
这是镜像native的testWithParamscallback办法, 它并不能换起原生, 真正调用原生的是EasyJS.call()
.
// jscall: function (obj, functionName, args) { let formattedArgs = []; for (let i = 0, l = args.length; i < l; i++) { if (typeof args[i] == 'function') { formattedArgs.push('f'); let cbID = '__cb' + (+new Date) + Math.random(); EasyJS.__callbacks[cbID] = args[i]; formattedArgs.push(cbID); } else { formattedArgs.push('s'); formattedArgs.push(encodeURIComponent(args[i])); } } let argStr = (formattedArgs.length > 0 ? ':' + encodeURIComponent(formattedArgs.join(':')) : ''); /** NativeListener 要与原生中addScriptMessageHandler的name保持一致 */ window.webkit.messageHandlers.NativeListener.postMessage(obj + ':' + encodeURIComponent(functionName) + argStr); let ret = EasyJS.retValue; EasyJS.retValue = undefined; if (ret) { return decodeURIComponent(ret); }}
Easy.call()
将js的回调函数生成惟一ID对应保留到EasyJS.__callbacks
,再将惟一ID和参数按约定的形式编译放入数组 ,而后用原生约定的监听名字NativeListener发送音讯给原生window.webkit.messageHandlers.NativeListener.postMessage(obj + ':' + encodeURIComponent(functionName) + argStr)
;
NativeListener在初始化webView时指定, 同时将原生交互类映射interfaces挂载到监听者.
// OC// add message handlerWKJSListener *listener = [[WKJSListener alloc] init];listener.javascriptInterfaces = interfaces;[configuration.userContentController addScriptMessageHandler:listener name:WKJSMessageHandler];
原生接管音讯并执行
js收回音讯后, 原生的监听WKJSListener能够接管到:
// OC- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { NSMutableArray <WKJSDataFunction *>* _funcs = [NSMutableArray new]; NSMutableArray <NSString *>* _args = [NSMutableArray new]; if ([message.name isEqualToString:WKJSMessageHandler]) { __weak WKJSWebView *webView = (WKJSWebView *)message.webView; NSString *requestString = [message body]; // native:testWithParams%3Acallback%3A:s%3Aabc%3Af%3A__cb1577786915804 NSArray *components = [requestString componentsSeparatedByString:@":"]; //NSLog(@"req: %@", requestString); NSString* obj = (NSString*)[components objectAtIndex:0]; NSString* method = [(NSString*)[components objectAtIndex:1] stringByRemovingPercentEncoding]; NSObject* interface = [self.javascriptInterfaces objectForKey:obj]; SEL selector = NSSelectorFromString(method); NSMethodSignature* sig = [interface methodSignatureForSelector:selector]; if (sig.numberOfArguments == 2 && components.count > 2) { NSString *assertDesc = [NSString stringWithFormat:@"*** -[%@ %@]: %@",NSStringFromClass([interface class]),method,@"理论接管参数个数与js传参数不相等"]; assertDesc = assertDesc ? : @""; NSAssert(NO, assertDesc); return; } if (!sig) { NSString *assertDesc = [NSString stringWithFormat:@"*** -[%@ %@]:%@",NSStringFromClass([interface class]),method,@"method signature argument cannot be nil"]; NSAssert(NO, assertDesc); return; } if (![interface respondsToSelector:selector]) { NSAssert(NO, @"该办法未实现"); return; } NSInvocation* invoker = [NSInvocation invocationWithMethodSignature:sig]; invoker.selector = selector; invoker.target = interface; if ([components count] > 2){ NSString *argsAsString = [(NSString*)[components objectAtIndex:2] stringByRemovingPercentEncoding]; NSArray* formattedArgs = [argsAsString componentsSeparatedByString:@":"]; if ((sig.numberOfArguments - 2) != [formattedArgs count] / 2) { NSString *assertDesc = [NSString stringWithFormat:@"*** -[%@ %@]: 理论接管参数个数%@,js传参个数%@",NSStringFromClass([interface class]),method,@(sig.numberOfArguments - 2),@([formattedArgs count] / 2)]; assertDesc = assertDesc ? : @""; NSAssert(NO, assertDesc); return; } for (unsigned long i = 0, j = 0, l = [formattedArgs count]; i < l; i+=2, j++){ NSString* type = ((NSString*) [formattedArgs objectAtIndex:i]); NSString* argStr = ((NSString*) [formattedArgs objectAtIndex:i + 1]); if ([@"f" isEqualToString:type]){ WKJSDataFunction *func = [[WKJSDataFunction alloc] initWithWebView:webView]; func.funcID = argStr; [_funcs addObject:func]; [invoker setArgument:&func atIndex:(j + 2)]; }else if ([@"s" isEqualToString:type]){ NSString* arg = [argStr stringByRemovingPercentEncoding]; [_args addObject:arg]; [invoker setArgument:&arg atIndex:(j + 2)]; } } } [invoker retainArguments]; [invoker invoke]; if ([sig methodReturnLength] > 0){ __unsafe_unretained NSString* tmpRetValue; [invoker getReturnValue:&tmpRetValue]; NSString *retValue = tmpRetValue; if (retValue == NULL || retValue == nil){ [webView wk_evaluateJavaScript:@"EasyJS.retValue=null;" completionHandler:nil]; }else{ retValue = [retValue stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet letterCharacterSet]]; retValue = [@"" stringByAppendingFormat:@"EasyJS.retValue=\"%@\";", retValue]; [webView wk_evaluateJavaScript:retValue completionHandler:nil]; } } } [_funcs removeAllObjects]; [_args removeAllObjects];}
在这里取出对象,办法,参数, 通过javascriptInterfaces
映射取原生对象(也就是JSInterface),而后执行办法.
执行js回调
原生办法执行后回调js:
// OC[_callback executeWithParams:nativeParams completionHandler:^(id response, NSError *error) {}];
executeWithParams:completionHandler:
办法如下:
// OC- (void)executeWithParams:(NSArray *)params completionHandler:(void (^)(id response, NSError *error))completionHandler { NSMutableArray * args = [NSMutableArray arrayWithArray:params]; for (int i=0; i<params.count; i++) { NSString* json = [params[i] mj_JSONString]; [args replaceObjectAtIndex:i withObject:json]; } NSMutableString* injection = [[NSMutableString alloc] init]; [injection appendFormat:@"EasyJS.invokeCallback(\"%@\", %@", self.funcID, self.removeAfterExecute ? @"true" : @"false"]; if (args) { for (unsigned long i = 0, l = args.count; i < l; i++){ NSString* arg = [args objectAtIndex:i]; NSCharacterSet *chars = [NSCharacterSet characterSetWithCharactersInString:@"!*'();:@&=+$,/?%#[]"]; NSString *encodedArg = [arg stringByAddingPercentEncodingWithAllowedCharacters:chars]; [injection appendFormat:@", \"%@\"", encodedArg]; } } [injection appendString:@");"]; if (_webView){ [_webView wk_evaluateJavaScript:injection completionHandler:^(id response, NSError *error) { if (completionHandler) {completionHandler(response, error);} }]; }}
通过EasyJS.invokeCallback()
传入回调函数惟一ID, 取出__callbacks
中对应的办法并执行:
// jsinvokeCallback: function (cbID, removeAfterExecute) { let args = Array.prototype.slice.call(arguments); args.shift(); // __cb1577786915804 args.shift(); // false for (let i = 0, l = args.length; i < l; i++) { args[i] = decodeURIComponent(args[i]); } let cb = EasyJS.__callbacks[cbID]; if (removeAfterExecute) { EasyJS.__callbacks[cbID] = undefined; } return cb.apply(null, args);},
args.shift()
移除多余的参数.
至此, js调原生流程完结.
原生调js
js注册函数
原生调用js, 须要js将办法注册到window, 注入js中提供了mount()
办法给js注册函数用:
// jsmount: function (funcName, handler) { EasyJS.__events[funcName] = handler;},
mount()
办法将JS函数handler寄存到__events, 以便提供给原生调用.
js中注册也很简略:
// jswindow.EasyJS.mount("divChangeColor", changeColor);
这样就将divChangeColor
函数注册了, 它对应js中的changeColor()
办法:
// jsfunction changeColor(param) { let div = document.getElementById("oi"); div.style.backgroundColor = param.color;};
原生调js
原生调用js函数divChangeColor
:
// OC[self.webView invokeJSFunction:@"divChangeColor" params:@{@"color": [self Ox_randomColor]} completionHandler:^(id response, NSError *error) { NSLog(@"原生调用JS办法实现.");}];
invokeJSFunction:params:completionHandler:
办法如下:
// OC- (void)invokeJSFunction:(NSString*)jsFuncName params:(id)params completionHandler:(void (^)(id response, NSError *error))completionHandler { NSString *paramJson = @""; if (params) { paramJson = [params mj_JSONString]; } paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"]; paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"]; paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"]; paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"]; NSString *script = [NSString stringWithFormat:@"%@('%@', '%@')", @"window.EasyJS.invokeJS", jsFuncName, paramJson]; [self wk_evaluateJavaScript:script completionHandler:completionHandler];}
通过EasyJS.invokeJS()
,取出__events
中对应divChangeColor
的函数并执行.
// jsinvokeJS: function (funcID, paramsJson) { let handler = EasyJS.__events[funcID]; if (handler && typeof (handler) === 'function') { let args = ''; try { if (typeof JSON.parse(paramsJson) == 'object') { args = JSON.parse(paramsJson); } else { args = paramsJson; } return handler(args); } catch (error) { console.log(error); args = paramsJson; return handler(args); } } else { console.log(funcID + '函数未定义'); }}
至此, 原生调用js实现.
三. WKJSWebView代码
WKJSWebView.h
#import <WebKit/WebKit.h>#import <Foundation/Foundation.h>#pragma mark - WKJSWebView@interface WKJSWebView : WKWebView- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration*)configuration scripts:(NSArray<NSString*>*)scripts withJavascriptInterfaces:(NSDictionary*)interfaces;/// 主线程执行js- (void)wk_evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler;/// native 调用 h5 办法- (void)invokeJSFunction:(NSString*)jsFuncName params:(id)params completionHandler:(void (^)(id response, NSError *error))completionHandler;@end#pragma mark - WKJSListener@interface WKJSListener : NSObject<WKNavigationDelegate,WKScriptMessageHandler>@property (nonatomic) NSDictionary *javascriptInterfaces;@end#pragma mark - WKJSDataFunction@interface WKJSDataFunction : NSObject@property (nonatomic, copy) NSString* funcID;@property (nonatomic, strong) WKJSWebView *webView;@property (nonatomic, assign) BOOL removeAfterExecute;- (instancetype)initWithWebView:(WKJSWebView*)webView;// 回调JS- (void)execute:(void (^)(id response, NSError* error))completionHandler;- (void)executeWithParam:(NSString *)param completionHandler:(void (^)(id response, NSError* error))completionHandler;- (void)executeWithParams:(NSArray *)params completionHandler:(void (^)(id response, NSError* error))completionHandler;@end
WKJSWebView.m
#import "WKJSWebView.h"#import <objc/runtime.h>#import "MJExtension.h"static NSString * const EASY_JS_INJECT_STRING = @"!function () {\ if (window.EasyJS) {\ return;\ }\ window.EasyJS = {\ __callbacks: {},\ __events: {},\ mount: function (funcName, handler) {\ EasyJS.__events[funcName] = handler;\ },\ invokeJS: function (funcID, paramsJson) {\ let handler = EasyJS.__events[funcID];\ if (handler && typeof (handler) === 'function') {\ let args = '';\ try {\ if (typeof JSON.parse(paramsJson) == 'object') {\ args = JSON.parse(paramsJson);\ } else {\ args = paramsJson;\ }\ return handler(args);\ } catch (error) {\ console.log(error);\ args = paramsJson;\ return handler(args);\ }\ } else {\ console.log(funcID + '函数未定义');\ }\ },\ invokeCallback: function (cbID, removeAfterExecute) {\ let args = Array.prototype.slice.call(arguments);\ args.shift();\ args.shift();\ for (let i = 0, l = args.length; i < l; i++) {\ args[i] = decodeURIComponent(args[i]);\ }\ let cb = EasyJS.__callbacks[cbID];\ if (removeAfterExecute) {\ EasyJS.__callbacks[cbID] = undefined;\ }\ return cb.apply(null, args);\ },\ call: function (obj, functionName, args) {\ let formattedArgs = [];\ for (let i = 0, l = args.length; i < l; i++) {\ if (typeof args[i] == 'function') {\ formattedArgs.push('f');\ let cbID = '__cb' + (+new Date) + Math.random();\ EasyJS.__callbacks[cbID] = args[i];\ formattedArgs.push(cbID);\ } else {\ formattedArgs.push('s');\ formattedArgs.push(encodeURIComponent(args[i]));\ }\ }\ let argStr = (formattedArgs.length > 0 ? ':' + encodeURIComponent(formattedArgs.join(':')) : '');\ window.webkit.messageHandlers.NativeListener.postMessage(obj + ':' + encodeURIComponent(functionName) + argStr);\ let ret = EasyJS.retValue;\ EasyJS.retValue = undefined;\ if (ret) {\ return decodeURIComponent(ret);\ }\ },\ inject: function (obj, methods) {\ window[obj] = {};\ let jsObj = window[obj];\ for (let i = 0, l = methods.length; i < l; i++) {\ (function () {\ let method = methods[i];\ let jsMethod = method.replace(new RegExp(':', 'g'), '');\ jsObj[jsMethod] = function () {\ return EasyJS.call(obj, method, Array.prototype.slice.call(arguments));\ };\ })();\ }\ }\ };\}()";static NSString * const WKJSMessageHandler = @"NativeListener";#pragma mark - WKJSWebView@implementation WKJSWebView/** 初始化WKWwebView,并将交互类的办法注入JS */- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration*)configuration scripts:(NSArray<NSString*>*)scripts withJavascriptInterfaces:(NSDictionary*)interfaces{ if (!configuration) { configuration = [[WKWebViewConfiguration alloc] init]; } if (!configuration.userContentController) { configuration.userContentController = [[WKUserContentController alloc] init]; } // add script for (NSString* script in scripts) { [configuration.userContentController addUserScript:[[WKUserScript alloc] initWithSource:script injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]]; } [configuration.userContentController addUserScript:[[WKUserScript alloc] initWithSource:EASY_JS_INJECT_STRING injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]]; NSMutableString* injectString = [[NSMutableString alloc] init]; for(NSString *key in [interfaces allKeys]) { [injectString appendString:@"EasyJS.inject(\""]; [injectString appendString:key]; [injectString appendString:@"\", ["]; NSObject* interfaceObj = [interfaces objectForKey:key]; if ([interfaceObj isKindOfClass:[NSObject class]]) { Class cls = object_getClass(interfaceObj); while (cls != [NSObject class]) { unsigned int mc = 0; Method * mlist = class_copyMethodList(cls, &mc); for (int i = 0; i < mc; i++) { [injectString appendString:@"\""]; [injectString appendString:[NSString stringWithUTF8String:sel_getName(method_getName(mlist[i]))]]; [injectString appendString:@"\""]; if ((i != mc - 1) || (cls.superclass != [NSObject class])) { [injectString appendString:@", "]; } } free(mlist); cls = cls.superclass; } } [injectString appendString:@"]);"]; //@"EasyJS.inject(\"native\", [\"testWithParams:callback:\"]);" }#ifdef DEBUG NSLog(@"injectString :\n%@", injectString);#endif [configuration.userContentController addUserScript:[[WKUserScript alloc] initWithSource:injectString injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]]; // add message handler WKJSListener *listener = [[WKJSListener alloc] init]; listener.javascriptInterfaces = interfaces; [configuration.userContentController addScriptMessageHandler:listener name:WKJSMessageHandler]; // init self = [super initWithFrame:frame configuration:configuration]; return self;}- (void)wk_evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler { if (![NSThread isMainThread]) { dispatch_async(dispatch_get_main_queue(), ^{ [self evaluateJavaScript:javaScriptString completionHandler:^(id _Nullable response, NSError * _Nullable error) { if (completionHandler) {completionHandler(response, error);} }]; }); } else { [self evaluateJavaScript:javaScriptString completionHandler:^(id _Nullable response, NSError * _Nullable error) { if (completionHandler) {completionHandler(response, error);} }]; }}- (void)invokeJSFunction:(NSString*)jsFuncName params:(id)params completionHandler:(void (^)(id response, NSError *error))completionHandler { NSString *paramJson = @""; if (params) { paramJson = [params mj_JSONString]; } paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"]; paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"]; paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"]; paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"]; paramJson = [paramJson stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"]; NSString *script = [NSString stringWithFormat:@"%@('%@', '%@')", @"window.EasyJS.invokeJS", jsFuncName, paramJson]; [self wk_evaluateJavaScript:script completionHandler:completionHandler];}@end#pragma mark - WKJSListener@implementation WKJSListener- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { NSMutableArray <WKJSDataFunction *>* _funcs = [NSMutableArray new]; NSMutableArray <NSString *>* _args = [NSMutableArray new]; if ([message.name isEqualToString:WKJSMessageHandler]) { __weak WKJSWebView *webView = (WKJSWebView *)message.webView; NSString *requestString = [message body]; // native:testWithParams%3Acallback%3A:s%3Aabc%3Af%3A__cb1577786915804 NSArray *components = [requestString componentsSeparatedByString:@":"]; //NSLog(@"req: %@", requestString); NSString* obj = (NSString*)[components objectAtIndex:0]; NSString* method = [(NSString*)[components objectAtIndex:1] stringByRemovingPercentEncoding]; NSObject* interface = [self.javascriptInterfaces objectForKey:obj]; // execute the interfacing method SEL selector = NSSelectorFromString(method); NSMethodSignature* sig = [interface methodSignatureForSelector:selector]; if (sig.numberOfArguments == 2 && components.count > 2) { // 办法签名获取到理论实现的办法无参数 && js调用的办法带参数 NSString *assertDesc = [NSString stringWithFormat:@"*** -[%@ %@]: %@",NSStringFromClass([interface class]),method,@"oc的交互办法不带参数,然而js调用的办法传了参数"]; // 因为pod报正告,所以加上这句,理论没有意义 assertDesc = assertDesc ? : @""; NSAssert(NO, assertDesc); return; } if (!sig) { NSString *assertDesc = [NSString stringWithFormat:@"*** -[%@ %@]:%@",NSStringFromClass([interface class]),method,@"method signature argument cannot be nil"]; NSAssert(NO, assertDesc); return; } if (![interface respondsToSelector:selector]) { NSAssert(NO, @"该办法未实现"); return; } NSInvocation* invoker = [NSInvocation invocationWithMethodSignature:sig]; invoker.selector = selector; invoker.target = interface; if ([components count] > 2){ NSString *argsAsString = [(NSString*)[components objectAtIndex:2] stringByRemovingPercentEncoding]; NSArray* formattedArgs = [argsAsString componentsSeparatedByString:@":"]; if ((sig.numberOfArguments - 2) != [formattedArgs count] / 2) { // 办法签名获取到理论实现的办法的参数个数 != js调用办法时传参个数 NSString *assertDesc = [NSString stringWithFormat:@"*** -[%@ %@]: oc的交互办法参数个数%@,js调用办法时传参个数%@",NSStringFromClass([interface class]),method,@(sig.numberOfArguments - 2),@([formattedArgs count] / 2)]; // 因为pod报正告,所以加上这句,理论没有意义 assertDesc = assertDesc ? : @""; NSAssert(NO, assertDesc); return; } for (unsigned long i = 0, j = 0, l = [formattedArgs count]; i < l; i+=2, j++){ NSString* type = ((NSString*) [formattedArgs objectAtIndex:i]); NSString* argStr = ((NSString*) [formattedArgs objectAtIndex:i + 1]); if ([@"f" isEqualToString:type]){ WKJSDataFunction *func = [[WKJSDataFunction alloc] initWithWebView:webView]; func.funcID = argStr; //do this to force retain a reference to it [_funcs addObject:func]; [invoker setArgument:&func atIndex:(j + 2)]; }else if ([@"s" isEqualToString:type]){ NSString* arg = [argStr stringByRemovingPercentEncoding]; //do this to force retain a reference to it [_args addObject:arg]; [invoker setArgument:&arg atIndex:(j + 2)]; } } } [invoker retainArguments]; [invoker invoke]; //return the value by using javascript if ([sig methodReturnLength] > 0){ __unsafe_unretained NSString* tmpRetValue; [invoker getReturnValue:&tmpRetValue]; NSString *retValue = tmpRetValue; if (retValue == NULL || retValue == nil){ [webView wk_evaluateJavaScript:@"EasyJS.retValue=null;" completionHandler:nil]; }else{ retValue = [retValue stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet letterCharacterSet]]; retValue = [@"" stringByAppendingFormat:@"EasyJS.retValue=\"%@\";", retValue]; [webView wk_evaluateJavaScript:retValue completionHandler:nil]; } } } //clean up any retained funcs [_funcs removeAllObjects]; //clean up any retained args [_args removeAllObjects];}@end#pragma mark - WKJSDataFunction@implementation WKJSDataFunction- (instancetype)initWithWebView:(WKJSWebView *)webView { self = [super init]; if (self) { _webView = webView; } return self;}- (void)execute:(void (^)(id response, NSError *error))completionHandler { [self executeWithParam:nil completionHandler:^(id response, NSError *error) { if (completionHandler) { completionHandler(response, error); } }];}- (void)executeWithParam:(NSString *)param completionHandler:(void (^)(id response, NSError *error))completionHandler { [self executeWithParams:param ? @[param] : nil completionHandler:^(id response, NSError *error) { if (completionHandler) { completionHandler(response, error); } }];}- (void)executeWithParams:(NSArray *)params completionHandler:(void (^)(id response, NSError *error))completionHandler { NSMutableArray * args = [NSMutableArray arrayWithArray:params]; for (int i=0; i<params.count; i++) { NSString* json = [params[i] mj_JSONString]; [args replaceObjectAtIndex:i withObject:json]; } NSMutableString* injection = [[NSMutableString alloc] init]; [injection appendFormat:@"EasyJS.invokeCallback(\"%@\", %@", self.funcID, self.removeAfterExecute ? @"true" : @"false"]; if (args) { for (unsigned long i = 0, l = args.count; i < l; i++){ NSString* arg = [args objectAtIndex:i]; NSCharacterSet *chars = [NSCharacterSet characterSetWithCharactersInString:@"!*'();:@&=+$,/?%#[]"]; NSString *encodedArg = [arg stringByAddingPercentEncodingWithAllowedCharacters:chars]; [injection appendFormat:@", \"%@\"", encodedArg]; } } [injection appendString:@");"]; if (_webView){ [_webView wk_evaluateJavaScript:injection completionHandler:^(id response, NSError *error) { if (completionHandler) {completionHandler(response, error);} }]; }}@end
四. demo代码
iOS
//// ViewController.m// TMEasyJSWebView//// Created by 吉久东 on 2019/8/13.// Copyright © 2019 JIJIUDONG. All rights reserved.//#import "ViewController.h"#import "WKJSWebView.h"#import "JSInterface.h"#import "MJExtension.h"@interface ViewController ()<WKNavigationDelegate>@property (nonatomic, strong) WKJSWebView *webView;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor lightGrayColor]; CGRect rect = CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height-150); self.webView = [[WKJSWebView alloc] initWithFrame:rect configuration:[WKWebViewConfiguration new] scripts:nil withJavascriptInterfaces:@{@"native":[JSInterface new]}]; self.webView.navigationDelegate = self; [self.view addSubview:self.webView]; NSString* _urlStr = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"]; NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:_urlStr]]; [self.webView loadRequest:request];}- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; UILabel* l = [UILabel new]; l.text = @"这里灰色局部是原生界面"; l.frame = CGRectMake(5, self.view.bounds.size.height - 150, 310, 20); [self.view addSubview:l]; UIButton * b = [UIButton buttonWithType:UIButtonTypeCustom]; b.backgroundColor = [UIColor yellowColor]; [b setTitle:@"黄色是原生按钮" forState:UIControlStateNormal]; [b setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; [b setTitleColor:[UIColor redColor] forState:UIControlStateHighlighted]; [b addTarget:self action:@selector(nativeButtonClicked) forControlEvents:UIControlEventTouchUpInside]; b.frame = CGRectMake(5, self.view.bounds.size.height-100, 310, 50); [self.view addSubview:b];}- (void)nativeButtonClicked { NSLog(@"点击了原生按钮"); [self.webView invokeJSFunction:@"divChangeColor" params:@{@"color": [self Ox_randomColor]} completionHandler:^(id response, NSError *error) { NSLog(@"原生调用JS办法实现."); }];}- (NSMutableString*)Ox_randomColor { NSMutableString* color = [[NSMutableString alloc] initWithString:@"#"]; NSArray * STRING = @[@"0",@"1",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"A",@"B",@"C",@"D",@"E",@"F"]; for (int i=0; i<6; i++) { NSInteger index = arc4random_uniform((uint32_t)STRING.count); NSString *c = [STRING objectAtIndex:index]; [color appendString:c]; } return color;}@end
h5
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> .a { width: 300px; height: 80px; font-size: 32px; text-align: center; line-height: 80px; margin-bottom: 10px; } </style> <script> function getCharacter() { window.native.testWithParamscallback('abc', (p1, p2, p3) => { console.log(p1, p2, p3); var obj1 = JSON.parse(p1); let div = document.getElementById("op"); div.innerHTML = obj1.letter; }); }; function changeColor(param) { let div = document.getElementById("oi"); div.style.backgroundColor = param.color; }; window.EasyJS.mount("divChangeColor", changeColor); </script></head><body> <p>这里是 h5 web 页面</p> <p>1.点击上面按钮,调用原生办法获取随机字母并显示到h5</p> <div id="op" class="a" style="background-color: pink;" onclick="getCharacter()"></div> <p>2.原生调用h5办法,扭转该元素背景色</p> <div id="oi" class="a" style="background-color: aqua;" onclick="changeColor()"></div></body></html>