ios webkit html交互,iOS WebView 与 JS 的交互封装

博客搬家

iOS UIWebView逐渐被淘汰, WKWebView成为主流. 本文封装了WKJSWebView(代码见第三节),参考EasyJSWebView的交互方式, 对其进行了修改和增加. 可以实现原生调用JS, 也可以JS调用原生。

一. 使用方法

JS调原生

创建一个交互类, 定义给js的交互接口

// OC

#import

#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调原生接口

// 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;

});

原生调JS

js注册方法

// js

function changeColor(param) {

let div = document.getElementById("oi");

div.style.backgroundColor = param.color;

};

window.EasyJS.mount("divChangeColor", changeColor);

原生调用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} 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 * _funcs = [NSMutableArray new];

NSMutableArray * _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

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

#import

#pragma mark - WKJSWebView

@interface WKJSWebView : WKWebView

- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration*)configuration scripts:(NSArray*)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

@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

#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*)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 * _funcs = [NSMutableArray new];

NSMutableArray * _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

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 ()

@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

Document

.a {

width: 300px;

height: 80px;

font-size: 32px;

text-align: center;

line-height: 80px;

margin-bottom: 10px;

}

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);

这里是 h5 web 页面

1.点击下面按钮,调用原生方法获取随机字母并显示到h5

2.原生调用h5方法,改变该元素背景色

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值