iOS 开发每天分享优质文章自鉴Swift 专栏

iOS与JS的交互(UIWebView与WKWebView)

2019-05-14  本文已影响0人  阿丶伦

随着移动开发的不断发展。只局限于原生可能已经不太满足目前的需求了。免不了要与网页打交道。在混合开发的大势下,跟web进行交互是必然的。
我们都知道在iOS的api中,提供了UIWebView和WKWebView。我们可以通过它们加载网页,并实现网页与原生之间的交互。
Hybrid的交互,分为两种。一为JS调用原生,二为原生调用JS.
demo地址

原生调用JS

关于原生调用JS,无论是UIWebView还是WKWebView...都提供了自己的API方法,稍后细说

//UIWebView
open func stringByEvaluatingJavaScript(from script: String) -> String?
//WKWebView
open func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil)

JS调用原生

JS调用原生有两种方法

下面将对以上的进行介绍,文章先从UIWebView说起

UIWebView

webview官方文档

文档上可以看到,UIWebView从iOS2.0开始启用,iOS12.0开始被弃用。但是继续使用也是可以的。当然无论性能还是速度,都比WKWebView要差。但是作为学习,我们还是先从它说起。

"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)

使用以上代码就可以实现用UIWebview加载网页了,当然,这里我加载的是我本地创建的html
接下来就到交互环节了,原生调JS比较简单,这里就先说原生调用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 = "原生调用"
        alert("111")
    }
</script>
</head>
<body>hello world
<div id='showNative'></div>
</body>
</html>

我在本地的html中,定义了一个nativeCall()的方法,要调用它,其实很简单。UIWebView提供了一个API方法

open func stringByEvaluatingJavaScript(from script: String) -> String?

所以,我们只需要拿到webview,调用该方法即可

//调用js中的方法
    @IBAction func callJS(_ sender: Any) {
        self.uiWebView.stringByEvaluatingJavaScript(from: "nativeCall()")
    }

结果如图


QQ20190507-225816-HD.gif

说完了原生调用JS,接下来就到了JS调用原生,方法可以分为两种。

方法一:拦截webview加载
image.png

上图的代理方法,就是我们需要用到的,每次页面进行加载时,该代理方法都会响应,我们根据对应的请求来判断是不是需要调用原生方法。

    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调用原生(href)</button>
<button onclick="callNativeByIFrame()">uiweiview调用原生(iframe)</button>

在html中,我添加了两个方法,并添加了两个按钮调用对应方法。 接下来,我们回到原生代码中

// MARK: -
// MARK: webview代理
extension UIWebViewController:UIWebViewDelegate {
    func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebView.NavigationType) -> Bool {
        //按照定制的请求规则,判断是否调用原生方法的
        if request.url?.host == "callNative" {
            self.CallNative()
            return false
        }
        return true
    }
}

如代码所示,两个方法任意一个,都会引起webview代理方法的回调,在代理中对加载请求进行判断,按照我们制定的规则,一旦符合,我们就可以调用我们的原生方法了。

方法二:使用JSCore

使用JS注入的方法,获取webview当前环境,向js中注入一个对象,也可以完成JS调用原生的方法。
JSCore提供了JSExport协议方法,让我们可以把方法注入到JSContext中,首先我们需要定义一个遵守JSExport协议的方法。由于Swift面向协议变成...我们直接定义一个遵循JSExport的协议,这样更加方便

image.png
从API介绍中,可以看到,该协议是由OC调用的,项目使用Swift,定义协议的时候,需要在protocol前加上@objc关键字,不然将无法注入
// MARK: 协议,定义js调取原生的方法列表
//千万千万千万要加@objc
@objc protocol CallNative:JSExport {
    func CallNative()
}

我们定义完方法之后,由我们当前的webview来完成就好了,代码如下

// MARK: -
// MARK: 完成协议中定义的方法,js调用原生会默认调用此扩展中的方法
extension UIWebViewController:CallNative {
    func CallNative() {
        print("展示信息:")
    }
}

注入的方法写好了,那么怎么完成注入呢,我们需要在加载页面完成时,获取当前的JSContext,然后注入app对象到JSContext中

//webview加载完成
    func webViewDidFinishLoad(_ webView: UIWebView) {
        self.callJSBtn.isEnabled = true
        //获取当前js context
        let context = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as! JSContext
        //webview加载完成后,设置当前viewcontroller为Html中的app对象
        context.setObject(self, forKeyedSubscript: "app" as NSCopying & NSObjectProtocol)
    }

接着我们回到HTML中,找到我们注入的app对象,调用我们定义的方法就完事了

function callNativeByJSCore() {
        document.getElementById('showNativeCall').innerHTML = "调用原生";
        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方法可以调用js,我们直接使用就行了

//native调用js中的方法
    @IBAction func callJS(_ sender: Any) {
        wkWebView.evaluateJavaScript("nativeCall()") { (obj, error) in
            print(error?.localizedDescription ?? "")
        }
    }

上面uiwebview的展示中,可以发现我们在HTML中做了一个alert弹框。但实际在wkwebview下,却不会弹出提示框。这是因为wkwebview拦截了alert方法.在WKWebView的一系列协议中,我们发现有WKUIDelegate协议,其中有三个代理方法

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)

分别对应html界面中的alert,confirm,prompt。文章只对alert进行简单介绍,由于wkwebview实现了对alert的拦截,我们需要在对应的代理方法中,手动的调出alert提示框

// MARK:-
// MARK:webviewUI代理
extension WKWebViewController:WKUIDelegate {
    //使用WkWebview时发现无法alert,原因是wkwebview拦截了该响应,需要在代理回调中手动弹出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)
    }
}

上述代码,可以完成弹框的实现。但切记,必须要处理方法返回的completionHandler闭包,否则程序会crash.


JS调用原生

拦截网页加载

UIWebView有shouldStartLoadWith代理方法,WKWebView也有对应的方法可以拦截到webview每次进行的加载。我们使用的是WKNavigationDelegate中的代理方法

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

具体实现如下

// 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的参数,我们进入api会发现,该类有一个属性WKUserContentController,根据注释可以看到该对象负责与webview的联系,我们也是通过该属性实现messageHandlers注入。

/*! @abstract The user content controller to associate with the web view.
    */
    open var userContentController: WKUserContentController

首先,我们创建一个config对象,并在该对象的userContentController添加我们要注入的messageHandlers名称

//生成webconfiguration
    func setWebConfigure() -> WKWebViewConfiguration {
        let config = WKWebViewConfiguration()
        config.userContentController = WKUserContentController()
        //在此处注册方法,js发送消息后,才可以掉调用原生方法
        //js发送消息为:window.webkit.messageHandlers.callNative.postMessage
        config.userContentController.add(self, name: "callNative")
        return config
    }

然后,我们在html中传递message,我们通过代码可以发现,messageHandlerspostMessage中间的参数,就是我们在生成config时添加的

function wkCallNative() {
        document.getElementById('showNativeCall').innerHTML = "WKWebView调用原生";
        window.webkit.messageHandlers.callNative.postMessage("1");
    }

最后,我们在native这边,会有WKScriptMessageHandler协议让我们接收到JS中发送过来的message,我们通过对应的message进行原生的方法调用

// MARK:-
// MARK:webview接收回调代理
extension WKWebViewController:WKScriptMessageHandler {
    //js发起message时会响应该代理,我们就是在该代理方法中完成原生与js的交互
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        //判断message的名称,确定调用哪一个方法
        if message.name.isEqual("callNative")  {
            self.callNative()
        }
    }
    
}

文章到这,就结束了。
什么时候使用哪种方法调用原生,不同调用方法的侧重点,我还在继续研究。
根据我的研究,cordova框架使用的是拦截web加载的方式完成native跟js交互的。当然,没有像文中那么简单的调用。cordova在调用原生方法前,会在当前JS中生成一个对象,对象中会包含请求的方法名,方法参数等等,然后js会加载一个固定的请求头,原生拦截到之后,会去加载JS中的方法,拿到上面我说的生成对象,拿到方法名以及请求参数,然后通过selector的方式调用对应方法。
至于React Native,还在入门中,深入了解后,或许会有另一篇文章介绍。
文章写完了,才疏学浅。有错的地方希望各位大神不吝赐教。

上一篇下一篇

猜你喜欢

热点阅读