WKJSWebView
iOS UIWebView逐渐被淘汰, WKWebView成为主流. 本文参考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
// OC
NSDictionary* 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()
方法:
// js
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));
};
})();
}
}
在window增加native对象,并且把JSInterface的交互方法都加到native对象.这里的native相当于JSInterface在h5中的镜像, 通过native,就可以调用原生方法:
// js
window.native.testWithParamscallback('abc', (p1, p2, p3) => {
// h5回调函数
});
发送消息给原生
但是, native.testWithParamscallback长这样的:
// js
function() {
return EasyJS.call(obj, method, Array.prototype.slice.call(arguments));
};
这是镜像native的testWithParamscallback方法, 它并不能换起原生, 真正调用原生的是EasyJS.call()
.
// js
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);
}
}
Easy.call()
将js的回调函数生成唯一ID对应保存到EasyJS.__callbacks
,再将唯一ID和参数按约定的方式编译放入数组 ,然后用原生约定的监听名字NativeListener发送消息给原生window.webkit.messageHandlers.NativeListener.postMessage(obj + ':' + encodeURIComponent(functionName) + argStr)
;
NativeListener在初始化webView时指定, 同时将原生交互类映射interfaces挂载到监听者.
// OC
// add message handler
WKJSListener *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
中对应的方法并执行:
// js
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);
},
args.shift()
移除多余的参数.
至此, js调原生流程结束.
原生调js
js注册函数
原生调用js, 需要js将方法注册到window, 注入js中提供了mount()
方法给js注册函数用:
// js
mount: function (funcName, handler) {
EasyJS.__events[funcName] = handler;
},
mount()
方法将JS函数handler存放到__events, 以便提供给原生调用.
js中注册也很简单:
// js
window.EasyJS.mount("divChangeColor", changeColor);
这样就将divChangeColor
函数注册了, 它对应js中的changeColor()
方法:
// js
function 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
的函数并执行.
// js
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 + '函数未定义');
}
}
至此, 原生调用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>