JS和原生之间的相互调用总结
基础知识
按照官方文档上的意思简单介绍这几个类的作用:
- JSVirtualMachine
JSVirtualMachine 是JavaScript的一个封闭的运行环境,主要用于支持JavaScript并行运行和管理JavaScript与OC或者Swift之间桥接的内存。
- JSContext
JSContext是JavaScript的运行环境,可以在OC或者Swift中创建一个上下文环境来执行JavaScript代码。
- JSValue
JSValue的实例是对JavaScript值的封装引用。可以用来在JavaScript和原生代码之间传递数据。
- JSManagedValue
JSManagedValue是对JSValue的包装,使用JSManagedValue不会引起循环引用。
- JSExport
JSExport将遵守该协议的方法、属性等导出给JavaScript使用。
功能演示
UIWebView
- 采用拦截URL请求的方式
单纯的采用JS调用原生方法.
html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js和原生的相互调用</title>
</head>
<body>
<h2>js和原生的相互调用</h2>
<button onclick="handleCallOCMethod()" style="border: 1px solid black">调用原生方法</button>
</body>
</html>
<script>
function handleCallOCMethod() {
// 方法一:
let url = "openVC://test?title=titelData&name=nameData";
var iFrame;
iFrame = document.createElement("iframe");
iFrame.setAttribute("src", url);
iFrame.setAttribute("style", "display:none;");
iFrame.setAttribute("height", "0px");
iFrame.setAttribute("width", "0px");
iFrame.setAttribute("frameborder", "0");
document.body.appendChild(iFrame);
// 发起请求后这个 iFrame 就没用了,所以把它从 dom 上移除掉
iFrame.parentNode.removeChild(iFrame);
iFrame = null;
// 方法二:
location.href = "openVC://test?title=titelData&name=nameData";
}
</script>
---------------------------------
// 原生核心代码
extension ViewController: UIWebViewDelegate {
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
let url = request.url!
let scheme = url.scheme
let absoluteString = url.absoluteString
let query = url.query
let host = url.host
print("url = \(url), absoluteString = \(absoluteString), scheme = \(scheme), query = \(query), host = \(host)")
if scheme! == "openvc" {
// 根据Url传递的信息处理逻辑
print("拦截js操作,处理原生逻辑")
return false
}
return true
}
}
- JavaScriptCore
利用JavaScriptCore来实现JS调用原生方法,处理操作之后,再用原生调用JS方法。
实现步骤:
1、在html页面中定义需要调用原生的方法、原生回调JS的方法
2、在webViewDidFinishLoad方法中实现html需要调用的方法,获取参数等
3、利用evaluateScript执行回调html的方法
方法一:
// html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js和原生的相互调用</title>
</head>
<body>
<h2>js和原生的相互调用</h2>
<button onclick="handleCallOCMethod()" style="border: 1px solid black">调用原生方法</button>
</body>
</html>
<script>
// js调用原生的方法
function handleCallOCMethod() {
openImagePickerVC('title', 'name');
}
// js调用的html的方法
function handleOCCallJSMothod(param) {
alert('OC调用JS方法 参数 = ' + param);
}
</script>
------------------------
// OC核心代码
- (void)webViewDidFinishLoad:(UIWebView *)webView {
JSContext * context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
__weak typeof(self) weakSelf = self;
context[@"openImagePickerVC"] = ^() {
// 获取js传递过来的参数
NSArray * params = [JSContext currentArguments];
NSLog(@"js 传递的参数 params = %@", params);
[weakSelf handleOtherOperating];
};
}
- (void)handleOtherOperating {
// 其他处理
NSLog(@"原生处理方法 thread = %@", [NSThread currentThread]);
// 回调js方法
JSContext * context = [_webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
[context evaluateScript:@"handleOCCallJSMothod('oc传递的参数')"];
}
@end
方法二:利用JSExport协议来处理
实现步骤:
1、在html页面中定义需要调用原生的方法、原生回调JS的方法
2、创建一个遵守JSExport的协议。提供一些html中需要调用的方法
3、创建一个类来实现这个协议中的方法
4、在webViewDidFinishLoad中利用JSContext将这个类暴露给html
// 创建一个工具类
@protocol JSCallOCProtocol<JSExport>
// 提供给js调用的方法,如果想暴露一些属性也是可以的
- (void)jsCallMethod;
@end
@interface Tools : NSObject<JSCallOCProtocol>
@property (nonatomic, strong) JSContext * context;
@end
#import "Tools.h"
@implementation Tools
- (void)jsCallMethod {
NSLog(@"js 调用 原生方法");
// 如果还想要调用js的方法就需要拿到webView的JSContext
[self.context evaluateScript:@"handleOCCallJSMothod('oc传递的参数')"];
}
@end
// html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js和原生的相互调用</title>
</head>
<body>
<h2>js和原生的相互调用</h2>
<button onclick="handleCallOCMethod()" style="border: 1px solid black">调用原生方法</button>
</body>
</html>
<script>
// js调用原生的方法
function handleCallOCMethod() {
// 调用tools提供的方法
tools.jsCallMethod();
}
// js调用的html的方法
function handleOCCallJSMothod(param) {
alert('OC调用JS方法 参数 = ' + param);
}
</script>
// 原生核心代码
- (void)webViewDidFinishLoad:(UIWebView *)webView {
JSContext * context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
Tools * tools = [Tools new];
tools.context = context;
// 方式一
context[@"tools"] = tools;
// 方式二
// [context setObject:tools forKeyedSubscript:@"tools"];
}
Swift版本:
// Tools.swift
import UIKit
import JavaScriptCore
@objc protocol SwiftTools: JSExport {
func jsCallMethod(_ param: String)
}
class Tools: NSObject, SwiftTools {
var context: JSContext?
func jsCallMethod(_ param: String) {
// 如果还想要调用js的方法就需要拿到webView的JSContext
print("js 调用 原生方法 param = \(param)")
_ = context?.evaluateScript("handleOCCallJSMothod('swift传递的参数')")
}
}
// html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js和原生的相互调用</title>
</head>
<body>
<h2>js和原生的相互调用</h2>
<button onclick="handleCallOCMethod()" style="border: 1px solid black">调用原生方法</button>
</body>
</html>
<script>
// js调用原生的方法
function handleCallOCMethod() {
swiftTools.jsCallMethod('name');
}
function handleOCCallJSMothod(param) {
alert('原生调用JS方法 参数 = ' + param);
}
</script>
// 核心代码
func webViewDidFinishLoad(_ webView: UIWebView) {
// 获取上下文
let jsContext = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as? JSContext
let tools = Tools()
tools.context = jsContext
jsContext?.setObject(tools, forKeyedSubscript: "swiftTools" as NSCopying & NSObjectProtocol)
}
WKWebView
-
一些基础知识介绍
-
配置信息
WKWebViewConfiguration用来初始化WKWebView的配置。
WKPreferences配置webView能否使用JS或者其他插件等
WKUserContentController用来配置JS交互的代码 -
UIDelegate
UIDelegate用来控制WKWebView中一些弹窗的显示(alert、confirm、prompt)。
JS端调用alert()方法会触发下面这个方法,并且通过message获取到alert的信息- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;
- WKNavigationDelegate
WKNavigationDelegate用来监听网页的加载情况,包括是否允许加载,加载失败、成功加载等一些列代理方法。
-
-
拦截URL的方式处理
// html 参考上面UIWebView例子代码
- (void)viewDidLoad {
[super viewDidLoad];
WKWebViewConfiguration * configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = [WKUserContentController new];
WKPreferences * preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
preferences.minimumFontSize = 50.0;
configuration.preferences = preferences;
_wkWebView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:configuration];
[self.view addSubview:_wkWebView];
NSURL * urlstring = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html"];
[_wkWebView loadRequest:[[NSURLRequest alloc] initWithURL:urlstring]];
_wkWebView.navigationDelegate = self;
_wkWebView.UIDelegate = self;
}
// 核心代码
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL * url = navigationAction.request.URL;
NSString * scheme = url.scheme;
NSString * query = url.query;
NSString * host = url.host;
NSLog(@"scheme = %@, query = %@, host = %@", scheme, query, host);
if ([scheme isEqualToString:@"openvc"]) {
[self handleJSMessage];
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
decisionHandler(WKNavigationActionPolicyAllow);
}
- (void)handleJSMessage {
// 回调JS方法
[_wkWebView evaluateJavaScript:@"handleOCCallJSMothod('123')" completionHandler:^(id _Nullable x, NSError * _Nullable error) {
NSLog(@"x = %@, error = %@", x, error.localizedDescription);
}];
}
#pragma mark - WKUIDelegate
// 处理JS中回调方法的alert方法
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
NSLog(@"message = %@", message);
UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"温馨提示" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
completionHandler();
}
- MessageHandler方式处理
步骤:
1.在原生代码中利用userContentController添加JS端需要调用的原生方法
2.实现WKScriptMessageHandler协议中唯一一个方法
3.在该方法中根据message.name获取调用的方法名做相应的处理,通过message.body获取JS端传递的参数
4.在JS端window.webkit.messageHandlers.methodName.postMessage([“name”,“zhangdan”,“age”, 18])调用该方法
代码如下:
// html
// js调用原生的方法
function handleCallOCMethod() {
// callMethod是原生定义接收的方法名
window.webkit.messageHandlers.callMethod.postMessage(["name","zhangdan","age", 18]);
}
function handleOCCallJSMothod(param) {
alert('原生调用JS方法 参数 = ' + param);
}
// 原生代码
// 添加JS端调用的代码
[configuration.userContentController addScriptMessageHandler:self name:@"callMethod"];
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSLog(@"message = %@", message.name);
NSLog(@"params = %@", message.body);
if ([message.name isEqualToString:@"callMethod"]) {
// 其他处理,如果要回调JS端方法,依然采用- (void)evaluateJavaScript: completionHandler:方法,和上面一样;
}
}
// 注意:在合适的地方将添加的方法移除,防止循环引用
[_wkWebView.configuration.userContentController removeScriptMessageHandlerForName:@"callMethod"];
swift版本代码
class WkWebViewViewController: UIViewController {
private let methodName = "callNativeMethod"
private lazy var wkWebView: WKWebView = {
let configuration = WKWebViewConfiguration()
configuration.userContentController = WKUserContentController()
configuration.userContentController.add(self, name: methodName)
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = true
preferences.minimumFontSize = 50.0
configuration.preferences = preferences
let wkWebView = WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
view.addSubview(wkWebView)
let urlstring = Bundle.main.url(forResource: "index-wkWebView", withExtension: "html")
wkWebView.load(URLRequest(url: urlstring!))
wkWebView.navigationDelegate = self;
wkWebView.uiDelegate = self;
return wkWebView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(wkWebView)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
wkWebView.configuration.userContentController.removeScriptMessageHandler(forName: methodName)
}
deinit {
print("deinit")
}
private func handleJSMessage() {
wkWebView.evaluateJavaScript("handleOCCallJSMothod('123')") { (x, error) in
print("x = \(x ?? ""), error = \(error?.localizedDescription ?? "")")
}
}
}
extension WkWebViewViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
let params = message.body
if methodName == message.name {
print("原生处理 params = \(params)")
self.handleJSMessage()
}
}
}
extension WkWebViewViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
let url = navigationAction.request.url!
let scheme = url.scheme ?? ""
let query = url.query ?? ""
let host = url.host ?? ""
print("url = \(url), scheme = \(scheme), query = \(query), host = \(host)")
if scheme == "openvc" {
// 根据Url传递的信息处理逻辑
print("拦截js操作,处理原生逻辑")
self.handleJSMessage()
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
}
extension WkWebViewViewController: WKUIDelegate {
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
let alert = UIAlertController(title: "温馨提示", message: message, preferredStyle: UIAlertControllerStyle.alert)
alert .addAction(UIAlertAction(title: "确定", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
completionHandler()
}
}
利用第三方库WebViewJavascriptBridge实现
该框架地址:https://github.com/marcuswestin/WebViewJavascriptBridge
根据GitHub的使用方法就能明白该框架的使用了。该框架的实现原理也是对URL进行拦截来实现的。具体详见代码。
// <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebViewJavascriptBridge</title>
</head>
<body>
<h1>WebViewJavascriptBridge</h2>
<button onclick="handleCallOCMethodUrl()" style="border: 1px solid black">调用原生方法-拦截url</button>
</body>
</html>
<script>
// 固定代码
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
setupWebViewJavascriptBridge(function(bridge) {
// 注册原生原生需要调用的js方法
bridge.registerHandler('jsMethod', function(data, responseCallback) {
console.log("原生传递的参数:", data)
responseCallback("js回调的参数")
})
})
function handleCallOCMethodUrl() {
// js 调用原生的方法
WebViewJavascriptBridge.callHandler('nativeMothod', {'js': '传递数据'}, function(response) {
alert('原生回调返回的数据:' + response);
})
}
</script>
// 原生代码
class JavascriptBridgeViewController: UIViewController {
private var bridge: WebViewJavascriptBridge!
private lazy var wkWebView: WKWebView = {
let configuration = WKWebViewConfiguration()
configuration.userContentController = WKUserContentController()
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = true
preferences.minimumFontSize = 50.0
configuration.preferences = preferences
let wkWebView = WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
view.addSubview(wkWebView)
let urlstring = Bundle.main.url(forResource: "index-JavascriptBridge", withExtension: "html")
wkWebView.load(URLRequest(url: urlstring!))
wkWebView.uiDelegate = self
return wkWebView
}()
override func viewDidLoad() {
super.viewDidLoad()
testWkWebView()
}
private func testWebView() {
let webView = UIWebView(frame: UIScreen.main.bounds)
let path = Bundle.main.path(forResource: "index-JavascriptBridge", ofType: "html")
let url = URL.init(fileURLWithPath: path!)
webView.loadRequest(URLRequest(url: url))
view.addSubview(webView)
bridge = WebViewJavascriptBridge(webView)
bridge.registerHandler("nativeMothod") { (data, callback) in
print("js 传递过来的数据 = \(String(describing: data))")
callback!("回调")
}
}
private func testWkWebView() {
view.addSubview(wkWebView)
bridge = WebViewJavascriptBridge(forWebView: wkWebView)
bridge.registerHandler("nativeMothod") { (data, callback) in
print("js 传递过来的数据 = \(String(describing: data))")
callback!("回调")
}
}
@IBAction func nativeCallJsMethod(_ sender: Any) {
bridge.callHandler("jsMethod", data: ["name" : 123]) { (response) in
print("response = \(response ?? "")")
}
}
}
extension JavascriptBridgeViewController: WKUIDelegate {
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
let alert = UIAlertController(title: "温馨提示", message: message, preferredStyle: UIAlertControllerStyle.alert)
alert .addAction(UIAlertAction(title: "确定", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
completionHandler()
}
}
总结
上面的几种方式基本就包括了常规的JS和原生之间的交互方式.
代码地址