JS和OC互调

本文使用了第三方框架:WebViewJavascriptBridge.h

一、obj-c调用javascript的机制

  • UIWebView是iOS最常用的SDK之一,它有一个stringByEvaluatingJavaScriptFromString方法可以将javascript嵌入页面中,通过这个方法我们可以在iOS中与UIWebView中的网页元素交互。
    使用stringByEvaluatingJavaScriptFromString方法,需要等UIWebView中的页面加载完成之后去调用。
    使用例子介绍:
    //获取当前页面的url
    [webView stringByEvaluatingJavaScriptFromString:@"document.location.href"]
    //获取页面title:
    [webview stringByEvaluatingJavaScriptFromString:@"document.title"]
    //表单提交:
    [webView stringByEvaluatingJavaScriptFromString:@"document.forms[0].submit();"]

从以上可以看出stringByEvaluatingJavaScriptFromString不仅可以执行js来获取信息,还可以通过调用js对webView执行操作。 所以可以通过此函数来实现obj-c到js的交互。

备注:** stringByEvaluatingJavaScriptFromString这个方法有个地方需要注意, 算不上bug, 但确实有问题, 需要注意!**
如果stringByEvaluatingJavaScriptFromString执行的是带参数的js函数, 这个参数里面如果带有(\r \n ')等等, js那边收不到这个值, 这些带\的需要转义,



二、 javascript调用obj-c

UIWebView对URL的跳转进行拦截,可以通过自定义协议来捕获请求,在通过obj-c的stringByEvaluatingJavaScriptFromString来获取js对于obj-c函数的调用。

UIWebView对于跳转的拦截方法:
- (BOOL)webView:(UIWebView)webView shouldStartLoadWithRequest:(NSURLRequest)request navigationType:(UIWebViewNavigationType)navigationType {

}

实质上oc与js的通信交互就是发送消息,也即函数调用,只要在交互的过程正确的指定好对方需要调用的函数和参数就ok

  • oc-->js stringByEvaluatingJavaScriptFromString,其参数是一NSString 字符串内容是js代码(这又可以是一个js函数、一句js代码或他们的组合),当js函数有返回值或一句js代码有值返回可通过stringByEvaluatingJavaScriptFromString的返回值获取

  • js-->oc 利用webView的重定向原理(即重新在js中指定document.location的值,此为一url),只要在这个url字符串中按自定义的规则指定好所需调用oc中的函数和参数,然后通过OC中的shouldStartLoadWithRequest函数去捕获处理请求,处理完最后,如果js还想获取一些返回参数的话,同样让oc去通过stringByEvaluatingJavaScriptFromString调用刚js传过来的回调js函数就行,顺道把参数也一起传了。



三、WebViewJavaScriptBridge实现机制

webViewJavaScriptBridge 包含三个文件:

WebViewJavascriptBridge.h

WebViewJavascriptBridge.m

WebViewJavascriptBridge.js.txt

很明显:WebViewJavascriptBridge.js.txt主要用于衔接UIWebView中的web page,而WebViewJavascriptBridge.h/m则主要用于与ObjC的native code打交道。他们作为一个整体,其实起到了一个“桥梁”的作用,这三个文件封装了他们具体的交互处理方式,只开放出一些对外的涉及到业务处理的API,因此你在需要UIWebView与Native code交互的时候,引入该库,则无需考虑太多的交互上的问题。整个的Bridge对你来说都是透明的,你感觉编程的时候,就像是web编程的前端和后端一样清晰。他们使用交互如图所示:

WebViewJavaScriptBridge交互图.png

下面我们对WebViewJavaScriptBridge的实现做出分析。

3.1 WebViewJavaScriptBridge中的初始化
WebViewJavaScriptBridge的初始化过程如下:

1. 初始化需要使用的数据结果

2. 保存registerHandler的函数名与block

3. webView 加载完成载人js代码
bridge初始化流程图.png

3.2 obj-c与js的交互
obj-c与js交互使用的机制是一样的,所以单项调用的处理是混在一起,所以现在按照流程单独梳理,最后在以源码为例来说明。
3.2.1 obj-c调用js过程:

- 1、obj-c保存回调的block,使用唯一码objc_cb_* 标识
- 2、obj-c拼装data(数据),handlerName(调用名),回调标识等信息
- 3、 对信息进行json拼装,及js一些字符的转码
- 4、 执行js端的分发消息的函数
- 5、js执行完成,执行函数块,调用register的responseCallback,发送responseId和responseData
- 6、 js拼装信息到消息队列中,URL重定向;obj-c端拦截请求,执行js代码拉取数据
- 7、obj-c端根据responseId来取得保存的block,执行block

大致流程图如下:

obj-c调用js流程图.png

3.1.2 js调用obj-c的过程:

   - 1、js端保存data与handlerName的映射
   - 2、js端生成唯一码callbackId保存responseCallback映射,并封装callbackId到发送消息中
   - 3、js端封装后的消息放入发送消息队列中, 并执行URL重定向
   - 4、obj-c端拦截请求,判定是自定义协议后,执行js的_fetchQueue()拉取发送的信息
   - 5、obj-c端重新封装responcallback 的block,执行时调用_queueMessage进行消息分发
   - 6、从obj-c端的注册队列中获取handler的block,并执行
   - 7、执行后回调block,封装了responseId和responseData数据,并进行分发
   - 8、数据的特殊字符处理,调用js的函数_handleMessageFromObjC()来传递消息
   - 9、js获取分发的responseId,获取保存在队列中的function

大致的流程图如下:


js调用obj-c流程图.png

3.2 源码解读
3.2.1 obj-c端源码
因js调用涉及到自定义协议的重定向,因此定义了协议及host

#define kCustomProtocolScheme @"wvjbscheme"
#define kQueueHasMessage@"__WVJB_QUEUE_MESSAGE__"

WebViewJavaScriptBridge的初始化因为涉及到OSX端WebView与iOS端UIWebView,故根据不同的系统来做变量的统一定义,如下:

OS X与iOS变量重定义.png

定义block,WVJBResponseCallback定义了回调block,js返回数据后回调。WVJBHandler定义了register的block,用作不同情况下的处理。**

typedef void(^WVJBResponseCallback)(id responseData);
typedef void(^WVJBHandler)(id data,WVJBResponseCallback responseCallback);

obj-c外部接口,用于native调用使用。其中register与callhandler函数封装数据后调用send处理。


obj-c与js交互函数.png

send函数发送消息到js端的源码:

send封装到调用js代码.png

参数data为传递给js函数的参数,可为NSString、NSDictionary等,responseCallback则为obj-C的回调,此回调函数执行流程简述为“js注册函数执行完毕后,会返回带有responseId的消息,最后在obj-c会取出(回调存储在responCallbacks字典中),并执行”,handlerName则为js定义的函数名称。
源码中在_queueMessage方法进行逻辑判断:若在H5頁面或者js资源并未加载完毕时,在obj-C的webview中就调用了js函数,则会把相关的操作(存储为Message格式)存储在startupMessageQueue,等待相关资源加载完毕(即在webview的webViewDidFinishLoad生命周期函数中执行存储在startupMessageQueue的命令数组,执行完毕并清除改队列)再調用js的函数;否則若startupMessageQueue队列为空,则直接执行暴露在js端的webViewJavascriptBridge.handleMessageFromObjC函数,获取被调用的函数名和传人参数,以及在objC的sendData:responseCallback:handlerName中设置的回调函数id—callbackId,最终执行js注册函数,并最终想obj-c端发送“doSend({ responseId:callbackResponseId, responseData:responseData })”格式的消息,待objC接收到消息,解析responseId,执行回调函数。

webView载入H5的delegate函数:


finishLoad函数.png

H5 页面载入完成后,载入js资源,在对开始消息队列中的数据进行处理,并置nil。 在把delegate抛出。

url拦截.png

有重定向请求时,首先判断scheme及host是自定的,使用_flushMessageQueue函数处理。

obj-c端对js端处理.png

这段代码通过回传消息的responseId来判断是回调还是call,来进行不同的解封调用。

3.2.2 js端源码

js端定义的WebViewJavascriptBridge对象:

js端WebViewJavascriptBridge类.png

js端的交互函数,逻辑处理,一个registerHandler和一个callHandler主要封装。_dosend函数进行消息封装放入发送消息队列,在产生一个src(url scheme),供obj-c端shouldStartLoadWithRequest捕捉

消息交互函数.png

obj-c端通过_fetchQueue()获取发送的消息

js拉取发送的消息.png

js中处理来自objc的消息,判断同obj-c获取到js端的消息

_dispatchMessageFromObjC.png

简单代码

  • #import <WebViewJavascriptBridge.h>

oc

//
//  ViewController.m
//  WebViewJSBridgeDemo
//
//  Created by taoyali on 16/8/1.
//  Copyright © 2016年 . All rights reserved.
//

#import "ViewController.h"
#import <WebViewJavascriptBridge.h>

@interface ViewController ()<UIWebViewDelegate>
@property (nonatomic, strong) WebViewJavascriptBridge *bridge;

@property (weak, nonatomic) IBOutlet UIWebView *webView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 2.加载网页
    NSString *indexPath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
    NSString *appHtml = [NSString stringWithContentsOfFile:indexPath encoding:NSUTF8StringEncoding error:nil];
    NSURL *baseUrl = [NSURL fileURLWithPath:indexPath];
    [self.webView loadHTMLString:appHtml baseURL:baseUrl];
    
    // 3.开启日志
    [WebViewJavascriptBridge enableLogging];
    
    // 4.给webView建立JS和OC的沟通桥梁
    self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView];
    [self.bridge setWebViewDelegate:self];
    
    
    /* JS调用OC的API:访问相册 */
    [self.bridge registerHandler:@"openCamera" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"需要%@图片", data[@"count"]);
        
        UIImagePickerController *imageVC = [[UIImagePickerController alloc] init];
        imageVC.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
        [self presentViewController:imageVC animated:YES completion:nil];
    }];
    
    
     /* JS调用OC的API:访问底部弹窗 */
    [self.bridge registerHandler:@"showSheet" handler:^(id data, WVJBResponseCallback responseCallback) {
        UIAlertController *vc = [UIAlertController alertControllerWithTitle:@"你猜我谈不谈?" message:@"不谈不谈,就不谈!!" preferredStyle:UIAlertControllerStyleActionSheet];
        UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
        UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"好的" style:UIAlertActionStyleDefault handler:nil];
        [vc addAction:cancelAction];
        [vc addAction:okAction];
        [self presentViewController:vc animated:YES completion:nil];
    }];
    
    
}

/*  获取用户信息  */
- (IBAction)getUserinfo {
    // 调用JS中的API
    [self.bridge callHandler:@"getUserInfo" data:@{@"userId":@"DX001"} responseCallback:^(id responseData) {
        NSString *userInfo = [NSString stringWithFormat:@"%@,姓名:%@,年龄:%@", responseData[@"userID"], responseData[@"userName"], responseData[@"age"]];
        UIAlertController *vc = [UIAlertController alertControllerWithTitle:@"从网页端获取的用户信息" message:userInfo preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
        UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"好的" style:UIAlertActionStyleDefault handler:nil];
        [vc addAction:cancelAction];
        [vc addAction:okAction];
        [self presentViewController:vc animated:YES completion:nil];
    }];
}

/* 弹框显示消息 */
- (IBAction)showInfo {
    // 调用JS中的API
    [self.bridge callHandler:@"alertMessage" data:@"调用了js中的Alert弹窗!" responseCallback:^(id responseData) {
       
    }];
}

/* 控制界面动态跳转 */
- (IBAction)pushToNewWebSite {
    // 调用JS中的API
    [self.bridge callHandler:@"pushToNewWebSite" data:@{@"url":@"http://m.jd.com"} responseCallback:^(id responseData) {
        
    }];
}

/* 刷新界面 */
- (IBAction)reloadWebPage {
    [self.webView reload];
}

/* 插入新图片 */
- (IBAction)insertImgToWebPage {
    
    NSDictionary *dict = @{
                           @"url" : @"http://upload-images.jianshu.io/upload_images/1268909-0e394c67e1ce6666.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240",
                           };
    // 调用JS中的API
    [self.bridge callHandler:@"insertImgToWebPage" data:dict responseCallback:^(id responseData) {
        
    }];
    
}

@end

html:

<!doctype html>
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
        <style>
            body{
                background-color:paleturquoise;
            }

            button{
                border:0;
                width: 150px;
                height: 35px;
                background-color: orangered;
                color: white;
                font-size: 16px;
                border-radius: 6px;
            }
        </style>
    </head>
    
    <body>
        <h2>JS调用OC中的方法</h2>
        <button id="btn">访问OC相册</button>
        <button id="btn1">调用OC提示窗</button>
        <p></p>
    </body>
    <script>
       // 这段代码是固定的,必须要放到js中
       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 = 'wvjbscheme://__BRIDGE_LOADED__';
            document.documentElement.appendChild(WVJBIframe);
            setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
        }
    
        // 与OC交互的所有JS方法都要在这里注册,才能让OC和JS之间相互调用
       setupWebViewJavascriptBridge(function(bridge) {

           /* JS给OC提供公开的API, 在OC中可以手动调用此API, 并且可以接收OC中传过来的参数,同时可回调OC */

           // 获取用户信息
           bridge.registerHandler('getUserInfo', function(data, responseCallback) {
               console.log("OC中传递过来的参数:", data);
               // 把处理好的结果返回给OC
               responseCallback({"userID":"DX001", "userName":"旋之华", "age":"18", "otherName":"旋之华"})
           });

           //  弹框输出
           bridge.registerHandler('alertMessage', function(data, responseCallback) {
               alert(data);
           });

           //  动态跳转到京东商城
           bridge.registerHandler('pushToNewWebSite', function(data, responseCallback) {
               window.location.href = data.url;
           });

           bridge.registerHandler('insertImgToWebPage', function(data, responseCallback) {

                var img = document.createElement('img');
                img.src = data.url;
                img.width = 200;
                document.body.appendChild(img);

           });


           /* OC给JS提供公开的API, 在JS中可以手动调用此API, 并且可以接收OC中传过来的参数,同时可回调OC */
                                    
           // 调用OC中的打开相册方法
           document.getElementById('btn').onclick = function () {
               bridge.callHandler('openCamera', {'count':'10张'}, function responseCallback(responseData) {
                   console.log("OC中返回的参数:", responseData)
               });
           };

           document.getElementById('btn1').onclick = function () {
               bridge.callHandler('showSheet', '', function responseCallback(responseData) {
                   console.log("OC中返回的参数:", responseData)
               });
           };
       })
    </script>
</html>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容