WKWebview - Complex communication between Javascript & native code
在WKWebView中,我们可以使用webkit消息处理程序调用ObjectiveC / swift代码
例如:
它非常适合没有参数的简单javascript函数。 但;
不幸的是,我找不到本机解决方案。
但是以下解决方法解决了我的问题
使用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调用
有一种方法可以使用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端执行以下操作:
实现协议
1 | self.webView.uiDelegate = self |
现在,将此
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) }} |
如果不调用
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中的
高层的解决方案是:
-
WebView(JS)代码:
-
创建一个通用函数以从本机获取数据(我为Native称它为
getDataFromNative ,它调用了另一个回调函数(我称其为callbackForNative )),该函数可以重新分配 -
当要使用某些数据调用Native并需要响应时,请执行以下操作:
-
将
callbackForNative 重新分配给所需的任何功能 -
使用
postMessage 从WebView调用Native
-
将
-
创建一个通用函数以从本机获取数据(我为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) } |
请考虑这篇关于承诺的好文章