关于javascript:WebView-与-JS-的交互

3次阅读

共计 29605 个字符,预计需要花费 75 分钟才能阅读完成。

博客搬迁
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
// 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>
正文完
 0