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