WKWebview - Javascript 和本机代码之间的复杂通信

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/29249132/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 03:03:28  来源:igfitidea点击:

WKWebview - Complex communication between Javascript & native code

javascriptiosobjective-cswiftwkwebview

提问by Clement Prem

In WKWebView we can call ObjectiveC/swift code using webkit message handlers eg: webkit.messageHandlers.<handler>.pushMessage(message)

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

It works well for simple javascript functions without parameters. But;

它适用于没有参数的简单 javascript 函数。但;

  1. Is it possible to call native code with JS callback function as parameters?
  2. Is it possible to return a value to JS function from native code?
  1. 是否可以使用JS回调函数作为参数调用本机代码?
  2. 是否可以从本机代码向 JS 函数返回值?

采纳答案by Clement Prem

Unfortunately I couldn't find a native solution.

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

But the following workaround solved my problem

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

Use javascript promises & you can call the resolve function from your iOS code.

使用 javascript promises & 你可以从你的 iOS 代码中调用 resolve 函数。

UPDATE

更新

This is how you can use promise

这就是你可以使用promise的方式

In JS

在JS中

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

in iOS

在 iOS 中

Call the window.onMessageReceivefunction with appropriate handler id

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

回答by Nathan Brown

There is a way to get a return value back to JS from the native code using WkWebView. It is a little hack but works fine for me without problems, and our production app uses a lot of JS/Native communication.

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

In the WKUiDelegate assigned to the WKWebView, override the RunJavaScriptTextInputPanel. This uses the way that the delegate handles the JS prompt function to accomplish this:

在分配给 WKWebView 的 WKUiDelegate 中,覆盖 RunJavaScriptTextInputPanel。这使用委托处理JS提示功能的方式来完成这个:

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

        }
    }

In my case, I am passing data type=xyz,name=xyz,data=xyz to pass the args in. My ToUiSynch() code handles the request and always returns a string, which goes back to the JS as a simple return value.

就我而言,我正在传递数据 type=xyz,name=xyz,data=xyz 来传递参数。我的 ToUiSynch() 代码处理请求并始终返回一个字符串,该字符串作为一个简单的返回值返回给 JS .

In the JS, I am simply calling the prompt() function with my formatted args string and getting a return value:

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

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

回答by Sasuke Uchiha

This answer uses the idea from Nathan Brown's answerabove.

这个答案使用了 Nathan Brown上面的答案的想法。

As far as I know, currently there is no way to return data back to javascript synchronousway. Hopefully apple will provide the solution in future release.

据我所知,目前没有办法将数据返回到 javascript同步方式。希望苹果在未来的版本中提供解决方案。

So hack is to intercept the prompt calls from js. Apple provided this functionality in order to show native popup design when js calls the alert, prompt etc. Now since prompt is the feature, where you show the data to user (we will exploit this as method param ) and the response from user to this prompt will be returned back to js (we'll exploit this as return data)

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

Only string can be returned. This happens in synchronous way.

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

We can implement the above idea as follows:

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

At the javascript end:call the swift method in the following way:

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

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

At the swift/xcode enddo as follows:

swift/xcode 端执行如下操作:

  1. Implement the protocol WKUIDelegateand then assign the implementation to WKWebviews uiDelegateproperty like this:

    self.webView.uiDelegate = self
    
  2. Now write this func webViewto override (?) / intercept the request for promptfrom javascript.

    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)
    }}
    
  1. 实现协议WKUIDelegate,然后将实现分配给 WKWebviewsuiDelegate属性,如下所示:

    self.webView.uiDelegate = self
    
  2. 现在写这个func webView来覆盖(?)/拦截prompt来自javascript的请求。

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

If you don't call the completionHandler()then js execution will not proceed. Now parse the json and call appropriate swift method.

如果你不调用completionHandler()then js 执行将不会继续。现在解析 json 并调用适当的 swift 方法。

    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
}

回答by soflare

XWebViewis the best choice currently. It can automatically expose native objects to javascript environment.

XWebView是目前最好的选择。它可以自动将本机对象暴露给 javascript 环境。

For the question 2, you have to pass an JS callback function to native to get result, because synchronized communication from JS to native is impossible.

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

For More details, check the sampleapp.

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

回答by u6902455

I have a workaround for question1.

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

PostMessage with JavaScript

使用 JavaScript 发布消息

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

Handle It in your Objective-C project

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

-(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);
        }
    }];
}

回答by Liran H

I managed to solve this problem - to achieve two-way communication between the native app and the WebView (JS) - using postMessagein the JS and evaluateJavaScriptin the Native code.

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

The solution from high-level was:

高层的解决方案是:

  • WebView (JS) code:
    • Create a general function to get data from Native (I called it getDataFromNativefor Native, which calls another callback function (I called it callbackForNative), which can be reassigned
    • When wanting to call Native with some data and requiring a response, do the following:
      • Reassign callbackForNativeto whatever function you want
      • Call Native from the WebView using postMessage
  • Native code:
    • Use the userContentControllerto listen to incoming messages from the WebView (JS)
    • Use evaluateJavaScriptto call your getDataFromNativeJS function with whatever params you want
  • WebView(JS)代码:
    • 创建一个通用函数从Native获取数据(我称它为getDataFromNativeNative,它调用另一个回调函数(我称之为callbackForNative),可以重新赋值
    • 当想要使用一些数据调用 Native 并需要响应时,请执行以下操作:
      • 重新分配callbackForNative给您想要的任何功能
      • 使用从 WebView 调用 Native postMessage
  • 原生代码:
    • 使用userContentController来侦听来自 WebView (JS) 的传入消息
    • 用于使用您想要的任何参数evaluateJavaScript调用您的getDataFromNativeJS 函数

Here is the code:

这是代码:

JS:

JS:

// 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' })

Native (Swift):

本机(斯威夫特):

// 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 ""
    }
}

P.S. I'm a Front-end Developer, so I'm very skilled in JS, but have very little experience in Swift.

PS我是一个前端开发者,所以我对JS非常熟练,但对Swift的经验很少。

P.S.2 Make sure your WebView is not cached, or you might get frustrated when the WebView doesn't change despite changes to the HTML/CSS/JS.

PS2 确保您的 WebView 没有被缓存,否则,尽管更改了 HTML/CSS/JS,但当 WebView 没有改变时,您可能会感到沮丧。

References:

参考:

This guide helped me a lot: https://medium.com/@JillevdWeerd/creating-links-between-wkwebview-and-native-code-8e998889b503

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

回答by Karol Klepacki

You can't. As @Clement mentioned, you can use promises and call the resolve function. Quite good (although using Deferred - which is considered to be anti-pattern now) example is GoldenGate.

你不能。正如@Clement 提到的,您可以使用承诺并调用解析函数。很好(虽然使用 Deferred - 现在被认为是反模式)示例是GoldenGate

In Javascript you can create object with two methods: dispatch and resolve: (I've compiled cs to js for easier reading)

在 Javascript 中,您可以使用两种方法创建对象:dispatch 和 resolve:(我已将 cs 编译为 js 以便于阅读)

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;

})();

Then you call

然后你打电话

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

And from the iOS side:

从 iOS 方面来看:

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

Please consider this great article about promises

请考虑这篇关于 Promise 的好文章