WKWebview - Javascript 与本机代码之间的复杂通信
WKWebview - Complex communication between Javascript & native code
在 WKWebView 中,我们可以使用 webkit 消息处理程序调用 ObjectiveC/swift 代码
例如:webkit.messageHandlers.<handler>.pushMessage(message)
它适用于没有参数的简单 javascript 函数。但是;
- 能否以JS回调函数为参数调用native代码?
- 是否可以return从本机代码向 JS 函数赋值?
XWebView是目前最好的选择。它可以自动将本机对象暴露给 javascript 环境。
对于问题2,必须给native传一个JS回调函数才能得到结果,因为JS到native的同步通信是不可能的。
有关更多详细信息,请查看 sample 应用程序。
很遗憾,我找不到本地解决方案。
但是下面的解决方法解决了我的问题
使用 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
函数
你不能。
正如@Clement 提到的,您可以使用 promises 并调用 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)
}
有一种方法可以使用 WkWebView 从本机代码中将 return 值返回给 JS。这是一个小 hack,但对我来说没有问题,而且我们的生产应用程序使用了很多 JS/Native 通信。
在分配给 WKWebView 的 WKUiDelegate 中,覆盖 RunJavaScriptTextInputPanel。这里使用delegate处理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.");
}
}
在我的例子中,我传递数据类型=xyz,名称=xyz,数据=xyz 来传递参数。我的 ToUiSynch() 代码处理请求并且总是 returns 一个字符串,它去回到 JS 作为一个简单的 return 值。
在 JS 中,我只是用格式化的 args 字符串调用 prompt() 函数并获得 return 值:
return prompt ("type=" + type + ";name=" + name + ";data=" + (typeof data === "object" ? JSON.stringify ( data ) : data ));
我有解决问题 1 的方法。
PostMessage 与 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);
}
}];
}
这个答案使用了上面 Nathan Brown 的想法。
据我所知,目前还没有办法return数据返回到javascript同步的方式。希望苹果在未来的版本中提供解决方案。
所以hack就是拦截js的提示调用。
Apple 提供此功能是为了在 js 调用警报、提示等时显示原生弹出设计。
现在由于提示是功能,您可以在其中向用户显示数据(我们将利用它作为方法参数)并且用户对此提示的响应将 returned 返回给 js(我们将利用它作为 return数据)
只能return编辑字符串。
这以同步方式发生。
我们可以将上面的思路实现如下:
在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结尾执行如下操作:
实现协议 WKUIDelegate
然后将实现分配给 WKWebviews uiDelegate
属性 像这样:
self.webView.uiDelegate = self
现在写这个 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) 之间的 two-way 通信 - 在 JS 中使用 postMessage
并在本机代码中使用 evaluateJavaScript
.
high-level 的解决方案是:
- 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 ""
}
}
P.S。我是一名 Front-end 开发人员,因此我非常精通 JS,但在 Swift.
方面的经验很少
P.S.2 确保您的 WebView 没有被缓存,否则当 HTML/CSS/JS.
发生变化时 WebView 没有改变时您可能会感到沮丧
参考文献:
本指南对我帮助很大:https://medium.com/@JillevdWeerd/creating-links-between-wkwebview-and-native-code-8e998889b503
在 WKWebView 中,我们可以使用 webkit 消息处理程序调用 ObjectiveC/swift 代码
例如:webkit.messageHandlers.<handler>.pushMessage(message)
它适用于没有参数的简单 javascript 函数。但是;
- 能否以JS回调函数为参数调用native代码?
- 是否可以return从本机代码向 JS 函数赋值?
XWebView是目前最好的选择。它可以自动将本机对象暴露给 javascript 环境。
对于问题2,必须给native传一个JS回调函数才能得到结果,因为JS到native的同步通信是不可能的。
有关更多详细信息,请查看 sample 应用程序。
很遗憾,我找不到本地解决方案。
但是下面的解决方法解决了我的问题
使用 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
函数
你不能。 正如@Clement 提到的,您可以使用 promises 并调用 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)
}
有一种方法可以使用 WkWebView 从本机代码中将 return 值返回给 JS。这是一个小 hack,但对我来说没有问题,而且我们的生产应用程序使用了很多 JS/Native 通信。
在分配给 WKWebView 的 WKUiDelegate 中,覆盖 RunJavaScriptTextInputPanel。这里使用delegate处理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.");
}
}
在我的例子中,我传递数据类型=xyz,名称=xyz,数据=xyz 来传递参数。我的 ToUiSynch() 代码处理请求并且总是 returns 一个字符串,它去回到 JS 作为一个简单的 return 值。
在 JS 中,我只是用格式化的 args 字符串调用 prompt() 函数并获得 return 值:
return prompt ("type=" + type + ";name=" + name + ";data=" + (typeof data === "object" ? JSON.stringify ( data ) : data ));
我有解决问题 1 的方法。
PostMessage 与 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);
}
}];
}
这个答案使用了上面 Nathan Brown
据我所知,目前还没有办法return数据返回到javascript同步的方式。希望苹果在未来的版本中提供解决方案。
所以hack就是拦截js的提示调用。 Apple 提供此功能是为了在 js 调用警报、提示等时显示原生弹出设计。 现在由于提示是功能,您可以在其中向用户显示数据(我们将利用它作为方法参数)并且用户对此提示的响应将 returned 返回给 js(我们将利用它作为 return数据)
只能return编辑字符串。 这以同步方式发生。
我们可以将上面的思路实现如下:
在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结尾执行如下操作:
实现协议
WKUIDelegate
然后将实现分配给 WKWebviewsuiDelegate
属性 像这样:self.webView.uiDelegate = self
现在写这个
的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) 之间的 two-way 通信 - 在 JS 中使用 postMessage
并在本机代码中使用 evaluateJavaScript
.
high-level 的解决方案是:
- WebView(JS)代码:
- 创建一个从Native获取数据的通用函数(Native我叫它
getDataFromNative
,它调用另一个回调函数(我叫它callbackForNative
),可以重新赋值 - 当想要使用一些数据调用 Native 并需要响应时,请执行以下操作:
- 将
callbackForNative
重新分配给您想要的任何功能 - 使用
postMessage
从 WebView 调用 Native
- 将
- 创建一个从Native获取数据的通用函数(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 ""
}
}
P.S。我是一名 Front-end 开发人员,因此我非常精通 JS,但在 Swift.
方面的经验很少P.S.2 确保您的 WebView 没有被缓存,否则当 HTML/CSS/JS.
发生变化时 WebView 没有改变时您可能会感到沮丧参考文献:
本指南对我帮助很大:https://medium.com/@JillevdWeerd/creating-links-between-wkwebview-and-native-code-8e998889b503