关于标题,我在说what?

稍微捣腾过React Native(后面简称RN)的同学肯定知道,在单纯的IOS项目里面如何开启Chrome远端调试:摇一摇手机,然后开启远端Chrome调试选项,打开Chrome页面,搞定!

但是,问题来了,注意我的标题,我遇到的问题是IOS Extension App,因为项目需求,我需要在IOS 8自定义键盘内嵌入我的RN页面,恩,问题就是这个,TM键盘是相对主App的一个独立系统服务进程(暂且个人理解)!

解决方案

有人说,难道不能摇一摇弹出那个开发者列表项么?我说:“要不你摇一摇,给我看看?”

问题没有那么简单,当然也没有那么复杂,这里我先把解决方案列出来,供给我和一样碰到关于自定义输入法嵌入RN无法调试的同学。

首先,假如我们用到的swift,需要用到RN提供的相关的一些导出JS方法的宏,那么我们需要在项目XXX-Bridge-Header.h里面导入相关的RN头文件:

// React Native
#import "RCTRootView.h"
#import "RCTBridgeModule.h"

然后,我们需要在项目建立一个关于导出给JS使用的一个开启,或者关闭调试的RN module,取名叫做ReactNativeDevManager.swift:

@objc(ReactNativeDevManager)
class ReactNativeDevManager : NSObject {

    // 保证导出方法在主线程调用
    func methodQueue() -> dispatch_queue_t {
        return dispatch_get_main_queue()
    }

    // 给js可以操作设置user default的方法
    @objc func setRemoteChromeDevEnabled(enabled: Bool) -> Void {
        let defaults = NSUserDefaults.standardUserDefaults()
        let settings = NSMutableDictionary.init(dictionary: defaults.dictionaryForKey("RCTDevMenu")!)
        if enabled {
            settings.setValue("RCTWebSocketExecutor", forKey: "executorClass")
        } else {
            settings.setValue(nil, forKey: "executorClass")
        }
        defaults.setValue(settings, forKey: "RCTDevMenu")
    }
}

为了导出这个swift提供出来的module,需要再建立一个.m文件作为桥接,这里我们在同级目录下建立一个ReactNativeDevManagerBridge.m

#import "RCTBridgeModule.h"

@interface RCT_EXTERN_MODULE(ReactNativeDevManager, NSObject)

RCT_EXTERN_METHOD(setRemoteChromeDevEnabled: (BOOL) enabled)

@end

OK,最后,我们回归我们熟悉的JSX文件,在我们的index.ios.js里面来

import { NativeModules } from 'react-native';  
NativeModules.ReactNativeDevManager.setRemoteChromeDevEnabled(true);  

到这里,你再回到你的项目里面,重新运行Xcode编译,模拟器下弹出你定义的系统键盘后,打开你心爱的Chrome到RN的调试页面,Wa See~~,一切是那么和谐!(请以OC开发为主的程序员,但是还不知道这里在干啥的,移步这里,仔细读读哈)

回归问题本身

有人说,没有Chrome调试,我也能开发啊,我把页面放在App里面调试完成了,再嵌入就好啦!恩,请说这话的人先靠边站站,我不想和你说话。

看到解决方案的人,懂一点IOS开发的,立马会明白,RN实际是会读取当前app的NSUserDefault的STD版本,这个玩意有点像前端的Cookie,属于轻持久化的用户配置,所以我们找到对应的key值,修改成RN调试时需要的value,那么RN在读入的时候,就会把一切事情帮你默默的搞定。就像摇一摇弹出列表一样,那个按钮在底层做的操作就是updateSettings。

这里贴一段RN的源代码哈,我们可以看看他在更新设置时,做了些什么鬼

- (void)updateSettings:(NSDictionary *)settings
{
  [_settings setDictionary:settings];

  // Fire handlers for items whose values have changed
  for (RCTDevMenuItem *item in _extraMenuItems) {
    if (item.key) {
      id value = settings[item.key];
      if (value != item.value && ![value isEqual:item.value]) {
        item.value = value;
        [item callHandler];
      }
    }
  }

  self.shakeToShow = [_settings[@"shakeToShow"] ?: @YES boolValue];
  self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue];
  self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue];
  self.hotLoadingEnabled = [_settings[@"hotLoadingEnabled"] ?: @NO boolValue];
  self.showFPS = [_settings[@"showFPS"] ?: @NO boolValue];
  self.executorClass = NSClassFromString(_executorOverride ?: _settings[@"executorClass"]);
}

我们看到最后一段:

self.executorClass = NSClassFromString(_executorOverride ?: _settings[@"executorClass"]);  

这个executorClass又是what?

Class jsDebuggingExecutorClass = NSClassFromString(@"RCTWebSocketExecutor");  
if (!jsDebuggingExecutorClass) {  
  [items addObject:[RCTDevMenuItem buttonItemWithTitle:[NSString stringWithFormat:@"%@ Debugger Unavailable", _webSocketExecutorName] handler:^{
    UIAlertView *alert = RCTAlertView(
      [NSString stringWithFormat:@"%@ Debugger Unavailable", _webSocketExecutorName],
      [NSString stringWithFormat:@"You need to include the RCTWebSocket library to enable %@ debugging", _webSocketExecutorName],
      nil,
      @"OK",
      nil);
    [alert show];
  }]];
} else {
  BOOL isDebuggingJS = _executorClass && _executorClass == jsDebuggingExecutorClass;
  NSString *debuggingDescription = [_defaults objectForKey:@"websocket-executor-name"] ?: @"Remote JS";
  NSString *debugTitleJS = isDebuggingJS ? [NSString stringWithFormat:@"Disable %@ Debugging", debuggingDescription] : [NSString stringWithFormat:@"Debug %@", _webSocketExecutorName];
  [items addObject:[RCTDevMenuItem buttonItemWithTitle:debugTitleJS handler:^{
    weakSelf.executorClass = isDebuggingJS ? Nil : jsDebuggingExecutorClass;
  }]];
}

这里的逻辑很简单,尝试从本地加载RCTWebSocketExecutor这个类,如果不存在这个类,那么他会告诉你,你想调试js,就要引入RCTWebSocketExecutor,这个类是用WebSokect作为与Chrome进行交互调试的一个核心类,当OC确认能从本地加载这个类的时候,它会初始化我们摇一摇看到的那个开启远程调试的Item,并且把handler这个回调block作为用户点击的一个事件处逻辑。

换句话来说,RN检测是否是处于调试状态,取决于executorClass这个类的成员是否为nil(相当于js的null),我们再后头看看这句话:

self.executorClass = NSClassFromString(_executorOverride ?: _settings[@"executorClass"]);  

只要我们预先向_settings[@"executorClass"]埋入好对应的值,那么RN则会自动在启动的时候读入这个值(实际上是,RN有在实时轮询读取这个值,来监控JS DEBUG是否需要开启),来开启js远程调试开关了。我们另外可以举一反三,一些其他的设置,我们也可以通过这种手段,去设置STD User Default里面的值,来弥补IOS Extension App无法通过摇一摇来触发JS Remote Debug的遗憾。

最后我再邪恶补充一下下,如果你觉得很懒,并且你又懂一点点OC或者swift,不需要那么麻烦,可以直接修改源码或者在load js bundle的时候,把executorClass初始化完成,这里原理和刚刚的解决方案基本一样,所以大家自己玩吧,祝玩得开心!