这是我参与更文挑战的第8天,活动详情查看: 更文挑战
H5的需求
-
1.跳转到加载到H5的控制器后,系统的NavigationBar需要隐藏,而导航栏由WebView加载的H5渲染,点击H5上的返回按钮,进行页面的pop操作。
-
2.在加载完的WebView页面,点击H5上的按钮,调起原生的相机应用(至于是系统相机,还是定制化UI的相机,由原生自己控制),使用相机拍照完成后,将图片通过base64字符串的形式传递给H5。
-
3.加载完成WebView后,需要将之前原生获取的token传递到H5端。
原生分析与方案确定:
-
第一个例子,点击H5的按钮,然后使得原生界面关闭是典型的JS调用原生。
-
第二个例子,就是典型的互调,先有JS调起原生相机,而后原生在获取到图片资源后,调用JS方法,将原生里的数据传递给JS。
-
第三个例子和第一个例子是一样的,也是JS调用原生,不过需要注意的WebView的生命周期,注意是WebView的生命周期,我在调试的时候饶了点弯路,稍后分享。
JS调用原生方法
既然已经分析完了到底是谁调用谁,那么接下来就是正确的调用函数即可了。
WKWebViewConfiguration
类中有个属性userContentController
open class WKWebViewConfiguration : NSObject, NSSecureCoding, NSCopying {
/** @abstract The user content controller to associate with the web view.
*/
open var userContentController: WKUserContentController
}
复制代码
userContentController属性是专门用来监听JS调用方法的,而userContentController的类WKUserContentController
中,有一个专门监听JS方法句柄的方法
open class WKUserContentController : NSObject, NSSecureCoding {
/** @abstract Adds a script message handler to the main world used by page content itself.
@param scriptMessageHandler The script message handler to add.
@param name The name of the message handler.
@discussion Calling this method is equivalent to calling addScriptMessageHandler:contentWorld:name:
with [WKContentWorld pageWorld] as the contentWorld argument.
*/
open func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String)
}
复制代码
WKScriptMessageHandler是需要设置的代理,一般是WebView所在的控制器,name就是JS的方法句柄。
需要注意的是这个WKScriptMessageHandler代理,最好是进行封装一层,避免循环引用,代码如下
import Foundation
import WebKit
class WeakScriptMessageDelegate: NSObject {
//MARK:- 属性设置
private weak var scriptDelegate: WKScriptMessageHandler!
//MARK:- 初始化
init(scriptDelegate: WKScriptMessageHandler) {
self.scriptDelegate = scriptDelegate
}
}
extension WeakScriptMessageDelegate: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
self.scriptDelegate.userContentController(userContentController, didReceive: message)
}
}
复制代码
接下来,我们就可以将这个配置项赋值给WebView了
private lazy var webView: WKWebView = {
/// 生成配置项
let config = WKWebViewConfiguration()
config.userContentController.add(WeakScriptMessageDelegate(scriptDelegate: self), name: "SeasonCallback")
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = true
config.preferences = preferences
/// webView初始化时,入参配置项
let webView = WKWebView(frame: view.frame, configuration: config)
webView.allowsBackForwardNavigationGestures = true
/// webView的监听
webView.navigationDelegate = self
return webView
}()
复制代码
然后,我们在控制器这一层实现WKScriptMessageHandler的代理:
extension WebViewController: WKScriptMessageHandler {
/// 原生界面监听JS运行,截取JS中的对应在userContentController注册过的方法
///
/// - Parameters:
/// - userContentController: WKUserContentController
/// - message: WKScriptMessage 其中包含方法名称已经传递的参数,WKScriptMessage,其中body可以接收的类型是Allowed types are NSNumber, NSString, NSDate, NSArray, NSDictionary, and NSNull
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print("方法名:\(message.name)")
print("参数:\(message.body)")
guard let msg = message.body as? [String: String], let value = msg["method"] else { return }
if value == "back" {
navigationController?.popViewController(animated: true)
}
}
}
复制代码
在HTML,我们需要注册这样一个JS方法:
JS中messageHandlers后注册的方法名必须和在原生注册的方法名一模一样!这里是SeasonCallback。
对应上面的原生方法的注释,WKScriptMessage中的body,实际就是js传递过来的参数argument,我个人比较偏向于在JS中传递JSON,这样在原生中转Dictionary一来方便,二来可以传递多个参数。
下面的方法和H5中按钮组件点击事件进行绑定:
function jsButtonAction() {
let argument = {
'method' : 'back',
'params' : {}
};
window.webkit.messageHandlers.SeasonCallback.postMessage(argument);
}
复制代码
这样JS调用原生函数的通道就打通了,我们可以注册多个句柄,或者在传参中method
传入不同的值来区分不同的方法。
要注意的是,为了避免循环引用,需要的控制器的析构函数中,将注册监听的JS句柄移除:
deinit {
webView.configuration.userContentController.removeScriptMessageHandler(forName: "SeasonCallback")
}
复制代码
原生调用JS方法
调用方式
原生调JS简单一点,只用调用WebView这个方法就可以了:
extension WKWebView {
public func evaluateJavaScript(_ javaScript: String, in frame: WKFrameInfo? = nil, in contentWorld: WKContentWorld, completionHandler: ((Result<Any, Error>) -> Void)? = nil)
}
复制代码
比如我们在H5中的JS编写这样一个函数:
function callJS(text) {
console.log(text);
return "你好,Swift!";
}
复制代码
在Swift中这样调用
webView.evaluateJavaScript("callJS('这是从Swift传递到JS的参数')") { any, error in
print(any)
print(error)
}
复制代码
调用时机
之前讲第三个需求加载完成WebView后,需要将之前原生获取的token传递到H5端。
。
我们项目由于前端是用Vue写的,在main.js里添加了这样一段代码,我理解的是用钩子注册了一个方法:
window.getToken = function(token) {
Vue.prototype.$token = token;
}
复制代码
而我在WebView所在的控制中添加了这样一段代码:
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(webView)
if let url = URL(string: url) {
let request = URLRequest(url: url)
webView.load(request)
webView.evaluateJavaScript("getToken('\(token)')") { any, error in
print(any)
print(error)
}
}
}
复制代码
我在webView.load(request)
后就立即执行了运行JS方法的函数,而前端的朋友根本就拿不到token!!!
为什么?!因为我理解的是在原生的viewDidLoad一旦添加了webView并加载了request,就可以运行 webView.evaluateJavaScript
了。但实际上,Vue的生命周期中window.getToken = function(token) { Vue.prototype.$token = token; }
这段代码是页面初始化后,该方法才被注册进去的。
所以我要调用这个JS方法,必须等到WebView加载完成之后才行!!!
给WebView添加WKNavigationDelegate代理,
webView.navigationDelegate = self
复制代码
在WKNavigationDelegate代理中,func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
该回调中执行evaluateJavaScript就可以正确的将token传递给H5页面。
extension WebViewController: WKNavigationDelegate {
/// 页面加载完成之后调用
///
/// - Parameters:
/// - webView: 实现该代理的webview
/// - navigation: 当前navigation
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("CUSCGetToken('\(token)')") { any, error in
print(any)
print(error)
}
}
}
复制代码
总结
-
Swift与JS方法互调并没有特别难的地方,首先需要明确就是谁调用谁,一旦明确这个是Swift调用JS,抑或是JS调用Swift,那么剩下来的就是使用正确的方法了。
-
在Swift与JS制定好方法名,传参格式,严格按照双方开发人员制定的规则传递函数与参数,这样才能成功调起函数并通信。
-
了解一些Html的初始化周期,有利于把握Swift调用JS的时机。
明日继续
明天可能会插播一下Flutter与JS交互的方案与例子,大家加油。