关于ios:WKWebview-Javascript与本机代码之间的复杂通信

WKWebview - Complex communication between Javascript & native code

在WKWebView中,我们可以使用webkit消息处理程序调用ObjectiveC / swift代码
例如:webkit.messageHandlers..pushMessage(message)

它非常适合没有参数的简单javascript函数。 但;

  • 是否可以使用JS回调函数作为参数来调用本机代码?
  • 是否可以从本机代码向JS函数返回值?

  • 不幸的是,我找不到本机解决方案。

    但是以下解决方法解决了我的问题

    使用JavaScript Promise,您可以从iOS代码中调用resolve函数。

    更新

    这是您可以使用诺言的方式

    在JS中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
       this.id = 1;
        this.handlers = {};

        window.onMessageReceive = (handle, error, data) => {
          if (error){
            this.handlers[handle].resolve(data);
          }else{
            this.handlers[handle].reject(data);
          }
          delete this.handlers[handle];
        };
      }

      sendMessage(data) {
        return new Promise((resolve, reject) => {
          const handle = 'm'+ this.id++;
          this.handlers[handle] = { resolve, reject};
          window.webkit.messageHandlers.<yourHandler>.postMessage({data: data, id: handle});
        });
      }

    在iOS中

    使用适当的处理程序ID调用window.onMessageReceive函数


    有一种方法可以使用WkWebView从本机代码中将返回值返回给JS。这是一个小技巧,但对我来说没问题,而且我们的生产应用程序使用了很多JS / Native通讯。

    在分配给WKWebView的WKUiDelegate中,重写RunJavaScriptTextInputPanel。这使用委托处理JS提示功能的方式来完成此任务:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
        public override void RunJavaScriptTextInputPanel (WebKit.WKWebView webView, string prompt, string defaultText, WebKit.WKFrameInfo frame, Action<string> completionHandler)
        {
            // this is used to pass synchronous messages to the ui (instead of the script handler). This is because the script
            // handler cannot return a value...
            if (prompt.StartsWith ("type=", StringComparison.CurrentCultureIgnoreCase)) {
                string result = ToUiSynch (prompt);
                completionHandler.Invoke ((result == null) ?"" : result);
            } else {
                // actually run an input panel
                base.RunJavaScriptTextInputPanel (webView, prompt, defaultText, frame, completionHandler);
                //MobApp.DisplayAlert ("EXCEPTION","Input panel not implemented.");

            }
        }

    就我而言,我要传递数据类型= xyz,名称= xyz,数据= xyz来传递args。我的ToUiSynch()代码处理该请求并始终返回一个字符串,该字符串作为简单的返回值返回JS 。

    在JS中,我只需要使用格式化的args字符串调用hint()函数并获取返回值即可:

    1
    return prompt ("type=" + type +";name=" + name +";data=" + (typeof data ==="object" ? JSON.stringify ( data ) : data ));


    该答案使用了内森·布朗(Nathan Brown)的答案。

    据我所知,目前尚无办法将数据返回到javascript同步方式。希望苹果将来会提供解决方案。

    因此,hack是拦截来自js的提示调用。
    Apple提供此功能是为了在js调用警报,提示等时显示本机弹出设计。
    现在,由于提示是该功能,您可以在其中向用户显示数据(我们将其用作方法param),并且用户对此提示的响应将返回给js(我们将其用作返回数据)

    只能返回字符串。
    这以同步方式发生。

    我们可以如下实现上述想法:

    在javascript端:
    通过以下方式调用swift方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
        function callNativeApp(){
        console.log("callNativeApp called");
        try {
            //webkit.messageHandlers.callAppMethodOne.postMessage("Hello from JavaScript");


            var type ="SJbridge";
            var name ="functionOne";
            var data = {name:"abc", role :"dev"}
            var payload = {type: type, functionName: name, data: data};

            var res = prompt(JSON.stringify (payload));

            //{"type":"SJbridge","functionName":"functionOne","data":{"name":"abc","role":"dev"}}
            //res is the response from swift method.

        } catch(err) {
            console.log('The native context does not exist yet');
        }
    }

    在swift / xcode端执行以下操作:

  • 实现协议WKUIDelegate,然后将实现分配给WKWebviews uiDelegate属性,如下所示:

    1
    self.webView.uiDelegate = self
  • 现在,将此func webView重写为(?)/拦截来自javascript的prompt请求。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {


    if let dataFromString = prompt.data(using: .utf8, allowLossyConversion: false) {
        let payload = JSON(data: dataFromString)
        let type = payload["type"].string!

        if (type =="SJbridge") {

            let result  = callSwiftMethod(prompt: payload)
            completionHandler(result)

        } else {
            AppConstants.log("jsi_","unhandled prompt")
            completionHandler(defaultText)
        }
    }else {
        AppConstants.log("jsi_","unhandled prompt")
        completionHandler(defaultText)
    }}
  • 如果不调用completionHandler(),则js执行不会继续。现在解析json并调用适当的swift方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
        func callSwiftMethod(prompt : JSON) -> String{

        let functionName = prompt["functionName"].string!
        let param = prompt["data"]

        var returnValue ="returnvalue"

        AppConstants.log("jsi_","functionName: \(functionName) param: \(param)")

        switch functionName {
        case"functionOne":
            returnValue = handleFunctionOne(param: param)
        case"functionTwo":
            returnValue = handleFunctionTwo(param: param)
        default:
            returnValue ="returnvalue";
        }
        return returnValue
    }

    XWebView是当前的最佳选择。它可以自动将原生对象公开给javascript环境。

    对于问题2,您必须将JS回调函数传递给native以获得结果,因为从JS到native的同步通信是不可能的。

    有关更多详细信息,请检查示例应用程序。


    我有一个解决问题1的方法。

    使用JavaScript的PostMessage

    1
    window.webkit.messageHandlers.<handler>.postMessage(function(data){alert(data);}+"");

    在您的Objective-C项目中处理它

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    -(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
        NSString *callBackString = message.body;
        callBackString = [@"(" stringByAppendingString:callBackString];
        callBackString = [callBackString stringByAppendingFormat:@")('%@');", @"Some RetString"];
        [message.webView evaluateJavaScript:callBackString completionHandler:^(id _Nullable obj, NSError * _Nullable error) {
            if (error) {
                NSLog(@"name = %@ error = %@",@"", error.localizedDescription);
            }
        }];
    }

    我设法解决了这个问题-实现了本机应用程序与WebView(JS)之间的双向通信-使用JS中的postMessage和本机代码中的evaluateJavaScript

    高层的解决方案是:

    • WebView(JS)代码:

      • 创建一个通用函数以从本机获取数据(我为Native称它为getDataFromNative,它调用了另一个回调函数(我称其为callbackForNative)),该函数可以重新分配
      • 当要使用某些数据调用Native并需要响应时,请执行以下操作:

        • callbackForNative重新分配给所需的任何功能
        • 使用postMessage从WebView调用Native
    • 本机代码:

      • 使用userContentController侦听来自WebView(JS)的传入消息
      • 使用evaluateJavaScript使用所需的任何参数来调用getDataFromNative JS函数

    这是代码:

    JS:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // Function to get data from Native
    window.getDataFromNative = function(data) {
        window.callbackForNative(data)
    }

    // Empty callback function, which can be reassigned later
    window.callbackForNative = function(data) {}

    // Somewhere in your code where you want to send data to the native app and have it call a JS callback with some data:
    window.callbackForNative = function(data) {
        // Do your stuff here with the data returned from the native app
    }
    webkit.messageHandlers.YOUR_NATIVE_METHOD_NAME.postMessage({ someProp: 'some value' })

    原生(Swift):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    // Call this function from `viewDidLoad()`
    private func setupWebView() {
        let contentController = WKUserContentController()
        contentController.add(self, name:"YOUR_NATIVE_METHOD_NAME")
        // You can add more methods here, e.g.
        // contentController.add(self, name:"onComplete")

        let config = WKWebViewConfiguration()
        config.userContentController = contentController
        self.webView = WKWebView(frame: self.view.bounds, configuration: config)
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        print("Received message from JS")

        if message.name =="YOUR_NATIVE_METHOD_NAME" {
            print("Message from webView: \(message.body)")
            sendToJavaScript(params: [
               "foo":"bar"
            ])
        }

        // You can add more handlers here, e.g.
        // if message.name =="onComplete" {
        //     print("Message from webView from onComplete: \(message.body)")
        // }
    }

    func sendToJavaScript(params: JSONDictionary) {
        print("Sending data back to JS")
        let paramsAsString = asString(jsonDictionary: params)
        self.webView.evaluateJavaScript("getDataFromNative(\(paramsAsString))", completionHandler: nil)
    }

    func asString(jsonDictionary: JSONDictionary) -> String {
        do {
            let data = try JSONSerialization.data(withJSONObject: jsonDictionary, options: .prettyPrinted)
            return String(data: data, encoding: String.Encoding.utf8) ??""
        } catch {
            return""
        }
    }

    附言我是前端开发人员,所以我对JS非常熟练,但是对Swift的经验很少。

    P.S.2确保未缓存您的WebView,否则即使更改了HTML / CSS / JS却不更改WebView时,您可能会感到沮丧。

    参考文献:

    该指南对我有很大帮助:https://medium.com/@JillevdWeerd/creating-links-between-wkwebview-and-native-code-8e998889b503


    你不能
    如@Clement所述,您可以使用promises并调用resolve函数。
    很好的例子(尽管使用Deferred-现在被认为是反模式)是GoldenGate。

    在Javascript中,您可以使用两种方法创建对象:调度和解决:
    (为了方便阅读,我已经将CS编译为js)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    this.Goldengate = (function() {
      function Goldengate() {}

      Goldengate._messageCount = 0;

      Goldengate._callbackDeferreds = {};

      Goldengate.dispatch = function(plugin, method, args) {
        var callbackID, d, message;
        callbackID = this._messageCount;
        message = {
          plugin: plugin,
          method: method,
         "arguments": args,
          callbackID: callbackID
        };
        window.webkit.messageHandlers.goldengate.postMessage(message);
        this._messageCount++;
        d = new Deferred;
        this._callbackDeferreds[callbackID] = d;
        return d.promise;
      };

      Goldengate.callBack = function(callbackID, isSuccess, valueOrReason) {
        var d;
        d = this._callbackDeferreds[callbackID];
        if (isSuccess) {
          d.resolve(valueOrReason[0]);
        } else {
          d.reject(valueOrReason[0]);
        }
        return delete this._callbackDeferreds[callbackID];
      };

      return Goldengate;

    })();

    然后你打电话

    1
      Goldengate.dispatch("ReadLater","makeSomethingHappen", []);

    从iOS方面来看:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
        func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
            let message = message.body as! NSDictionary
            let plugin = message["plugin"] as! String
            let method = message["method"] as! String
            let args = transformArguments(message["arguments"] as! [AnyObject])
            let callbackID = message["callbackID"] as! Int

            println("Received message #\(callbackID) to dispatch \(plugin).\(method)(\(args))")

            run(plugin, method, args, callbackID: callbackID)
        }

        func transformArguments(args: [AnyObject]) -> [AnyObject!] {
            return args.map { arg in
                if arg is NSNull {
                    return nil
                } else {
                    return arg
                }
            }
        }

        func run(plugin: String, _ method: String, _ args: [AnyObject!], callbackID: Int) {
            if let result = bridge.run(plugin, method, args) {
                println(result)

                switch result {
                case .None: break
                case .Value(let value):
                    callBack(callbackID, success: true, reasonOrValue: value)
                case .Promise(let promise):
                    promise.onResolved = { value in
                        self.callBack(callbackID, success: true, reasonOrValue: value)
                        println("Promise has resolved with value: \(value)")
                    }
                    promise.onRejected = { reason in
                        self.callBack(callbackID, success: false, reasonOrValue: reason)
                        println("Promise was rejected with reason: \(reason)")
                    }
                }
            } else {
                println("Error: No such plugin or method")
            }
        }

        private func callBack(callbackID: Int, success: Bool, reasonOrValue: AnyObject!) {
            // we're wrapping reason/value in array, because NSJSONSerialization won't serialize scalar values. to be fixed.
            bridge.vc.webView.evaluateJavaScript("Goldengate.callBack(\(callbackID), \(success), \(Goldengate.toJSON([reasonOrValue])))", completionHandler: nil)
        }

    请考虑这篇关于承诺的好文章