iOS與JS的交互(UIWebView與WKWebView)

隨著移動開發(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

webview官方文檔

文檔上可以看到,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é)果如圖


QQ20190507-225816-HD.gif

說完了原生調(diào)用JS,接下來就到了JS調(diào)用原生,方法可以分為兩種。

方法一:攔截webview加載
image.png

上圖的代理方法,就是我們需要用到的,每次頁面進(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é)議,這樣更加方便

image.png

從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),messageHandlerspostMessage中間的參數(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é)淺。有錯的地方希望各位大神不吝賜教。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內(nèi)容