美文网首页iOS面试题iosDevSupport
论 iOS 开发中 JS 与 Native 的交互方式

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

作者: CoCodeDZ | 来源:发表于2016-03-08 17:01 被阅读7664次

    前言

    在 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" ]];
    

    相关文章

      网友评论

      • 谁拿浮生伴我一世流年:请问楼主 我调用JS时 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType 这个方法不触发 是因为什么呢
      • 谁拿浮生伴我一世流年:请问楼主 有demo吗
      • zeus2:清晰明了,楼主问下,如果吧方法注册到window.xx.func, 其中JSExport对象是xx,如果是一级对象可以注册,二级对象有什么办法吗?
      • AllenZYQ:window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
        lz 这个方法怎么找?

      本文标题:论 iOS 开发中 JS 与 Native 的交互方式

      本文链接:https://www.haomeiwen.com/subject/lfyzkttx.html