隨著移動開發(fā)的不斷發(fā)展。只局限于原生可能已經(jīng)不太滿足目前的需求了。免不了要與網(wǎng)頁打交道。在混合開發(fā)的大勢下,跟web進(jìn)行交互是必然的。
我們都知道在iOS的api中,提供了UIWebView和WKWebView。我們可以通過它們加載網(wǎng)頁,并實現(xiàn)網(wǎng)頁與原生之間的交互。
Hybrid的交互,分為兩種。一為JS調(diào)用原生,二為原生調(diào)用JS.
demo地址
原生調(diào)用JS
關(guān)于原生調(diào)用JS,無論是UIWebView還是WKWebView...都提供了自己的API方法,稍后細(xì)說
//UIWebView
open func stringByEvaluatingJavaScript(from script: String) -> String?
//WKWebView
open func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil)
JS調(diào)用原生
JS調(diào)用原生有兩種方法
- 1.攔截webview加載,訂制對應(yīng)規(guī)則,完成方法的調(diào)用
- 2.向JS注入對象,完成方法調(diào)用
下面將對以上的進(jìn)行介紹,文章先從UIWebView說起
UIWebView
文檔上可以看到,UIWebView從iOS2.0開始啟用,iOS12.0開始被棄用。但是繼續(xù)使用也是可以的。當(dāng)然無論性能還是速度,都比WKWebView要差。但是作為學(xué)習(xí),我們還是先從它說起。
"Talk is cheap,show me the code"
let url = Bundle.main.url(forResource: "index", withExtension: "html")
let request = URLRequest(url: url!)
self.uiWebView.loadRequest(request)
使用以上代碼就可以實現(xiàn)用UIWebview加載網(wǎng)頁了,當(dāng)然,這里我加載的是我本地創(chuàng)建的html
接下來就到交互環(huán)節(jié)了,原生調(diào)JS比較簡單,這里就先說原生調(diào)用JS.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1, maximum-scale=1, user-scalable=no">
<script type="text/javascript">
function nativeCall() {
document.getElementById('showNative').innerHTML = "原生調(diào)用"
alert("111")
}
</script>
</head>
<body>hello world
<div id='showNative'></div>
</body>
</html>
我在本地的html中,定義了一個nativeCall()
的方法,要調(diào)用它,其實很簡單。UIWebView提供了一個API方法
open func stringByEvaluatingJavaScript(from script: String) -> String?
所以,我們只需要拿到webview,調(diào)用該方法即可
//調(diào)用js中的方法
@IBAction func callJS(_ sender: Any) {
self.uiWebView.stringByEvaluatingJavaScript(from: "nativeCall()")
}
結(jié)果如圖
說完了原生調(diào)用JS,接下來就到了JS調(diào)用原生,方法可以分為兩種。
方法一:攔截webview加載
上圖的代理方法,就是我們需要用到的,每次頁面進(jìn)行加載時,該代理方法都會響應(yīng),我們根據(jù)對應(yīng)的請求來判斷是不是需要調(diào)用原生方法。
function callNativeByHref() {
window.location.href = ("test://callNative");
}
function callNativeByIFrame() {
var execIframe = document.createElement('iframe');
execIframe.style.display = 'none';
execIframe.src = 'test://callNative';
document.body.appendChild(execIframe);
}
<button onclick="callNativeByHref()">uiweiview調(diào)用原生(href)</button>
<button onclick="callNativeByIFrame()">uiweiview調(diào)用原生(iframe)</button>
在html中,我添加了兩個方法,并添加了兩個按鈕調(diào)用對應(yīng)方法。 接下來,我們回到原生代碼中
// MARK: -
// MARK: webview代理
extension UIWebViewController:UIWebViewDelegate {
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebView.NavigationType) -> Bool {
//按照定制的請求規(guī)則,判斷是否調(diào)用原生方法的
if request.url?.host == "callNative" {
self.CallNative()
return false
}
return true
}
}
如代碼所示,兩個方法任意一個,都會引起webview代理方法的回調(diào),在代理中對加載請求進(jìn)行判斷,按照我們制定的規(guī)則,一旦符合,我們就可以調(diào)用我們的原生方法了。
方法二:使用JSCore
使用JS注入的方法,獲取webview當(dāng)前環(huán)境,向js中注入一個對象,也可以完成JS調(diào)用原生的方法。
JSCore提供了JSExport協(xié)議方法,讓我們可以把方法注入到JSContext中,首先我們需要定義一個遵守JSExport協(xié)議的方法。由于Swift面向協(xié)議變成...我們直接定義一個遵循JSExport的協(xié)議,這樣更加方便
從API介紹中,可以看到,該協(xié)議是由OC調(diào)用的,項目使用Swift,定義協(xié)議的時候,需要在
protocol
前加上@objc
關(guān)鍵字,不然將無法注入
// MARK: 協(xié)議,定義js調(diào)取原生的方法列表
//千萬千萬千萬要加@objc
@objc protocol CallNative:JSExport {
func CallNative()
}
我們定義完方法之后,由我們當(dāng)前的webview來完成就好了,代碼如下
// MARK: -
// MARK: 完成協(xié)議中定義的方法,js調(diào)用原生會默認(rèn)調(diào)用此擴(kuò)展中的方法
extension UIWebViewController:CallNative {
func CallNative() {
print("展示信息:")
}
}
注入的方法寫好了,那么怎么完成注入呢,我們需要在加載頁面完成時,獲取當(dāng)前的JSContext,然后注入app對象到JSContext中
//webview加載完成
func webViewDidFinishLoad(_ webView: UIWebView) {
self.callJSBtn.isEnabled = true
//獲取當(dāng)前js context
let context = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as! JSContext
//webview加載完成后,設(shè)置當(dāng)前viewcontroller為Html中的app對象
context.setObject(self, forKeyedSubscript: "app" as NSCopying & NSObjectProtocol)
}
接著我們回到HTML中,找到我們注入的app
對象,調(diào)用我們定義的方法就完事了
function callNativeByJSCore() {
document.getElementById('showNativeCall').innerHTML = "調(diào)用原生";
app.CallNative();
}
UIWebview的介紹到這里位置
WKWebview
首先我們初始化一個wkwebview對象,加載index.html。由于版本問題,無法在sb文件中通過拉控件的方式加載wkwebview。所以我們純代碼生成
var wkWebView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
//添加wkwebview.如果要向下兼容的話,無法在sb文件中添加wkwebview。需要手動添加
self.initWebView()
}
//初始化webview
func initWebView() {
self.wkWebView = WKWebView.init(frame: self.view.bounds)
self.wkWebView.navigationDelegate = self
self.wkWebView.uiDelegate = self
self.view.addSubview(self.wkWebView)
let url = Bundle.main.url(forResource: "index", withExtension: "html")
let request = URLRequest(url: url!)
wkWebView.load(request)
}
上面說到wkwebview提供了api方法可以調(diào)用js,我們直接使用就行了
//native調(diào)用js中的方法
@IBAction func callJS(_ sender: Any) {
wkWebView.evaluateJavaScript("nativeCall()") { (obj, error) in
print(error?.localizedDescription ?? "")
}
}
上面uiwebview的展示中,可以發(fā)現(xiàn)我們在HTML中做了一個alert彈框。但實際在wkwebview下,卻不會彈出提示框。這是因為wkwebview攔截了alert方法.在WKWebView的一系列協(xié)議中,我們發(fā)現(xiàn)有WKUIDelegate
協(xié)議,其中有三個代理方法
optional func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void)
optional func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void)
optional func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void)
分別對應(yīng)html界面中的alert
,confirm
,prompt
。文章只對alert進(jìn)行簡單介紹,由于wkwebview實現(xiàn)了對alert的攔截,我們需要在對應(yīng)的代理方法中,手動的調(diào)出alert提示框
// MARK:-
// MARK:webviewUI代理
extension WKWebViewController:WKUIDelegate {
//使用WkWebview時發(fā)現(xiàn)無法alert,原因是wkwebview攔截了該響應(yīng),需要在代理回調(diào)中手動彈出alert,
//注意此處需要返回completionHandler,不然程序會crash
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
let alert = UIAlertController(title: "提示", message: message, preferredStyle: .alert)
let action = UIAlertAction(title: "確定", style: .default) { (action) in
completionHandler()
}
alert.addAction(action)
self.present(alert, animated: true, completion: nil)
}
}
上述代碼,可以完成彈框的實現(xiàn)。但切記,必須要處理方法返回的completionHandler
閉包,否則程序會crash.
JS調(diào)用原生
攔截網(wǎng)頁加載
UIWebView有shouldStartLoadWith
代理方法,WKWebView也有對應(yīng)的方法可以攔截到webview每次進(jìn)行的加載。我們使用的是WKNavigationDelegate
中的代理方法
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
具體實現(xiàn)如下
// MARK:-
// MARK:webview加載代理
extension WKWebViewController:WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
let url = navigationAction.request.url;
if url?.host == "callNative" {
self.callNative()
decisionHandler(.cancel)
return;
}
decisionHandler(.allow)
}
}
messageHandlers注入
首先,我們注意到,初始化WKWebView時,有一種生成方式
/*! @abstract Returns a web view initialized with a specified frame and
configuration.
@param frame The frame for the new web view.
@param configuration The configuration for the new web view.
@result An initialized web view, or nil if the object could not be
initialized.
@discussion This is a designated initializer. You can use
@link -initWithFrame: @/link to initialize an instance with the default
configuration. The initializer copies the specified configuration, so
mutating the configuration after invoking the initializer has no effect
on the web view.
*/
public init(frame: CGRect, configuration: WKWebViewConfiguration)
需要傳入一個名為configuration
的參數(shù),我們進(jìn)入api會發(fā)現(xiàn),該類有一個屬性WKUserContentController
,根據(jù)注釋可以看到該對象負(fù)責(zé)與webview的聯(lián)系,我們也是通過該屬性實現(xiàn)messageHandlers注入。
/*! @abstract The user content controller to associate with the web view.
*/
open var userContentController: WKUserContentController
首先,我們創(chuàng)建一個config對象,并在該對象的userContentController添加我們要注入的messageHandlers名稱
//生成webconfiguration
func setWebConfigure() -> WKWebViewConfiguration {
let config = WKWebViewConfiguration()
config.userContentController = WKUserContentController()
//在此處注冊方法,js發(fā)送消息后,才可以掉調(diào)用原生方法
//js發(fā)送消息為:window.webkit.messageHandlers.callNative.postMessage
config.userContentController.add(self, name: "callNative")
return config
}
然后,我們在html中傳遞message,我們通過代碼可以發(fā)現(xiàn),messageHandlers
跟postMessage
中間的參數(shù),就是我們在生成config時添加的
function wkCallNative() {
document.getElementById('showNativeCall').innerHTML = "WKWebView調(diào)用原生";
window.webkit.messageHandlers.callNative.postMessage("1");
}
最后,我們在native這邊,會有WKScriptMessageHandler
協(xié)議讓我們接收到JS中發(fā)送過來的message,我們通過對應(yīng)的message進(jìn)行原生的方法調(diào)用
// MARK:-
// MARK:webview接收回調(diào)代理
extension WKWebViewController:WKScriptMessageHandler {
//js發(fā)起message時會響應(yīng)該代理,我們就是在該代理方法中完成原生與js的交互
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
//判斷message的名稱,確定調(diào)用哪一個方法
if message.name.isEqual("callNative") {
self.callNative()
}
}
}
文章到這,就結(jié)束了。
什么時候使用哪種方法調(diào)用原生,不同調(diào)用方法的側(cè)重點,我還在繼續(xù)研究。
根據(jù)我的研究,cordova
框架使用的是攔截web加載的方式完成native跟js交互的。當(dāng)然,沒有像文中那么簡單的調(diào)用。cordova在調(diào)用原生方法前,會在當(dāng)前JS中生成一個對象,對象中會包含請求的方法名,方法參數(shù)等等,然后js會加載一個固定的請求頭,原生攔截到之后,會去加載JS中的方法,拿到上面我說的生成對象,拿到方法名以及請求參數(shù),然后通過selector的方式調(diào)用對應(yīng)方法。
至于React Native
,還在入門中,深入了解后,或許會有另一篇文章介紹。
文章寫完了,才疏學(xué)淺。有錯的地方希望各位大神不吝賜教。