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
WKWebview - Complex communication between Javascript & native code
提问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 函数。但;
- Is it possible to call native code with JS callback function as parameters?
- Is it possible to return a value to JS function from native code?
- 是否可以使用JS回调函数作为参数调用本机代码?
- 是否可以从本机代码向 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 端执行如下操作:
Implement the protocol
WKUIDelegateand then assign the implementation to WKWebviewsuiDelegateproperty like this:self.webView.uiDelegate = selfNow write this
func webViewto override (?) / intercept the request forpromptfrom 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) }}
实现协议
WKUIDelegate,然后将实现分配给 WKWebviewsuiDelegate属性,如下所示:self.webView.uiDelegate = self现在写这个
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 itcallbackForNative), 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
- Reassign
- Create a general function to get data from Native (I called it
- Native code:
- Use the
userContentControllerto listen to incoming messages from the WebView (JS) - Use
evaluateJavaScriptto call yourgetDataFromNativeJS function with whatever params you want
- Use the
- WebView(JS)代码:
- 创建一个通用函数从Native获取数据(我称它为
getDataFromNativeNative,它调用另一个回调函数(我称之为callbackForNative),可以重新赋值 - 当想要使用一些数据调用 Native 并需要响应时,请执行以下操作:
- 重新分配
callbackForNative给您想要的任何功能 - 使用从 WebView 调用 Native
postMessage
- 重新分配
- 创建一个通用函数从Native获取数据(我称它为
- 原生代码:
- 使用
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

