App与Js交互(一)iOS

作者: gwpp | 来源:发表于2018-02-07 11:03 被阅读672次

    目录

    示例代码

    Demo: https://github.com/gwpp/jsinterface

    前言

    不论是在创业团队中快速试错,还是在成熟团队中快速迭代复杂需求,还或者是其他原因,WebView在APP中的大量使用已经成为了一个明显的趋势,这也应该算是大前端融合的一个表象吧。笔者在工作中也遇到过很多App&Js交互的问题,粗浅的研究了一下,这里也分享给大家,如果有错误的地方还请下方留言指出,共同进步。

    iOS系统中的交互

    众所周知,iOS有UIWebViewWKWebView两个组件可以用来渲染嵌入页面。前者使用甚广,出生的也早,后者是iOS8推出的,优化了加载速度和内存,安全性上也有所提升。具体的两者比较百度、简书上都很多,这里不做赘述。

    方案一,拦截跳转

    • WebView:UIWebView
    • 原生调用JS:
      UIWebView直接调用Js方法,示例代码如下:
      [self.webView stringByEvaluatingJavaScriptFromString:@"showResponse('123')"];
      
    • JS调用原生:
      拦截跳转是我们最常见的一种方式,也是最简单,最容易理解的一种。我们可以在UIWebView的代理方法中拦截每一个请求,如果是特殊的链接就可以做一些事情,比如跳转、执行某些方法等。示例如下:
      // JS端
      window.location = 'app://login?account=13011112222&password=123456';
      
      // iOS端
      - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {  
        NSString *scheme = request.URL.scheme;
        NSString *host = request.URL.host;
        
        // 一般用作交互的链接都会有一个固定的协议头,这里我们一“app”作为协议头为了,实际项目中可以修改
        if ([scheme isEqualToString:@"app"]) { // scheme为“app”说明是做交互的链接
            if ([host isEqualToString:@"login"]) { // host为“login”对应的就是登录操作
                NSDictionary *paramsDict = [request.URL getURLParams];
                NSString *account = paramsDict[@"account"];
                NSString *password = paramsDict[@"password"];
                NSString *msg = [NSString stringWithFormat:@"执行登录操作,账号为:%@,密码为:%@", account, password];
                UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"原生弹窗" message:msg delegate:nil cancelButtonTitle:@"好" otherButtonTitles:nil, nil];
                [alert show];
            }
        return YES;
      }
      

    方案二,拦截跳转

    • WebView:WKWebView

    • 原生调用JS:
      WKWebView直接调用Js方法,示例代码如下:

      [self.webView evaluateJavaScript:@"showResponse('点击了原生的按钮22222222222')" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
            if (error) {
                NSLog(@"%@", error);
            } else {
                NSLog(@"%@", response);
            }
       }];
      

      它相对于UIWebView而言最大的优点就是支持callback,不想UIWebView那样只能一去不复返。

    • JS调用原生:
      类似UIWebView,在WK中我们同样可以拦截跳转,原理相同,代码不同。示例如下:

      // JS端
      window.location = 'app://login?account=13011112222&password=123456';
      
      // iOS端
      - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
        NSURLRequest *request = navigationAction.request;
        NSString *scheme = request.URL.scheme;
        NSString *host = request.URL.host;
        
        // 一般用作交互的链接都会有一个固定的协议头,这里我们一“app”作为协议头为了,实际项目中可以修改
        if ([scheme isEqualToString:@"app"]) { // scheme为“app”说明是做交互的链接
            if ([host isEqualToString:@"login"]) { // host为“login”对应的就是登录操作
                NSDictionary *paramsDict = [request.URL getURLParams];
                NSString *account = paramsDict[@"account"];
                NSString *password = paramsDict[@"password"];
                NSString *msg = [NSString stringWithFormat:@"执行登录操作,账号为:%@,密码为:%@", account, password];
                UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"原生弹窗" message:msg delegate:nil cancelButtonTitle:@"好" otherButtonTitles:nil, nil];
                [alert show];
            }
            
            // ... 这里可以继续加 else if
            
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
        }
        decisionHandler(WKNavigationActionPolicyAllow);
      }
      

    阶段小结

    前两种方法到此就介绍完了,很简单,但是在项目大了之后拦截跳转的代理方法中会有非常多的判断。冗余、可维护性差,硬编码重。所以我们会有下面的其他方法。

    方案三,JSContext

    JSContext即JavaScriptContext,这个东西在UIWebView中可以拿到,但是在WKWebView中却是取不到了,所以只能用在UIWebView中。除此以外Android里也有类似的一个东西,所以使用JSContext就有了在JS端多平台统一的可能,这里不多说,在《App与Js交互(三)》中会有详细说明。
    JSContext的原理就是iOS暴露出去一个遵守<JSExport>协议的对象给JS,JS可以直接调用该对象的public方法。

    • WebView:UIWebView
    • 原生调用JS:
      // 有两种方式。jsContext 是一个【JSContext *】变量,需要在【webViewDidFinishLoad: 】方法中每次赋值
      // 方式1:
      [self.jsContext evaluateScript:@"showResponse('点击了按钮1111111111111111')"];
      
      // 方式2:
      JSValue *value = self.jsContext[@"showResponse"];
      [value callWithArguments:@[@"点击了按钮222222222"]];
      
    • JS调用原生:
      // JS端,app是iOS中注册的一个对象
      app.login("13011112222", "123456");
      
      // iOS端
      // 每次嵌入页面加载完毕都要给jsContext赋值,否则在js端调用可能会失效。
      - (void)webViewDidFinishLoad:(UIWebView *)webView {
        self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
        self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
            context.exception = exception;
            NSLog(@"异常信息:%@", exception);
        };
        // app是随便取的名字,可以改,改了之后JS要同步修改。如果Android端使用@JavaScriptInterface的形式,那么还要保证Android、iOS两端同步,建议都用app
        self.jsContext[@"app"] = [[JSContextModel alloc] init];
      }
      
      // JSContextModel,
      @protocol JsContextExport<JSExport>
      /**
       * 登出方法,js调用的方法名也是logout
       */
      - (void)logout;
      /**
       * 登录方法,JSExportAs的作用就是给OC方法导出一个js方法名,例如下面的方法js调用就是 login("your account", "your password")。在多参数的方法声明时必须使用这种方式
       */
      JSExportAs(login, - (void)loginWithAccount:(NSString *)account   password:(NSString *)password);
      /**
       * 获取登录信息
       * @return 当前登录用户的身份信息。JSContext方式调用OC时,方法的返回值只能是NSString、NSArray、NSDictionary、NSNumber、BooL,其他类型不能解析
       */
      - (NSDictionary *)getLoginUser;
      @end
      
      @interface JSContextModel : NSObject<JsContextExport>
      @end
      

    方案四,WebKit

    window.webkit.messagehandlers.<name>.postMessage是apple推荐使用的WKWebView的JS交互方式,使用起来比较简单,不支持callback回调。

    • WebView:WKWebView
    • 原生调用JS:
      参考【方案二】的原生调用JS
    • JS调用原生:
      // js
      window.webkit.messageHandlers.login.postMessage({
        'account': '13000000000',
        'password': '123456'
      });
      
      // iOS - 初始化WKWebView时设置 configuration
      self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:[[WKWebViewConfiguration alloc] init]];
      WKUserContentController *confVc = self.webView.configuration.userContentController;
        [confVc addScriptMessageHandler:self name:@"login"];
        
      // iOS - 在ScriptMessageHandler 的代理方法中处理
      - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
        if ([message.name isEqualToString:@"login"]) {
            if (![message.body isKindOfClass:[NSDictionary class]]) {
                return;
            }
            NSDictionary *data = message.body;
            NSString *account = data[@"account"];
            NSString *password = data[@"password"];
            
            NSString *msg = [NSString stringWithFormat:@"执行登录操作,账号为:%@,密码为:%@", account, password];
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"原生弹窗" message:msg delegate:nil cancelButtonTitle:@"好" otherButtonTitles:nil, nil];
            [alert show];
            return;
        }
      }
      

    方案五,JsBridge

    • WebView:UIWebView、WKWebView同时支持,且方法名完全没有差异,但是特别要注意的一点就是:iOS原生是不支持这种方式的,我们需要依赖于一个三方库 —— WebViewJavascriptBridge,这是一个很有名的库,具体有多牛逼这里也不做过多需求,百度一下你就知道。
    • 初始化代码:
      // JS初始化代码
      /**
       * 初始化jsbridge
       * @param readyCallback 初始化完成后的回调
       */
       function initJsBridge(readyCallback) {
           var u = navigator.userAgent;
           var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; //android终端
           var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
           // 注册jsbridge
           function connectWebViewJavascriptBridge(callback) {
               if (isAndroid) {
                   if (window.WebViewJavascriptBridge) {
                       callback(WebViewJavascriptBridge)
                   } else {
                       document.addEventListener(
                           'WebViewJavascriptBridgeReady'
                           , function () {
                               callback(WebViewJavascriptBridge)
                           },
                           false
                       );
                   }
                   return;
               }
               if (isiOS) {
                   if (window.WebViewJavascriptBridge) {
                       return callback(WebViewJavascriptBridge);
                   }
                   if (window.WVJBCallbacks) {
                       return window.WVJBCallbacks.push(callback);
                   }
                   window.WVJBCallbacks = [callback];
                   var WVJBIframe = document.createElement('iframe');
                   WVJBIframe.style.display = 'none';
                   WVJBIframe.src = 'https://__bridge_loaded__';
                   document.documentElement.appendChild(WVJBIframe);
                   setTimeout(function () {
                       document.documentElement.removeChild(WVJBIframe)
                   }, 0)
               }
           }
           // 调用注册方法
           connectWebViewJavascriptBridge(function (bridge) {
               if (isAndroid) {
                   bridge.init(function (message, responseCallback) {
                       console.log('JS got a message', message);
                       responseCallback(data);
                   });
               }
                
               // 只有在这里注册过的方法,在原声代码里才能用callHandler的方式调用
               bridge.registerHandler('jsbridge_showMessage', function (data, responseCallback) {
                   showResponse(data);
               });
               bridge.registerHandler('jsbridge_getJsMessage', function (data, responseCallback) {
                    showResponse(data);
                    responseCallback('native 传过来的是:' + data);
                });
      
                readyCallback();
            });
        }
      
      // iOS初始化代码
       - (void)setupJsBridge {
        if (self.bridge) return;
        // self.webview既可以是UIWebView,又可以是WKWebView
        self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView];
        
        [self.bridge registerHandler:@"getOS" handler:^(id data, WVJBResponseCallback responseCallback) {
            // 这里Response的回调可以传id类型数据,但是为了保持Android、iOS的统一,全部使用json字符串作为返回数据
            NSDictionary *response = @{@"error": @(0), @"message": @"", @"data": @{@"os": @"ios"}};
            responseCallback([response jsonString]);
        }];
        
        [self.bridge registerHandler:@"login" handler:^(id data, WVJBResponseCallback responseCallback) {
            if (data == nil || ![data isKindOfClass:[NSDictionary class]]) {
                NSDictionary *response = @{@"error": @(-1), @"message": @"调用参数有误"};
                responseCallback([response jsonString]);
                return;
            }
            
            NSString *account = data[@"account"];
            NSString *passwd = data[@"password"];
            NSDictionary *response = @{@"error": @(0), @"message": @"登录成功", @"data" : [NSString stringWithFormat:@"执行登录操作,账号为:%@、密码为:@%@", account, passwd]};
            responseCallback([response jsonString]);
        }];
      }
      
    • 原生调用JS
      [self.bridge callHandler:@"jsbridge_getJsMessage" data:@"点击了原生的按钮222222222" responseCallback:^(id responseData) {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"显示jsbridge返回值" message:responseData delegate:nil cancelButtonTitle:@"好" otherButtonTitles:nil, nil];
         [alert show];
      }];
      
    • JS调用原生
      // 首先调用JSBridge初始化代码,完成后再设置其他
      initJsBridge(function () {
        $("#getOS").click(function () {
              // 通过JsBridge调用原生方法,写法固定,第一个参数时方法名,第二个参数时传入参数,第三个参数时响应回调
              window.WebViewJavascriptBridge.callHandler('getOS', null, function (response) {
              showResponse(response);
            });
        });
       });
      

    相关文章

      网友评论

      • 杭城小刘:在拦截跳转的部分,你写到了 window.location 这种请求网络的方式如果只请求一次是没问题的,如果连续多次修改window.location.href的值,在Native层只能接收到最后一次请求,前面的请求都会被忽略掉。 大名鼎鼎的 WebViewJavascriptBridge 推荐使用 iframe 的形式发起请求

      本文标题:App与Js交互(一)iOS

      本文链接:https://www.haomeiwen.com/subject/hseozxtx.html