博客搬迁
2020.1.3

iOS UIWebView逐步被淘汰, WKWebView成为支流. 本文封装了WKJSWebView(代码见第三节),参考EasyJSWebView的交互方式, 对其进行了批改和减少. 能够实现原生调用JS, 也能够JS调用原生。

一. 应用办法

JS调原生

  1. 创立一个交互类, 定义给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
  1. 初始化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];}
  1. 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

  1. js注册办法
<script>  // js  function changeColor(param) {      let div = document.getElementById("oi");      div.style.backgroundColor = param.color;  };  window.EasyJS.mount("divChangeColor", changeColor);</script>
  1. 原生调用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>