iOS Native与JS通信:JSBridge

文章目录

  • 一、简介
  • [二、JS 调用 Native](#二、JS 调用 Native)
    • [1.使用 URL Scheme](#1.使用 URL Scheme)
    • [2.使用 JavaScriptCore (iOS 7+)](#2.使用 JavaScriptCore (iOS 7+))
    • [3.使用 WKWebView 和 WKScriptMessageHandler (iOS 8+)](#3.使用 WKWebView 和 WKScriptMessageHandler (iOS 8+))
  • [三、Native 调用 JS](#三、Native 调用 JS)
    • [1.使用 UIWebView](#1.使用 UIWebView)
    • [2.使用 WKWebView](#2.使用 WKWebView)
    • [3.使用 JavaScriptCore (iOS 7+)](#3.使用 JavaScriptCore (iOS 7+))

一、简介

对于移动应用程序的开发,有多种技术选型,最基础的是原生开发,后面由于动态化和跨平台的需求,引入了跨端的方案,比如H5、RN。以原生+H5 混合开发模式为例,H5页面经常需要使用到Native端的功能,比如打开二维码扫描、调用本地相册、获取用户信息等,同时Native端也需要向H5页面发送消息、更新状态等。所以需要一种通信机制,来让两端进行通信。这时候就引入了桥(Bridge)的概念。

JSBridge(JavaScript Bridge)是一种设计模式,用于在JavaScript和原生代码(如iOS的Objective-C/Swift或Android的Java/Kotlin)之间建立通信桥梁。通过JSBridge,Web页面中的JavaScript代码可以调用原生功能,原生代码也可以调用JavaScript方法,从而实现Web和原生代码的互操作性。

JSBridge的核心思想是通过特定的通信协议在JavaScript和原生代码之间传递消息和数据(🔗iOS与JS交互)。其工作原理通常包括以下步骤:

  1. JavaScript调用原生代码:
  • JavaScript通过调用特定的接口或注入的对象,发送消息给原生代码。
  • 原生代码接收到消息后,执行相应的操作,并将结果返回给JavaScript。
  1. 原生代码调用JavaScript:
  • 原生代码通过特定的接口或注入的对象,发送消息给JavaScript。
  • JavaScript接收到消息后,执行相应的操作,并将结果返回给原生代码。

二、JS 调用 Native

1.使用 URL Scheme

这种方法通过在 JavaScript 端构建一个特定的 URL,然后在原生端捕获这个 URL 并解析出需要调用的方法和参数。

a.UIWebView

使用 UIWebView 的代理方法 webView:shouldStartLoadWithRequest:navigationType: 来拦截即将加载的请求,并根据请求的URL参数执行相应的逻辑,包括注入JavaScript代码、显示提示信息、处理登录结果等。

JS端:

objectivec 复制代码
function callNativeMethod(method, params) {
    var url = "myapp://" + method + "?" + encodeURIComponent(JSON.stringify(params));
    window.location.href = url;
}

Native端:

objectivec 复制代码
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSURL *url = [request URL];
    if ([[url scheme] isEqualToString:@"myapp"]) {
        NSString *method = [url host];
        NSString *query = [url query];
        NSData *data = [query dataUsingEncoding:NSUTF8StringEncoding];
        NSDictionary *params = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
        
        if ([method isEqualToString:@"someNativeMethod"]) {
            [self someNativeMethod:params];
        }
        return NO;
    }
    return YES;

这里举一个例子:

stringByEvaluatingJavaScriptFromString: 方法返回执行JS脚本的结果,通过这种方式,我们可以在UIWebView中拦截即将加载的请求,注入JavaScript代码,并根据URL参数执行相应的逻辑。这种方法可以用于处理各种需要与Web内容交互的场景,如动态调整页面布局、显示提示信息、处理登录结果等。(UIWebView在iOS 12之后已经被废弃)

b.WKWebView

WKWebView 有两个代理,一个是 WKNavigationDelegate,另一个是 WKUIDelegate。这里是使用WKWebView的代理方法 webView:decidePolicyForNavigationAction:decisionHandler:

JS端:

objectivec 复制代码
function callNative() {
    loadURL("your_func_name://xxx");
}  

Native端:

objectivec 复制代码
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    NSURL *url = navigationAction.request.URL;
    // 与约定好的函数名作比较
    if ([[url scheme] isEqualToString:@"your_func_name"]) {
        // just do it
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    
    decisionHandler(WKNavigationActionPolicyAllow);
}

//decisionHandler 是当你的应用程序决定是允许还是取消导航时,要调用的代码块。 
//该代码块使用单个参数,它必须是枚举类型 WKNavigationActionPolicy 的常量之一。如果不调用 decisionHandler 会引起 crash。

这里简单介绍一下 WKUIDelegate 中的代理方法 webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:

webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler: 是 WKUIDelegate 协议中的一个方法。当网页中通过 JavaScript 调用 alert 方法时,WebKit 会调用这个委托方法。你可以在此方法中自定义显示一个警告框并在用户点击确定按钮后执行相应的操作。也就是说,可以将 JS 端调用 alert 方法视作向 Native 发送一个消息,Native 接受到这个消息后实现一些自定义的行为,但这种行为一般都是对警告框进行操作。

2.使用 JavaScriptCore (iOS 7+)

iOS 7 有了 JavaScriptCore 专门用来做 Native 与 JS 的交互。我们可以在 webview 完成加载之后获取 JSContext,然后利用 JSContext 将 JS 中的对象引用过来用 Native 代码对其作出解释或响应。先贴个文档🔗深入理解JSCore

JS端:

objectivec 复制代码
function callNative() {
		native.showAlert('Hello from JavaScript!');
}

Native端:

objectivec 复制代码
#import <UIKit/UIKit.h>
#import <JavaScriptCore/JavaScriptCore.h>

// 定义一个协议,声明可以被 JavaScript 调用的方法
@protocol JSExportProtocol <JSExport>
- (void)showAlert:(NSString *)message;
@end

@interface ViewController : UIViewController <JSExportProtocol>

@property (nonatomic, strong) UIWebView *webView;
@property (nonatomic, strong) JSContext *jsContext;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化并配置 UIWebView
    self.webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.webView];    
    
    self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; // 获取 JavaScript 上下文
    
    self.jsContext[@"native"] = self;  // 绑定原生对象到 JavaScript 上下文
}

// 实现协议方法,供 JavaScript 调用
- (void)showAlert:(NSString *)message {/*......*/}
@end

3.使用 WKWebView 和 WKScriptMessageHandler (iOS 8+)

当应用程序需要一种方法来响应网页视图中的JavaScript消息时,请采用WKScriptMessageHandler协议。当JavaScript代码发送一个特定目标的消息到消息处理器时,WebKit将调用处理器的userContentController:didReceiveScriptMessage:方法。使用该方法来实现响应。例如,可以根据网页内容的更改来更新应用程序的其他部分。

objectivec 复制代码
@protocol WKScriptMessageHandler

如何添加一个消息处理器呢?WKUserContentController 类有一个方法:

objectivec 复制代码
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
//scriptMessageHandler:实现了 WKScriptMessageHandler 协议的对象,该对象会处理来自 JavaScript 的消息。
//name:消息的名称,JavaScript 通过这个名称发送消息。
//The name of this function is window.webkit.messageHandlers.<name>.postMessage(<messageBody>),
//where <name> corresponds to the value of this parameter. For example, if you specify the string MyFunction, 
//the user content controller defines the window.webkit.messageHandlers.MyFunction.postMessage() function in JavaScript.

举个例子:

objectivec 复制代码
@interface ViewController () <WKScriptMessageHandler>
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // 配置 WebView
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    WKUserContentController *userContentController = [[WKUserContentController alloc] init];
    [userContentController addScriptMessageHandler:self name:@"messageHandler"]; //----------添加脚本处理器-----------------
    configuration.userContentController = userContentController;
    WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
    [self.view addSubview:webView];
}

//WKScriptMessageHandler 协议方法,在接收到脚本信息时触发 //------------我们需要实现它,处理来自 JavaScript 的消息-------------------
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"messageHandler"]) {
        // ......
        NSLog(@"Received message from JavaScript: %@", message.body);
    }
}
@end

//在JS中按下面方式调用就可以了
window.webkit.messageHandlers.messageHandler.postMessage({body: 'Hello, world!'});

这里解释一下 WKUserContentController 和 WKWebView 的关系:

在 WKWebView 的初始化函数中有一个入参 configuration,它的类型是 WKWebViewConfiguration。WKWebViewConfiguration 中包含一个属性 userContentController,这个 userContentController 就是 WKUserContentController 类型的实例,我们可以用这个 userContentController 来添加不同名称的脚本处理器。

objectivec 复制代码
@interface WKWebView : UIView
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
@end

@interface WKWebViewConfiguration : NSObject <NSSecureCoding, NSCopying>
@property (nonatomic, strong) WKUserContentController *userContentController;
@end

这里需要注意一下循环引用的问题:scriptMessageHandler 入参会被强引用,那么如果你把当前 WKWebView 所在的 UIViewController 作为第一个入参,这个 viewController 被他自己所持有的 webview.configuration. userContentController 所持有,就会造成循环引用。

所以一般情况下我们的代码会在 viewWillAppear 和 viewWillDisappear 成对儿的添加和删除 MessageHandler:

objectivec 复制代码
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self.webview.configuration.userContentController addScriptMessageHandler:self name:@"YourFuncName"];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.webview.configuration.userContentController removeScriptMessageHandlerForName:@"YourFuncName"];
}

三、Native 调用 JS

1.使用 UIWebView

在 UIWebView 中,可以使用 stringByEvaluatingJavaScriptFromString: 方法,来返回运行JavaScript脚本的结果。它是一个同步方法,会阻塞当前线程!

2.使用 WKWebView

在 WKWebView 中,可以使用 evaluateJavaScript:completionHandler: 方法来调用 JavaScript。因为它异步执行 JavaScript 代码,所以不会阻塞主线程,并在完成后通过回调处理结果,它的回调代码块总是在主线程中运行。

objectivec 复制代码
- (void)evaluateJavaScript:(NSString *)javaScriptString 
         completionHandler:(void (^)(id, NSError *error))completionHandler;

3.使用 JavaScriptCore (iOS 7+)

JavaScriptCore 框架允许直接在 Objective-C 和 JavaScript 之间互相调用方法。

Native端:

objectivec 复制代码
#import <JavaScriptCore/JavaScriptCore.h>

@interface ViewController ()
@property (nonatomic, strong) JSContext *jsContext;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
}

- (void)callJavaScriptFunction {
    JSValue *jsFunction = self.jsContext[@"javascriptFunctionName"];
    [jsFunction callWithArguments:@[@"param1", @"param2"]];
}
@end

JS端:

objectivec 复制代码
function javascriptFunctionName(param1, param2) {
    console.log("Called from Native with params:", param1, param2);
}
相关推荐
远望清一色5 分钟前
基于MATLAB的实现垃圾分类Matlab源码
开发语言·matlab
GIS程序媛—椰子11 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
confiself15 分钟前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
DogEgg_00117 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端20 分钟前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x24 分钟前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
木舟100924 分钟前
ffmpeg重复回听音频流,时长叠加问题
前端
XiaoLeisj26 分钟前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee
杜杜的man30 分钟前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*31 分钟前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go