WKWebview-Javascript和;本机代码

WKWebview - Complex communication between Javascript & native code

本文关键字:本机代码 WKWebview-Javascript      更新时间:2023-09-26

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

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

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

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

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

使用javascript承诺&您可以从iOS代码中调用解析函数。

更新

这就是如何使用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函数

有一种方法可以使用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.");
        }
    }

在我的例子中,我传递datatype=xyz,name=xyz,data=xyz来传递args。我的ToUiSynch()代码处理请求并总是返回一个字符串,该字符串作为一个简单的返回值返回到JS。

在JS中,我只是用格式化的args字符串调用prompt()函数,然后得到一个返回值:

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

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

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

所以破解就是拦截来自js的提示调用。苹果提供这一功能是为了在js调用警报、提示等时显示本地弹出窗口设计。现在,由于提示是一个功能,您可以向用户显示数据(我们将利用它作为方法参数),用户对此提示的响应将返回给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
}

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

高层的解决方案是:

  • WebView(JS)代码:
    • 创建一个通用函数来从Native获取数据(我称之为getDataFromNative for Native,它调用另一个回调函数(我称其为callbackForNative),可以重新分配
    • 当想要使用某些数据调用Native并需要响应时,请执行以下操作:
      • callbackForNative重新分配给您想要的任何函数
      • 使用postMessage从WebView调用Native
  • 本机代码:
    • 使用userContentController收听来自WebView(JS)的传入消息
    • 使用evaluateJavaScript使用您想要的任何参数调用getDataFromNativeJS函数

这是代码:

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/createing-links--wkwebview和本地代码-8e99889b503

XWebView是当前的最佳选择。它可以自动向javascript环境公开本机对象。

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

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

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

带有JavaScript 的PostMessage

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

你不能。正如@Clement提到的,您可以使用promise并调用resolve函数。GoldenGate是一个非常好的例子(尽管使用了Deferred,现在被认为是反模式的)。

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

然后你打电话给

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

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