论 iOS 开发中 JS 与 Native 的交互方式

96
CoCodeDZ
0.1 2016.03.08 17:01* 字数 1447

前言

在 iOS 开发中,JS 与 Native 的交互分为两种,第一种是 Native 调 JS,即通过在 Native 代码中执行 JS 达到在 webkit 控件中展现相应 JS 代码的效果;另一种就是 JS 调用 Native ,通过 web 前段 JS 的执行来调用 Native 本地的方法,用以实现例如开启照相机、数据持久化等等只能通过 Native 代码实现的效果。

目前进行 JS 和 Native 交互主要有两种方式,下面进行一一介绍:

一、WebView 方法/代理方法

通常来说,iOS 中实现加载 web 页面主要有两种控件,UIWebView 和 WKWebview,两种控件对应具体的实现方法不同,我们在这里分开进行介绍:

UIWebView控件

  • Native 调用 JS:

在 Native 中执行 JS 语句非常简单, JS 作为脚本语言它的执行需要解释器的存在,即浏览器,所以 UIWebView 作为浏览器控件,提供了 native 调用 JS 的对象方法:

//script 是要执行的 JS 语句
//返回值为 JS 执行结果,如果 JS 执行失败则返回 nil,如果 JS 执行没有返回值,则返回值为空字符串
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

这里编写了一个 demo 仅供参考:

- (void)webViewDidFinishLoad:(UIWebView*)webView
{
    NSString* str = [self.webView stringByEvaluatingJavaScriptFromString:@"pageDidLoad()"];
    NSLog(@"%@", str);
}

当 WebView 加载完毕的时候调用 JS 中的 pageDidLoad 方法,并在控制台打印 JS 的执行结果。

  • JS 调用 Native:
    使用 WebView 方法/代理方法完成 JS 调用 Native 要稍微复杂一点,需要 Native前端和 web 前端的良好配合,主要原理是通过 UIWebVIew 的代理方法截取 web 前端的跳转请求,通过识别与 web 前端约定好的自定义协议头来判断本次请求是否为 JS 调用 Native 的请求,来调用对应的 Native 方法。
    其中涉及到的 UIWebView 代理方法为:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

下面通过例子来进行演示:

JavaScript 代码:

function btnOnClickBaidu() {
        var url = "http://www.baidu.com";
        alert("马上跳转的页面是:" + url);
        window.location.href = url;
    }
    function btnOnClickNative() {
        var url = "DZBridge://printSomeWords";
        alert("马上跳转的页面是:" + url);
        window.location.href = url;
    }
    function btnOnClickNativeWithConfig() {
        var url = "DZBridge://printSomeWords?{\"string\":\"Hello World\"}";
        alert("马上跳转的页面是:" + url);
        window.location.href = url;
    }
    function pageDidLoad() {
        alert("页面加载完毕!");
        return 11;
    }

OC代码:

- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
    //dzbridge 为约定好的协议头,如果是,则页面不进行跳转
    if ([request.URL.scheme isEqualToString:@"dzbridge"]) {
        //截取字符串来判断是否存在参数
        NSArray<NSString*>* arr = [request.URL.absoluteString componentsSeparatedByString:@"?"];
        if (arr.count > 1) {
            NSString* str = [arr[1] stringByRemovingPercentEncoding];
            NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options:0 error:NULL];
            NSLog(@"%@", dict[@"string"]);
        }
        else {
            NSLog(@"没有参数的打印");
        }
        return NO;
    }
    //不是自定义协议头,跳转页面
    return YES;
}

WKWebView控件

iOS8 以后,苹果推出了新框架 WKWebKit, 其中提供了可以替换 UIWebView 的组件 WKWebView。原来 UIWebView 的各种问题得到了改善,速度更快了,占用内存少了(模拟器加载百度与开源中国网站时,WKWebView 占用23M,而UIWebView 占用85M),目前来看,WKWebView 是 App 内部加载网页更佳的选择!
WKWebView 相对 UIWebView 做了较大幅度的重构,将 UIWebViewDelegate 与 UIWebView 重构成了14类与3个协议,因此,在 WKWebView 中进行 JS 与 Native 的交互与 UIWebView 相比也有较大的不同。

  • Native 调用 JS:
    在 WKWebView 中 Native 调用 JS 的方式与 UIWebview 中比较相似,也是通过自己本身的一个对象方法:
// javaScriptString 为待执行的 JS 语句
// completionHandler 为执行 JS 完毕后的回调,block 的第一个参数为执行结果,第二个参数为错误
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;

看下面一个小例子:

#pragma mark----- WKNavigationDelegate -----

- (void)webView:(WKWebView*)webView didFinishNavigation:(WKNavigation*)navigation
{
    [self.webView evaluateJavaScript:@"pageDidLoad()" completionHandler:^(id _Nullable value, NSError* _Nullable error) {
        NSLog(@"%@", value);
    }];
}
  • JS 调用 Native:
    WKWebView 中 JS 调用 Native 与 UIWebView 有着比较大的不同,首先需要介绍几个类(/协议/属性):
  1. WKWebViewConfiguration:是 WKWebView 初始化时的配置类,里面存放着初始化 WK 的一系列属性;
  2. WKUserContentController:为 JS 提供了一个发送消息的通道并且可以向页面注入 JS 的类;
  3. WKScriptMessageHandler:一个协议,协议中只有一个方法,这个方法是页面执行特定 JS 的一个回调,这个特定的 JS 格式为:window.webkit.messageHandlers.<name>.postMessage(<messageBody>)

WKWebViewConfiguration作为 WK 的配置类,其中有一个属性为

@property (nonatomic, strong) WKUserContentController *userContentController;

WKUserContentController的一个实例,WKUserContentController有一个对象方法为:

/*! @abstract Adds a script message handler.
 @param scriptMessageHandler The message handler to add.
 @param name The name of the message handler.
 @discussion Adding a scriptMessageHandler adds a function
 window.webkit.messageHandlers.<name>.postMessage(<messageBody>) for all
 frames.
 */
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;

从苹果给出的注释来看,通过该方法能够添加一个脚本消息的处理器,即(id <WKScriptMessageHandler>)scriptMessageHandler,另外还能发现,添加脚本处理器后,需要在 JS 中添加window.webkit.messageHandlers.<name>.postMessage(<messageBody>)才能起作用。

demo:

// 创建并配置 WKWebView 的相关参数
WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init];
WKUserContentController* userContent = [[WKUserContentController alloc] init];
// self 指代的对象需要遵守 WKScriptMessageHandler 协议
[userContent addScriptMessageHandler:self name:@"test"];
config.userContentController = userContent;

在页面上的 JS 执行window.webkit.messageHandlers.<name>.postMessage(<messageBody>)时,被添加的ScriptMessageHandler就会执行实现的WKScriptMessageHandler协议的方法,例如:

#pragma mark----- WKScriptMessageHandler -----
/**
 *  JS 调用 OC 时 webview 会调用此方法
 *
 *  @param userContentController  webview 中配置的 userContentController 信息
 *  @param message                js 执行传递的消息
 */
- (void)userContentController:(WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage*)message
{
    NSLog(@"%@", message);
}

在代理方法中实现相应的 Native 代码,即完成了 JS 调用 Native 的过程。

二、JavaScriptCore

OS X Mavericks 和 iOS 7 引入了 JavaScriptCore 库,把 WebKit 的 JavaScript 引擎用 Objective-C 封装,提供了简单,快速以及安全的方式接入 JavaScript。

JavaScriptCore中类及协议

  • JSContext:JavaScript 运行的上下文环境
  • JSValue:JavaScript 和 Objective-C 数据和方法的桥梁
  • JSExport:这是一个协议,如果采用协议的方法交互,自己定义的协议必须遵守此协议
  • JSManagedValue:管理数据和方法的类
  • JSVirtualMachine:处理线程相关,使用较少

JavaScript 调用 Native

使用 JavaScriptCore 进行 JS 和 Native 的交互,无论想要实现什么样的效果都需要获得一个有效的 JSContext 实例,即一个有效的 JS 运行的上下文(这一步骤以下不再重复提及)。

  • 获得当前的 JSContext:
    可以在页面加载完毕后,采用 KVC 的方式从webView 中获得,如下:
JSContext* jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
  • 将想要被暴露给 JS 的方法抽象成为一个协议(protocol),该协议需要遵守 JSExport 协议:
@protocol JSObjcDelegate <JSExport>
- (void)callCamera;
- (NSString*)share:(NSString*)shareString;
@end
  • 将要暴露给 JS 的对象的类需要遵守自定义的协议,如上:JSObjcDelegate
  • 将 OC 对象桥接到 JS 环境中,并设置异常处理
// 将本对象与 JS 中的 DZBridge 对象桥接在一起,在 JS 中 DZBridge 代表本对象
[self.jsContext setObject:self forKeyedSubscript:@"DZBridge"];
self.jsContext.exceptionHandler = ^(JSContext* context, JSValue* exceptionValue) {
        context.exception = exceptionValue;
        NSLog(@"异常信息:%@", exceptionValue);
    };
  • 在 JS 中通过 DZBridge 调用本对象暴露出的方法:
var callShare = function() {
        var shareInfo = JSON.stringify({"title": "标题", "desc": "内容", "shareUrl": "http://www.jianshu.com"});
        var str = DZBridge.share(shareInfo);
        alert(str);
    }

Native 调用 JavaScript

  • 第一种方式同 UIWebView 中类似,都是直接执行 JS 字符串,通过 JSContext 执行 JS 代码:
[self.jsContext evaluateScript:@"alert(\"执行 JS\")"];
  • 另一种方式适用于执行 web 页面上已有的方法,通过 JSValue 来调用 JS 中的方法,JSValue 是 JavaScript 中值得一个引用,他可能包装着一个 JavaScript 的方法,通过 callWithArguments: 方法进行调用,例如:
JSValue* picCallback = self.jsContext[@"picCallback"];
[picCallback callWithArguments:@[ @"photos" ]];
iOS中的一些事情
Web note ad 1