对于移动应用程序的开发,有多种技术选型,最基础的是原生开发,后面由于动态化和跨平台的需求,引入了跨端的方案,比如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交互)。其工作原理通常包括以下步骤:
这种方法通过在 JavaScript 端构建一个特定的 URL,然后在原生端捕获这个 URL 并解析出需要调用的方法和参数。
使用 UIWebView 的代理方法 webView:shouldStartLoadWithRequest:navigationType:
来拦截即将加载的请求,并根据请求的URL参数执行相应的逻辑,包括注入JavaScript代码、显示提示信息、处理登录结果等。
JS端:
function callNativeMethod(method, params) {
var url = "myapp://" + method + "?" + encodeURIComponent(JSON.stringify(params));
window.location.href = url;
}
Native端:
- (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之后已经被废弃)
WKWebView 有两个代理,一个是 WKNavigationDelegate,另一个是 WKUIDelegate。这里是使用WKWebView的代理方法 webView:decidePolicyForNavigationAction:decisionHandler:
。
JS端:
function callNative() {
loadURL("your_func_name://xxx");
}
Native端:
- (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 接受到这个消息后实现一些自定义的行为,但这种行为一般都是对警告框进行操作。
iOS 7 有了 JavaScriptCore 专门用来做 Native 与 JS 的交互。我们可以在 webview 完成加载之后获取 JSContext,然后利用 JSContext 将 JS 中的对象引用过来用 Native 代码对其作出解释或响应。先贴个文档深入理解JSCore
JS端:
function callNative() {
native.showAlert('Hello from JavaScript!');
}
Native端:
#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
当应用程序需要一种方法来响应网页视图中的JavaScript消息时,请采用WKScriptMessageHandler协议。当JavaScript代码发送一个特定目标的消息到消息处理器时,WebKit将调用处理器的userContentController:didReceiveScriptMessage:方法。使用该方法来实现响应。例如,可以根据网页内容的更改来更新应用程序的其他部分。
@protocol WKScriptMessageHandler
如何添加一个消息处理器呢?WKUserContentController 类有一个方法:
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
//scriptMessageHandler:实现了 WKScriptMessageHandler 协议的对象,该对象会处理来自 JavaScript 的消息。
//name:消息的名称,JavaScript 通过这个名称发送消息。
//The name of this function is window.webkit.messageHandlers..postMessage(),
//where 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.
举个例子:
@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 来添加不同名称的脚本处理器。
@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:
- (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"];
}
在 UIWebView 中,可以使用 stringByEvaluatingJavaScriptFromString: 方法,来返回运行JavaScript脚本的结果。它是一个同步方法,会阻塞当前线程!
在 WKWebView 中,可以使用 evaluateJavaScript:completionHandler: 方法来调用 JavaScript。因为它异步执行 JavaScript 代码,所以不会阻塞主线程,并在完成后通过回调处理结果,它的回调代码块总是在主线程中运行。
- (void)evaluateJavaScript:(NSString *)javaScriptString
completionHandler:(void (^)(id, NSError *error))completionHandler;
JavaScriptCore 框架允许直接在 Objective-C 和 JavaScript 之间互相调用方法。
Native端:
#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端:
function javascriptFunctionName(param1, param2) {
console.log("Called from Native with params:", param1, param2);
}