【问题标题】:WKWebview - Complex communication between Javascript & native codeWKWebview - Javascript 和本机代码之间的复杂通信
【发布时间】:2015-05-28 17:43:01
【问题描述】:

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

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

  1. 可以用JS回调函数作为参数调用native代码吗?
  2. 是否可以从原生代码向 JS 函数返回值?

【问题讨论】:

  • 您应该为此目的使用 PhoneGap (phonegap.com)。它的跨平台并在 webview 和本机代码之间提供最可靠的通信。
  • 我的应用程序使用了一些第三方 skds 来连接外部硬件,所以我无法移动到电话间隙。我可以用 UIwebview + JSCore 做到这一点,我正在寻找类似的解决方案
  • 好的,那么您可能需要检查一下:github.com/marcuswestin/WebViewJavascriptBridge
  • 基本上它通过UIWebviewDelegate方法从javascript到native通信;并通过stringByEvaluatingJavaScriptFromString 方法从本机到javascript。您也可以像这样创建自己的桥梁。
  • 我正在尝试在没有任何第三方库的情况下解决同样的问题。我尝试将回调保存在带有关键请求 ID 的 JS 全局字典中。本机代码回调 webView.evaluateJavaScript(request ID)。这在某些情况下有效,但不是全部。可能是因为全局变量可能是每帧。仍在调查。

标签: javascript ios objective-c swift wkwebview


【解决方案1】:

很遗憾,我找不到本地解决方案。

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

使用 javascript 承诺,您可以从 iOS 代码中调用 resolve 函数。

更新

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

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

在 iOS 中

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

【讨论】:

  • 例子? “使用 Javascript 承诺”不是很清楚。
  • 我正在考虑使用 wkwebview 或 crosswalkwebview 从 obj c 方法返回值,你能提供 ios 代码
  • 谢谢朋友!我想到了 Promises,但是如果在 JS 方面的经验很少 - 无法实现它。你的 sn-p 帮助我理解它。
  • @ClementPrem 您是否设法使其适用于这种方法?我无法理解它(我不是 JS 大师)。如何添加新的处理程序?只是在 this.handlers 中硬编码它们?处理程序是函数吗?第 3 行的语法是否正确?(=>) 谁调用了 sendMessage(data)?
  • 为了以同步方式执行此操作,this 回答有效。
【解决方案2】:

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

在分配给 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.");

        }
    }

在我的例子中,我传递 data type=xyz,name=xyz,data=xyz 来传递参数。我的 ToUiSynch() 代码处理请求并始终返回一个字符串,该字符串作为简单的返回值。

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

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

【讨论】:

  • 不错!我正在使用 webViews 将项目从 android(本地调用可以返回值)移植到 iOS。这为我节省了很多时间和代码更改。谢谢分享,先生。
  • 你知道如何在 Objective-c 中做到这一点吗?
  • WKUiDelegate 中没有可以覆盖的函数。
  • 这是一个非常聪明的主意;感谢分享。
  • 警告,此方法不稳定!如果大量消息发送非常快,WKWebView 将崩溃(变黑)或重新加载。
【解决方案3】:

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

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

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

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

我们可以如下实现上述思路:

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

swift/xcode结束做如下:

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

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

    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 方法。

    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
}

【讨论】:

  • 我有一个代码应该在提示调用之前执行,例如document.getElementId('hint').innerHTML = "hello world"; return prompt("send message to native"); 并在 swift 中覆盖 RunJavaScripttextInputPanel(...) ,如此处所述。提示之前的代码永远不会执行;提示只是首先执行,在从本机返回后,提示上方的行(例如document.getElementId('hint').innerHTML = "hello world";)将不再执行。
  • 我觉得javascript的提示方式肯定是走对了。但是,我尝试实现它并注意到它只在第一次工作(该页面将执行十几个这样的请求)。有没有人见过类似的问题?
【解决方案4】:

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

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

更多详情,请查看sample 应用程序。

【讨论】:

  • 这会保留 Javascript 的作用域上下文吗?例如,如果我通过var a = 2; var cb = function() { console.warn(a, arguments); }; ios.doSomething(cb);,它会记录2吗?
  • 很遗憾没有。本机代码只能评估全局范围内的回调函数。解决方法: var a = 2; var cb = function(a) { console.warn(a, arguments); }; ios.doSomething(cb.bind(this, a));
【解决方案5】:

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

高层的解决方案是:

  • WebView (JS) 代码:
    • 创建一个通用函数来从 Native 中获取数据(对于 Native,我称之为 getDataFromNative,它调用另一个回调函数(我称之为 callbackForNative),可以重新赋值
    • 当想要使用一些数据调用 Native 并需要响应时,请执行以下操作:
      • callbackForNative 重新分配给您想要的任何功能
      • 使用 postMessage 从 WebView 调用 Native
  • 本机代码:
    • 使用userContentController 监听来自 WebView (JS) 的传入消息
    • 使用evaluateJavaScript 调用您的getDataFromNative 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' })

原生 (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 ""
    }
}

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

P.S.2 确保您的 WebView 没有被缓存,否则您可能会因为 HTML/CSS/JS 发生更改而 WebView 没有更改而感到沮丧。

参考资料:

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

【讨论】:

    【解决方案6】:

    我有问题 1 的解决方法。

    使用 JavaScript 发布消息

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

    在您的 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);
            }
        }];
    }
    

    【讨论】:

      【解决方案7】:

      你不能。 正如@Clement 提到的,您可以使用 Promise 并调用 resolve 函数。 相当不错(尽管使用 Deferred - 现在被认为是反模式)示例是 GoldenGate

      在 Javascript 中,您可以使用两种方法创建对象:调度和解析: (为了方便阅读,我将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;
      
      })();
      

      然后你打电话

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

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

      请考虑this great article about promises

      【讨论】:

      • 不要在你的答案中随意使用变量,而绝对不解释它是什么:参见“bridge.run(plugin, method, args)”
      猜你喜欢
      • 2012-05-25
      • 2014-07-25
      • 2012-11-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-07
      相关资源
      最近更新 更多