JS和原生之间的相互调用总结

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wj610671226/article/details/82860198

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和原生之间的交互方式.
代码地址

猜你喜欢

转载自blog.csdn.net/wj610671226/article/details/82860198
今日推荐