react native之WebView中注入js接口(jsBridge)

前言

在react native之前,大都采用hybird方案,目前WebView已经是app中不可或缺的一部分,采用react native之后依然需要支撑。react native核心库中就带有WebView的封装,但只是最基础支撑,要扩展WebView的功能,手段之一就是注入js,俗称jsBridge。

react native需要iOS7以上系统支撑,因此注入js有两种方案:

  1. 通过Request Url截获解析。这是在iOS7之前采用的方式。
  2. 通过系统提供的javascriptCore通信方式。

这里我们讨论第二种方案,如果你对jsBridge不太熟悉,可以看这篇H5与native之间的通信。如果对javascriptCore不熟悉,可以看这个javascriptCore详解

既然已经有成熟的方案,为什么还要写这篇文章?

还是那句话,最好不要修改react native原有代码,对以后的版本控制以及维护都不好,下面就来看看如何不修改react native实现需求,先放出项目地址

JS注入实现

要给WebView注入js,需要WebView资源加载完毕时,获取WebView的JSContext。也就是在- (void)webViewDidFinishLoad:(UIWebView *)webView回调方法中通过[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]获取上下文,然后就可以为所欲为了。

那么关键点还是在:如何不侵入react native内部源码

这时候category和swizzling隆重登场。首先使用swizzling替换原有webViewDidFinishLoad方法:

+(void)load {
  
  RCTSwapInstanceMethods([RCTWebView class], @selector(initWithFrame:), @selector(newInitWithFrame:));
  RCTSwapInstanceMethods([RCTWebView class], @selector(webViewDidFinishLoad:), @selector(newWebViewDidFinishLoad:));
}

然后在新方法中,除了执行原有逻辑之外,再执行js注入:

- (void)newWebViewDidFinishLoad:(UIWebView *)webView {
  
  [self injectWebView: webView];
  [self newWebViewDidFinishLoad: webView];
}

-(void) injectWebView: (UIWebView *) webView {
  
  JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
  if (context == nil) {
    return;
  }
  
  //自定义注入对象
  __weak typeof(self) weakSelf = self;
  context[@"alert"] = ^(NSString *message) {
    
    dispatch_async(dispatch_get_main_queue(), ^{
      
      UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message: message delegate:weakSelf cancelButtonTitle:@"cancel" otherButtonTitles:nil, nil];
      [alert show];
    });
    
  };
}

这里定义了个简单的函数alert,用于调用native方法。下面验证下注入是否有效,在js侧定义WebView,并自动调用alert方法,为简单起见WebView加载本地html:

const HTML = `<html>

  <body>
    <h1>Hello web view</h1>
    <input type="button" value="call native" onclick='buttonAction()'/>
    <script>
      function buttonAction() {
        alert('native button alert');
      };
      alert('native alert');
    </script>
  </body>
</html>`;

  <WebView
    ref={'webView'}
    automaticallyAdjustContentInsets={false}
    // style={styles.webView}
    source={{html: HTML}}
    // source={{url:'https://www.baidu.com'}}
    javaScriptEnabled={true}
    domStorageEnabled={true}
    decelerationRate="normal"
    onNavigationStateChange={this.onNavigationStateChange}
    onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
    // startInLoadingState={true}
    scalesPageToFit={true}/>

看下效果:

单独提一下,iOS中的WebView每次finishLoad时JSContext都会发生变化,所以要在每次load结束时重新注入js。还有一种情况是,在资源加载过程中需要调用native接口,那么就要在WebView创建时同时获取JSContext注入js:

- (instancetype)newInitWithFrame:(CGRect)frame {
  RCTWebView *slf = [self newInitWithFrame: frame];
  
  UIWebView *webView = [slf valueForKey:@"_webView"];
  [self injectWebView: webView];
  return slf;
}

RCTWebView自身提供了一个属性injectedJavaScript,用于资源加载完毕时自动执行的一段js脚本。比如你需要把jsBridge的js侧代码库注入到目标页,可以使用这个属性。

react native的JSContext获取、注入

react native项目自身使用的JSContext与WebView的JSContext不是一回事儿,也就是说你在react native的JSContext中注入接口,WebView是无法访问到的,反之亦然。如果你需要将js接口在react中和WebView中能同时使用,必须两边都要注入。

react native关于JSContext的封装在RCTJSCExecutor中,它实现了一个通知RCTJavaScriptContextCreatedNotification,当JSContext创建完毕,还未加载main.jsbundle时会发送通知,JSContext作为通知的参数传递过来。

于是,一切就简单了,我们注册这个通知获取JSContext:

+(void)load {
  
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(recieveNotification:) name:RCTJavaScriptContextCreatedNotification object:nil];
}

+(void) recieveNotification: (NSNotification *) notification {
  
  JSContext *context = notification.object;
  __weak typeof(self) weakSelf = self;
  
  //这里由js线程调用,所以UI操作需要指定主线程
  context[@"alert"] = ^(NSString *message) {
    
    dispatch_async(dispatch_get_main_queue(), ^{
      
      UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"native alert" delegate:weakSelf cancelButtonTitle:@"cancel" otherButtonTitles:nil, nil];
      
      [alert show];
    });
    
  };
}

功能实现了,不过这里要提醒一句,react native实现了一套js与native模块化通信的机制,虽然我们依然可以给react通过JSContext注入的方式,但不建议这么使用,通过react native提供的模块导出方法才是正道。

关于react native模块的知识,可以参考react native之模块

如果需要理解react native通信机制原理,可以参考react native之OC与js之间交互

这些都偏源码,可能有的读者不喜欢看,后面会单独出一篇react native模块开发的章节。

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

推荐阅读更多精彩内容