iOS开发中或多或少会嵌入h5页面,h5页面有时需要和原生进行交互:比如h5界面需要通知原生处理一些事情(如拍照等),原生界面需要传参给h5界面(如拍照的图片数据);由于iOS现在已弃用UIWebView,今天主要详细梳理下WKWebView和JavaScript交互细节;
demo准备
完整代码
为了说明WKWebView和JavaScript的交互,搭建了一个简单的界面:
iOS界面:
- 蓝色为原生button
- 红色为原生label
- 青色为webview界面(加载本地html),带有h5的button
webview = WKWebView(frame: CGRect(x: 0, y: label.frame.maxY, width:
view.bounds.width, height: 200))
view.addSubview(webview)
let fileURL = Bundle.main.url(forResource: "iOSTest", withExtension: "html")
webview.loadFileURL(fileURL!, allowingReadAccessTo: Bundle.main.bundleURL)
iOSTest.html代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iOS WKWebView 交互</title>
</head>
<body style="background-color: aquamarine">
<button id="mm_btn" style="font-size: 17px;" onclick="alert(alertText())">
web button
</button>
<script>
function alertText() {
return 'hello js'
}
function jsFunction() {
return 'CallJSFunction'
}
function jsFunctionParameter(prefix) {
return prefix + 'CallJSFunction'
}
</script>
</body>
</html>
提示框
h5的button编写了一个alert弹框的代码,在浏览器上打开点击按钮能正常显示提示框;但在我们这个代码WKWebView中却没任何反应;
这是因为WKWebView默认是不显示h5弹框的,需要手动编写代码提示原生的弹框;
- 设置webview的ui代理
webview.uiDelegate = self
- 实现协议,拦截弹框事件;协议方法有3个,对应1个按钮的提示框,2个按钮的及带输入框的提示框;可以通过回调的方法取得提示框的内容,然后使用原生的提示框:
extension ViewController: WKUIDelegate {
// 只有一个 按钮的Alert
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
let alertController = UIAlertController(title: "js alert", message: message, preferredStyle: .alert)
alertController.addAction(
UIAlertAction(title: "ok", style: .default, handler: { _ in
completionHandler()
})
)
present(alertController, animated: true)
}
// 两个按钮的 调用block返回yes或者no来确定是点击了取消按钮还是同意按钮
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
// 原生的弹框... completionHandler(true)
}
// 带输入框的alert completionHandler可以回调给js输入框输入的内容
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
// 原生的弹框... completionHandler("xxx")
}
}
iOS调用JS
WKWebView提供了执行JS的方法evaluateJavaScript
/* @abstract Evaluates the given JavaScript string.
@param javaScriptString The JavaScript string to evaluate.
@param completionHandler A block to invoke when script evaluation completes or fails.
@discussion The completionHandler is passed the result of the script evaluation or an
error.
*/
open func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil)
使用比较简单:
// webview调用js 已有函数
webview.evaluateJavaScript("jsFunction()") {[weak self] (data, _) in
self?.label.text = data as? String ?? "data error"
}
// webview调用js 已有函数(带参数)
webview.evaluateJavaScript("jsFunctionParameter('wkWebview')") {[weak self] (data, _)
in
self?.label.text = data as? String ?? "data error"
}
点击原生button时调用以上evaluateJavaScript,可以发现能正常调用JS:
iOS注入JS代码
另一种iOS调用JS的方式是通过添加WKUserScript注入的方式;
let webConfig = WKWebViewConfiguration()
let webUserController = WKUserContentController()
// 注入js
let injectionScript = WKUserScript(source:"jsFunctionParameter('wkWebview')", injectionTime: .atDocumentEnd,
forMainFrameOnly: true)
webUserController.addUserScript(injectionScript)
webConfig.userContentController = webUserController
// 通过自定义config的方式创建webview
webview = WKWebView(frame: CGRect(x: 0, y: label.frame.maxY, width:
view.bounds.width, height: 200), configuration: webConfig)
....
以上代码和之前evaluateJavaScript的方式效果一样;
evaluateJavaScript和addUserScript的区别
1 . 调用时机不同:
evaluateJavaScript可以在任何需要的时候调用(比如点击哪个原生的按钮);而addUserScript注入JS的方式只有2种时机
public enum WKUserScriptInjectionTime : Int {
// Document加载前
case atDocumentStart = 0
// Document加载后
case atDocumentEnd = 1
}
- 作用不同
- 从2者的API名称可以看出,evaluateJavaScript侧重于调用JS现有的代码;addUserScript侧重于添加JS代码(即原先JS没有对应的代码)
例如,使用addUserScript可以为h5动态添加元素:
let script = "var p = document.createElement('p'); p.innerHTML='injection from
WKWebView'; document.getElementsByTagName(\"body\")[0].appendChild(p);"
let injectionScript = WKUserScript(source: script, injectionTim
forMainFrameOnly: true)
webUserController.addUserScript(injectionScript)
webConfig.userContentController = webUserController
webview = WKWebView(frame: CGRect(x: 0, y: label.frame.maxY, wi
view.bounds.width, height: 200), configuration: webConfig)
....
重新运行代码,可以看到webview上已多了个p标签:
但是对于这点其实也没有严格的要求,两者都能实现新增JS代码、调用已有JS代码;
- evaluateJavaScript能取得JS的返回值,addUserScript没有返回值
JS调用iOS
JS调用iOS,WKWebView提供了scriptMessageHandler的API:
- WKWebView添加scriptMessageHandler
let webConfig = WKWebViewConfiguration()
let webUserController = WKUserContentController()
// 为防止循环引用,自定义WeakScriptMessageDelegate封装了一层delegate
let scriptDelegate = WeakScriptMessageDelegate(delegate: self)
webUserController.add(scriptDelegate, name: WebScriptHandlerName.clickButton)
webConfig.userContentController = webUserController
webview = WKWebView(frame: CGRect(x: 0, y: label.frame.maxY, widt
view.bounds.width, height: 200), configuration: webConfig)
....
- JS端messageHandlers需要发送事件通知WKWebView:
// html
<button id="mm_btn" style="font-size: 17px;" onclick="jsCalliOS()">
web button
</button>
// script
function jsCalliOS() {
// 无参数
// postMessage必须带参数 否则iOS收不到回调,无参数空值如postMessage({})
window.webkit.messageHandlers.clickButton.postMessage({})
// 带参数
window.webkit.messageHandlers.clickButton.postMessage('js_parameter')
}
window.webkit.messageHandlers固定写法,后面的clickButton为handler名称需要和iOS中webUserController.add(scriptDelegate, name: WebScriptHandlerName.clickButton),name一致;
- iOS端接收JS的回调(实现WKScriptMessageHandler协议方法)处理自己的逻辑,即实现了JS调用iOS
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == WebScriptHandlerName.clickButton {
// 无参数
print("call from js")
// openCamera()...
// 带参数 Allowed types are NSNumber, NSString, NSDate, NSArray,NSDictionary,NSNull
guard let parameter = message.body as? String else { return }
print("call from js " + parameter)
// openCamera(parameter)
}
}
}
JS调用iOS并获取iOS的返回值
以上JS调用iOS的交互,存在一个问题;比如调用了iOS代码并需要得到iOS处理的结果,JS怎么才能获取到这个返回值呢?WKWebView貌似并没有提供相关的API;
这时,我们就需要结合上面的提到过的知识,曲折、间接的实现返回值的功能;
- 通过提示框的方式实现
之前提到的,提示框的回调有3种;其中runJavaScriptTextInputPanelWithPrompt这种是能拿到h5提示框的内容,同时还能回传iOS输入的内容;这种情况正符合这种JS、iOS交互需求;
h5代码修改
// script
function jsCalliOS() {
// prompt弹框 result即为iOS输入框completionHandler回调回来的
var result = prompt('clickButton')
var p = document.getElementsByTagName('body')[0]
p.innerHTML = result
}
iOS端代码
// 带输入框的alert completionHandler可以回调给js输入框输入的内容
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String,
defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler:
@escaping (String?) -> Void) {
if prompt == "clickButton" {
completionHandler("result from wkwebview")
}
}
这种方式很好的利用了prompt特性,但是也存在一些问题;如果该h5不是在iOS上打开而是在其他端(浏览器、安卓)打开,由于它们不同于iOS的WKWebView,prompt就会正常弹出;因此这种方式还需要额外判断环境;
- JS调iOS,再让iOS调用JS
JS调用iOS的方式同上;
JS和iOS 2端约定一个新的function用于接收iOS返回值
// script
function getClickButtonResult(parameter) {
var p = document.getElementsByTagName('body')[0]
p.innerHTML = parameter
}
iOS端在收到JS调用回调时,再主动调用刚约定的JS的接收返回值的function:
func userContentController(_ userContentController: WKUserContentController, didReceive
message: WKScriptMessage) {
if message.name == WebScriptHandlerName.clickButton {
// js调用webview, webview再调用js 将需要的返回值通过参数带入
webview.evaluateJavaScript("getClickButtonResult('result from wkwebview')")
}
}
这种方式没有环境问题,但相对来说比较麻烦;
无法修改h5源码的情况,JS与WKWebView的交互
大部分情况,h5都是自己或者同事编写的;JS与WKWebView的交互只需要按照上面的方式2端编码就行;但如果我们用的别人的h5,肯定不会有我们需要的交互代码;
这时,我们还是可以通过注入JS的方式动态实现:
- 浏览器查看h5源码,想方设法取到需要交互的节点(如通过id,tag);例如事件的button id=mm_btn,则可以注入以下代码:
let script = """
function injectionJsCalliOS() {
window.webkit.messageHandlers.clickButton.postMessage({})
};
var button = document.getElementById('mm_btn');
button.onclick = injectionJsCalliOS;
"""
let injectionScript = WKUserScript(source: script, injection
forMainFrameOnly: true)
webUserController.addUserScript(injectionScript)
webConfig.userContentController = webUserController
之后,iOS端仍一样处理:
func userContentController(_ userContentController: WKUserContentController, didReceive
message: WKScriptMessage) {
if message.name == WebScriptHandlerName.clickButton {
// 无参数
print("call from js")
// openCamera()...
}
WebViewJavascriptBridge
上面我们使用了系统提供的API实现了交互,过程相对繁琐;
现有一个轻量的第三方库WebViewJavascriptBridge,能大大简化交互流程;
github主页
- iOS端WKWebView不再需要添加scriptMessageHandler,取而代之使用WebViewJavascriptBridge:
webview = WKWebView(frame: CGRect(x: 0, y: label.frame.maxY, width: view.bounds.width, height: 200))
...
webBridge = WebViewJavascriptBridge(webview)
WebViewJavascriptBridge调用JS:
// 无参数
webBridge.callHandler("jsFunction", data: nil) {[weak self] (rps) in
self?.label.text = rps as? String ?? "data error"
}
// 带参数
webBridge.callHandler("jsFunction", data: "wkWebview") {[weak self] (rps) in
self?.label.text = rps as? String ?? "data error"
}
WebViewJavascriptBridge添加scriptMessageHandler供JS调用(responseCallback可以直接返回值给JS,不用向系统API那样麻烦):
webBridge.registerHandler(WebScriptHandlerName.clickButton) { (data,
responseCallback) in
print("CalliOSFunction with JSParameter" + (data as? String ?? ""))
// 返回值给JS
responseCallback?("result from wkwebview")
}
- JS代码
// script
function setupWebViewJavascriptBridge(callback) {
// 官方指定固定写法
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
setupWebViewJavascriptBridge(function(bridge) {
/* Initialize your app here */
bridgeObj = bridge
// 添加Handler供iOS端调用
bridge.registerHandler('jsFunction', function(data, responseCallback) {
//data就是从iOS获取的值 responseCallback返回给iOS的值
responseCallback((data === undefined ? '' : data) + 'CallJSFunction')
})
})
JS调用iOS:
// script
function jsCalliOS() {
//-----------------------WebViewJavascriptBridge API--------------------------
bridgeObj.callHandler('clickButton', 'js_parameter', function(rsp) {
var p = document.getElementsByTagName('body')[0]
p.innerHTML = rsp
})
//----------------------------------------------------------------------------
}
ps:
Handler的名称2端同样需要约定一致;
使用了WebViewJavascriptBridge,不要再手动添加scriptMessageHandler;否则WebViewJavascriptBridge所有交互都会不生效;
扩展
Android WebView与JS的交互
- WebView调用JS
// Android 4.4
webView.loadUrl("javascript:androidCallJS()");
// Android 4.4以上, 第二个参数为闭包可以获取JS返回值
webview.evaluateJavascript("javascript:androidCallJS()", null)
- JS调用Android
Android代码
webView.addJavascriptInterface(new MyJavascriptInterface(this), "injectedObject");
public class MyJavascriptInterface {
private Context context;
public MyJavascriptInterface(Context context) {
this.context = context;
}
@JavascriptInterface
public void clickButton(String data) {
}
}
JS代码
// script
window.injectedObject.
clickButton('call from js')
参考:https://www.jianshu.com/p/97f52819a19d
WinForm CefSharp与JS的交互
CefSharp调用JS
webView.ExecuteScriptAsync("CefSharpCallJS()");
JS调用CefSharp
WinForm代码
// 将c#对象注册为 js对象
public class JsEvent
{
public void ClickButton(string parameter)
{
.....
}
}
webview.RegisterJsObject("injectedObject", new JsEvent(), false)
JS代码
// script
injectedObject.ClickButton('call from js')
参考: https://blog.csdn.net/gong_hui2000/article/details/48155547