WKWebview-Javascript和;本机代码
WKWebview - Complex communication between Javascript & native code
在WKWebView中,我们可以使用webkit消息处理程序调用ObjectiveC/swift代码例如:webkit.messageHandlers.<handler>.pushMessage(message)
它适用于没有参数的简单javascript函数。但是
- 是否可以使用JS回调函数作为参数来调用本机代码
- 是否可以从本机代码向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端执行以下操作:
实现协议
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)之间的双向通信——在JS中使用postMessage
,在本机代码中使用evaluateJavaScript
。
高层的解决方案是:
-
WebView(JS)代码:
- 创建一个通用函数来从Native获取数据(我称之为
getDataFromNative
for Native,它调用另一个回调函数(我称其为callbackForNative
),可以重新分配 - 当想要使用某些数据调用Native并需要响应时,请执行以下操作:
- 将
callbackForNative
重新分配给您想要的任何函数 - 使用
postMessage
从WebView调用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 ""
}
}
附言:我是一名前端开发人员,所以我对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)
}
请考虑这篇关于承诺的伟大文章
- 将对应用内购买的支持构建为react本机代码
- 控制台中的“function floor(){[本机代码]}”是什么
- 如何将chrome.tabCapture流从js传递到c++PNACL本机代码
- WKWebview-Javascript和;本机代码
- Array.insert函数错误,因为Object函数Array(){〔本机代码〕}没有方法'插入'
- 如何使用js ctypes Firefox扩展来调用本机C代码
- TypeError:对象函数Object(){〔本机代码〕}没有方法'方法'
- JavascriptCore:从本机代码执行JavaScript定义的回调函数
- 使用 JS-ctypes 将结构化数据传递给本机代码
- 对象函数 Date() { [本机代码] } 在 Chrome 中使用 Date.js 时没有方法“compareTo”
- 我怎样才能找到javascript函数的本机代码
- 在Phonegap(Java Android)中运行本机代码
- .bind(),它不返回javascript中的本机代码
- Javascript警报导致冻结.(带有AFNetworking和本机代码回调)
- javascript错误{〔本机代码〕}
- Backbone.js打印“;函数String(){〔本机代码〕}"而不是模型属性
- 如何使我的函数返回[本机代码]
- 在javascript中反映本机代码函数参数
- 在浏览器中执行本机代码
- 不能使用 jquery 和 d3 通过 id 获取元素,但可以通过本机代码工作