iOS与JS的交互(UIWebView与WKWebView)
随着移动开发的不断发展。只局限于原生可能已经不太满足目前的需求了。免不了要与网页打交道。在混合开发的大势下,跟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调用原生有两种方法
- 1.拦截webview加载,订制对应规则,完成方法的调用
- 2.向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的协议,这样更加方便
从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,我们通过代码可以发现,messageHandlers
跟postMessage
中间的参数,就是我们在生成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
,还在入门中,深入了解后,或许会有另一篇文章介绍。
文章写完了,才疏学浅。有错的地方希望各位大神不吝赐教。