V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
tunnyios
V2EX  ›  iOS

原生与 H5 的交互

  •  
  •   tunnyios · 2016-07-20 21:29:57 +08:00 · 6655 次点击
    这是一个创建于 3042 天前的主题,其中的信息可能已经有所发展或是发生改变。

    欢迎关注个人博客:tunnycoder.com

    在 iOS 开发中很多时候我们会和 WebView 打交道,并且很多应用都采用了 webView 的混合编程技术。 iOS 的 webview 有 2 个类,一个叫UIWebView,另一个是WKWebView。两者的基础方法都差不多。 WkWebView 是苹果在 iOS8 中引入的新组件,性能更好,如果不需要乡下兼容 iOS8 以前的版本,可以考虑用 WKWebView 。

    iOS7 以前版本中的交互方式

    Objective-C 调用 JavaScript

    iOS7 以前,Objective-C调用JavaScript的方式只有一中,就是通过 UIWebView 对象的stringByEvaluatingJavaScriptFromString:方法。

    /**
     * 执行一段 JavaScript 代码,并返回字符串类型的返回值
     */
    UIWebView *webView = [[UIWebView alloc] init];
    // 返回结果 @"2"
    NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"1+1"];
    
    // 调用 js 方法
    NSString *result2 = [webView stringByEvaluatingJavaScriptFromString:
                            @"alert('hello js');"];
    

    JavaScript 调用 Objective-C

    iOS7 以前,JavaScript调用Objective-C的方法有两种:

    URL 请求拦截

    在 Objective-C 中实现UIWebViewDelegate代理,实现shouldStartLoadWithRequest方法,该方法可以监听到 UIWebView 中发出的 URL 请求,通过与 H5 协商一个 URL 通信协议,来拦截指定的 URL ,做相应的操作。

    HTML

    <div class="suitaqu wrap">
        <div class="stq-headerbar blue"><a href="stqnative://back" class="back"><i class="glyphicon glyphicon-menu-left"></i> </a>关于我们</div>
    

    Objective-C

    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
                                                     navigationType:(UIWebViewNavigationType)navigationType
    {
        NSString *url = request.URL.absoluteString;
        NSRange nativeRange = [url rangeOfString:@"stqnative://"];
        if (nativeRange.location != NSNotFound) { // url 的协议头是 stqnative://
            // do something
            // 返回 NO 以阻止 `URL` 的加载或者跳转
            // return NO;
        }
    
        return YES;
    }
    

    监听 Cookie

    在 UIWebView 中, Objective-C同样可以通过NSHTTPCookieManagerCookiesChangedNotification事件以监听 cookie 的变化,来做相应的处理。当 JavaScript 修改 document.cookie 后, Objective-C 可以通过分析 cookie 以得到信息.

    NSNotificationCenter *center = NSNotificationCenter.defaultCenter;
    [defaultCenter addObserverForName:NSHTTPCookieManagerCookiesChangedNotification
                               object:nil
                                queue:nil
                           usingBlock:^(NSNotification *notification) {
                               NSHTTPCookieStorage *cookieStorage = notification.object;
                               // do something with cookieStorage
                           }];
    

    说明

    • 该方法中Objective-C调用JavaScript代码的时候是同步的JavaScript调用Objective-C代码的时候也是同步的
    - (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
    
    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
    
    • 以上方法当然也适用与 iOS7 以后的版本。

    iOS7 以后版本中的交互方式

    iOS7 中引入了JavaScriptCore, 使得JavaScriptObjective-C更容易相互操作。

    JavaScriptCore 几种类型解释

    • JSContext, JSContext 是代表 JS 的执行环境,通过evaluateScript:方法就可以执行 JS 代码
    • JSValue, JSValue 封装了 JS 与 ObjC 中的对应的类型,以及调用 JS 的 API 等
    • JSExport, JSExport 是一个协议,遵守此协议,就可以定义我们自己的协议,在协议中声明的 API 都会在 JS 中暴露出来,才能调用

    Objective-C 调用 JavaScript

    -(void)webViewDidFinishLoad:(UIWebView *)webView  
    {  
        //网页加载完成调用此方法  
    
        //获取当前 JS 环境
        JSContext *context=[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];  
        NSString *alertJS=@"alert('hello js')"; //准备执行的 js 代码  
        [context evaluateScript:alertJS];//通过 oc 方法调用 js 的 alert  
    
    }
    

    JavaScript 调用 Objective-C

    JavaScript 调用 Objective-C 有两种方法:直接调用 JS注入模型

    直接调用 JS

    HTML

    <input type="button" value="测试 log" onclick="log('测试');" />
    

    Objective-C

    -(void)webViewDidFinishLoad:(UIWebView *)webView  
    {  
        //网页加载完成调用此方法  
    
        //获取当前 JS 环境  
        JSContext *context=[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];   
    
        //其中 log 是 js 的方法名称,赋给是一个 block 里面是 iOS 代码  
        //此方法最终将打印出所有接收到的参数
        context[@"log"] = ^() {  
            NSArray *args = [JSContext currentArguments];  
            for (id obj in args) {  
                NSLog(@"%@",obj);  
            }  
        };  
    }  
    

    注入模型的方式

    此种情况是: HTML 中是通过一个对象来调用 JS 中的方法的,此处就需要用到JSExprot,只要添加了JSExport协议的协议,所规定的方法,变量等就会对 JS 开放,我们就可以通过 JS 调用到。

    HTML

    <a href="#" class="col-xs-6 viewcike" title="2">
      <div class="shadow">
        <div class="thumb" style="background-image:url( http://xxxxxx.jpeg)"></div>
        <div class="bottom">
          <h4 class="text-overflow stq-name">音乐喷泉~</h4>
          <div class="text-overflow dateandlocation">
            <div class="pull-left">大雁塔景区</div>
            <div class="pull-right zancount">1</div></div>
            <div class="text-overflow dateandlocation">
            <div class="pull-left">2016-4-8</div>
            <div class="pull-right plcount">0</div>
          </div>
        </div>
      </div>
    </a>
    

    JavaScript

    i.callNative = function (t, n) {
            return console.log("namespace STQN is ", e.STQN), e.STQN || (e.STQN = {
                say: function (t) {
                    console.log("call native action ", t);
                    var e = JSON.parse(t);
                    return "authError" === e.action ? i.go("login.html") : void 0
                }
            }), e.STQN.say(JSON.stringify({action: t, data: n}))
        }
    
    $(document).on("click", ".viewcike", function (t) {
            t.preventDefault(), STQ.callNative("viewCikeImage", {cikes: i, current: 1 * $(this).attr("title")})
        })
    

    :稍微解释一下此处,这里用的是jQuery Mobile框架,点击事件通过 viewcike 这个 class 来进行绑定,执行相应的方法。调用STQ.callNative方法,一步一步最终调用e.STQN.say(JSON.stringify({action: t, data: n})因此要在当前 JS 环境中,监听STQN这个对象并实现say ** PS:如果 js 是一个参数或者没有参数的话 就比较简单,我们的方法名和 js 的方法名保持一致即可;如果 js 是多个参数的话 我们代理方法的所有变量前的名字连起来要和 js 的方法名字一样 ** 方法。 Objective-C 中需要做的就是,创建一个模型并且规定一个 实现了JSExport协议 的协议,在模型中实现相应的方法。 Objective-C

    #import <Foundation/Foundation.h>
    #import <JavaScriptCore/JavaScriptCore.h>
    
    //首先创建一个实现了 JSExport 协议的协议  
    @protocol STQJSObjectProtocol <JSExport>
    JSExportAs
    (say  /** JSObjectAction 作为 js 方法的别名 */,
     - (void)JSObjectAction:(NSString *)message
     );
    @end
    
    @protocol STQJSObjectDelegate <NSObject>
    @optional
    /**
     *  监听 js 方法
     *  @param message acton 和 data
     */
    - (void)JSObjectToOCWithMessage:(NSDictionary *)message;
    @end
    
    @interface STQJSObject : NSObject <STQJSObjectProtocol>
    @property (nonatomic, weak) id<STQJSObjectDelegate> delegate;
    @end
    
    
    #import "STQJSObject.h"
    
    @implementation STQJSObject
    - (void)JSObjectAction:(NSString *)message
    {
      //JSON 格式的字符串转成字典
        NSDictionary *dict = [STQUtiltiy dictionaryWithJsonString:message];
        if ([self.delegate respondsToSelector:@selector(JSObjectToOCWithMessage:)]) {
            [self.delegate JSObjectToOCWithMessage:dict];
        }
    }
    
    //控制器中执行
    -(void)webViewDidFinishLoad:(UIWebView *)webView  
    {  
      //获取当前 JS 环境
      _content = [_detailWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
      // 打印异常,由于 JS 的异常信息是不会在 OC 中被直接打印的,所以我们在这里添加打印异常信息,
      _content.exceptionHandler =
      ^(JSContext *context, JSValue *exceptionValue)
      {
          context.exception = exceptionValue;
          NSLog(@"%@", exceptionValue);
      };
    
      //获取 JS 事件
      STQJSObject *testJO=[STQJSObject new];
      testJO.delegate = self;
      _content[@"STQN"]=testJO;
    }
    
    #pragma mark - jsModel 代理事件
    - (void)JSObjectToOCWithMessage:(NSDictionary *)message
    {
      //在代理中执行相应的操作,应注意改方法是异步的,不是主线程执行,因此如果需要界面操作,需要 GCD 回主线程操作。
        NSLog(@"delegate msg is %@, current is %d", message, [NSThread currentThread].isMainThread);
    }
    

    :

    • 代理中是异步执行的,如果在需要修改界面或者跳转之类的,需要通过 GCD 回主线程操作。
    • 如果 JS 函数中的参数又包含一个函数,则需要在 Objective-C 中用JSValue接收。即:STQ.callNative("action" : "viewCikeImage", "callFun":function(string){alert'string'})参数需要用 JSValue 来接收。*(PS:当然 JSValue 也可以接收普通的键值对儿参数)*
    JSExportAs
    (callNative,
     -(void) callNative:(JSValue*)value
     );
    
    //在.m 文件中,实现 callNative 方法
    -(void) callNative:(JSValue*)value
    {
    NSString * text = [value valueForProperty:@"action"];//打印"viewCikeImage"
    JSValue * func =  [value valueForProperty:@"callFun"]; //这里是 JS 参数中的 func;
    
    //调用这个函数
    [func callWithArguments:@[@"参数"]];
    }
    

    UIWebView 和 WKWebView

    UIWebView 内存占用巨大, iOS8 中苹果给出了一个新的高性能 WebView 解决方案,使用Nitro JavaScript引擎,性能、稳定性、功能都得到大幅提升。WKWebView不支持JavaScriptCore的方式但提供message handler的方式为JavaScriptObjective-C通信.

    关于 WKWebView 部分,下次再写~。~

    未完~待续 ING...

    10 条回复    2016-07-23 14:39:17 +08:00
    param
        1
    param  
       2016-07-20 21:34:42 +08:00
    我仿佛又听到有人在背后偷偷 @我
    tuimaochang
        2
    tuimaochang  
       2016-07-20 22:45:27 +08:00
    贴代码的好评!
    tuimaochang
        3
    tuimaochang  
       2016-07-20 22:48:37 +08:00
    已 rss
    dangyuluo
        4
    dangyuluo  
       2016-07-20 23:09:24 +08:00
    不错。
    BenX
        5
    BenX  
       2016-07-20 23:36:53 +08:00
    eddiechen
        6
    eddiechen  
       2016-07-21 10:06:59 +08:00
    赞!
    lwbjing
        7
    lwbjing  
       2016-07-21 10:31:16 +08:00
    姿势贴,感谢分享...
    qq2511296
        8
    qq2511296  
       2016-07-21 10:44:09 +08:00
    666 不错的文章
    fish19901010
        9
    fish19901010  
       2016-07-21 17:13:42 +08:00
    我们现在就是用这个方式自己做 Hybird 。
    jinzhe
        10
    jinzhe  
       2016-07-23 14:39:17 +08:00
    WKWebView 有个问题就是用 f7 框架的时候无法 ajax 加载 html
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3306 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 12:18 · PVG 20:18 · LAX 04:18 · JFK 07:18
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.