如何评价 React Native?

write native apps with React.js?
关注者
7970
被浏览
1142563

在写这个回答之前,我犹豫了很久,到底要不要唱反调呢,毕竟我也是一个也正在用RN做开发的人。但是看到前面这么多吹的,我怕有的老板在看了前面的回答之后,觉得只要找几个前端工程师,就能在做前端页面之外也能做原生开发了,我决定写几个在实际开发中遇到的问题及解决方法,如果大家觉得真的能hold住,再决定项目是不是完全转向RN。

由于我最熟悉的还是iOS的那点东西,下面说的可能iOS多一些,但你要想查看更多问题,欢迎你到这里查看:github.com/facebook/rea

1. Cache:

我们现在项目中的图片缓存完全是自己借助[Redux-Persist](rt2zz/redux-persist)实现的(主要是为了能够离线查看),但是这种Cache除了一些性能问题外,本身与iOS的URL Loading System是没有关系的。比如说,正常情况下,如果图片是在WebView打开查看的,你想再取出来,你只要不做任何特殊设置,你就可以通过NSURLCache取出来,像这样:

NSURLCache *cache = [NSURLCache sharedURLCache];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSData *imgData = [cache cachedResponseForRequest:request].data;
UIImage *image = [UIImage imageWithData:imgData];

但是你会发现,在RN下URL Cache是取不出来的(至少我在40之前是这样的,现在由于使用我们自己造的缓存,在新版本中这个情况没有验证),那你需要创建一个NSURLProtocol的子类,自己实现利用NSURLCache缓存:

#import "HttpProtocol.h"

@interface HttpProtocol ()
<
NSURLSessionDelegate,
NSURLSessionDataDelegate
>

@property(copy, nonatomic) NSURLSession* session;
@property(strong, nonatomic)NSURLSessionDataTask* task;

@end

@implementation HttpProtocol

+ (void)start {
  [NSURLProtocol registerClass:self];
}

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
  if (request
      && ([request.URL.scheme isEqualToString:@"http"] || [request.URL.scheme isEqualToString:@"https"])
      && ([request.URL.pathExtension isEqualToString:@"jpg"] || [request.URL.pathExtension isEqualToString:@"png"] || [request.URL.pathExtension isEqualToString:@"bmp"] || [request.URL.pathExtension isEqualToString:@"gif"] ||
          [request.URL.pathExtension isEqualToString:@"tiff"]|| [request.URL.pathExtension isEqualToString:@"jpeg"]||
          [request.URL.pathExtension isEqualToString:@"JPEG"])) {
        return YES;
      }

  return NO;
}

-(NSURLSession *)session {
  if (!_session) {
    NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
    config.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
    _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue new]];
  }
  return _session;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
  return request;
}

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
  return [super requestIsCacheEquivalent:a toRequest:b];
}

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client {
  return [super initWithRequest:request cachedResponse:cachedResponse client:client];
}

- (void)startLoading {
  NSURLCache* cache = [NSURLCache sharedURLCache];
  NSCachedURLResponse* cachedResponse = [cache cachedResponseForRequest:self.request];
    if (cachedResponse) {//有缓存,从缓存中加载...
      NSData* data= cachedResponse.data;
      NSString* mimeType = cachedResponse.response.MIMEType;
      NSString* encoding = cachedResponse.response.textEncodingName;
      NSURLResponse* response = [[NSURLResponse alloc]initWithURL:self.request.URL MIMEType:mimeType expectedContentLength:data.length textEncodingName:encoding];
      [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
      [self.client URLProtocol:self didLoadData:data];
      [self.client URLProtocolDidFinishLoading:self];
  } else {
    NSMutableURLRequest* newRequest = [self.request mutableCopy];
    newRequest.cachePolicy = NSURLRequestUseProtocolCachePolicy;
    self.task = [self.session dataTaskWithRequest:newRequest];
    [self.task resume];
  }
}

-(void)stopLoading {
  [self.task cancel];
  self.task = nil;
}

-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
  [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
  completionHandler(NSURLSessionResponseAllow);
}

-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
  [self.client URLProtocol:self didLoadData:data];
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse * _Nullable cachedResponse))completionHandler{
  completionHandler(proposedResponse);
}

-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
  if (error) {
    [self.client URLProtocol:self didFailWithError:error];
  } else {
    [self.client URLProtocolDidFinishLoading:self];
  }
}

@end

实际上,我认为这样做缓存更加好,但是没有时间改……

2. WebView:

讲真,现在不用Webview的客户端真的似乎好像是不存在,但是RN自身的UIWebview由于添加了一些员原来UIWebview不具备的能力,比如postMessage(WKWebview里面的messagehandler),但是RN源码本身hack实现是有一些问题的:

if (_messagingEnabled) {
    #if RCT_DEV
    // See isNative in lodash
    NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
    BOOL postMessageIsNative = [
      [webView stringByEvaluatingJavaScriptFromString:testPostMessageNative]
      isEqualToString:@"true"
    ];
    if (!postMessageIsNative) {
      RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
    }
    #endif
    NSString *source = [NSString stringWithFormat:
      @"window.originalPostMessage = window.postMessage;"
      "window.postMessage = function(data) {"
        "window.location = '%@://%@?' + encodeURIComponent(String(data));"
      "};", RCTJSNavigationScheme, RCTJSPostMessageHost
    ];
    [webView stringByEvaluatingJavaScriptFromString:source];
  }

可以看见,window对象的postMessage对象本身被hack掉了,如果你的页面逻辑又重写了postMessage方法,就会有问题。

同样的,向页面发消息是通过webviewRef提供的postMessage方法(尽管在文档中没有提及),源码实现是这样的:

- (void)postMessage:(NSString *)message
{
  NSDictionary *eventInitDict = @{
    @"data": message,
  };
  NSString *source = [NSString
    stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
    RCTJSONStringify(eventInitDict, NULL)
  ];
  [_webView stringByEvaluatingJavaScriptFromString:source];
}

如果客户端想向发消息,你可能发现无法通信,这时你可以试试直接调用window.dispatchEvent

(今天我就遇到了,在Safari连接到真机网页,并通过终端打印,document.dispatchEvent === window.dispatchEvent 的结果为true,但是前者无法通信,后者可以)

3. DEBUG:

详细有很多人跟我一样使用Webstorm进行调试,在最新版本中,你可以选择通过Node还是Chrome进行debug:

我选择使用Chrome调试主要是因为官方[Devtool](chrome.google.com/webst)的原因。虽然工具很强大,但是你需要慎用,尤其是你想试试计时器是否起作用的时候:

github.com/facebook/rea

4.动画:

RN的动画真的很难用,至少我是这么认为的。在看完腾讯Alloy Team相关技术文章后,做动画还是很别扭,这种别扭感超过了我在搞Mac开发时做动画的感觉。使用LayoutAnimation可能还能好一些,但是能做的实现是在有限,更不用说原生那种转场动画,做跨组件之间的动画更加蛋疼。

然而这不是关键,如果你如果没有正确的使用动画,会对你业务代码的执行造成影响。比如说我们都知道使用InteractionManager.runAfterInteractions来跑耗时操作,然而里面代码的执行是依赖动画执行情况的,已经有很多人提出类似的issue了,比如这个:

InteractionManager.runAfterInteractions doesn&amp;amp;amp;amp;amp;#x27;t finished properly · Issue #7714 · facebook/react-native

一些针对动画性能的优化上,比如你想对listview的cell的动画做一下深度定制,一些国外的实践经验也是要针对平台优化(也就是写原生的package),而不是琢磨RN的Animation:

medium.com/@talkol/perf

5.手势:

这大概是另一个RN做的比较屎的地方,由于安卓和iOS在手势响应上有很大的差异,RN干脆自己搞了一套,但是并不很好。比如说你想在一个View上加一个双指触控的手势,大概需要一个这样的实现:

this._panResponder = PanResponder.create({
      // 要求成为响应者:
      onStartShouldSetPanResponder: (evt, gestureState) => {
        return gestureState.numberActiveTouches === 2
      },
      onStartShouldSetPanResponderCapture: (evt, gestureState) => {
        return gestureState.numberActiveTouches === 2
      },
      onMoveShouldSetPanResponder: (evt, gestureState) => false,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => false,
      onPanResponderMove: (evt, gestureState) => {
        // 最近一次的移动距离为gestureState.move{X,Y}
        if (gestureState.numberActiveTouches === 2) {
          this.method()
        }
        // 从成为响应者开始时的累计手势移动距离为gestureState.d{x,y}
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
        // 用户放开了所有的触摸点,且此时视图已经成为了响应者。
        // 一般来说这意味着一个手势操作已经成功完成。
      },
      onPanResponderTerminate: (evt, gestureState) => {
        // 另一个组件已经成为了新的响应者,所以当前手势将被取消。
      },
      onShouldBlockNativeResponder: (evt, gestureState) => {
        // 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者
        // 默认返回true。目前暂时只支持android。
        return true;
      },
    })

但是如果你添加的视图如果是WebView,由于WebView本事也有一套手势系统,导致你添加的这个不起作用,我的解决方法是干脆自己添加一个原生手势,然后同过DeviceEmitter通知RN,像这样:

#import "RootViewController.h"
#import "RCTBundleURLProvider.h"
#import "RCTRootView.h"

@interface RootViewController ()<UIGestureRecognizerDelegate>

@end

@implementation RootViewController

- (instancetype)initWithApplication: (UIApplication*)application andLaunchOptions: (NSDictionary*)launchOptions {
  if (self = [super init]) {
    NSURL *jsCodeLocation;

    jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];

    RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                        moduleName:@"mockingbot"
                                                 initialProperties:nil
                                                     launchOptions:launchOptions];
    rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
    UITapGestureRecognizer* tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleTap:)];
    tap.numberOfTouchesRequired = 2;
    tap.delegate = self;
    tap.delaysTouchesBegan = true;
    [rootView addGestureRecognizer:tap];
    self.view = rootView;
  }
    return self;
}
//需要设置于WebView自带手势共存
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
  return YES;
}
- (void)handleTap:(UITapGestureRecognizer*) tap {
  [[NSNotificationCenter defaultCenter] postNotificationName:TapGesture object: nil];
}
@end

手势Package:

#import "RootResponManager.h"
#import "RootViewController.h"

@implementation RootResponManager
{
  bool hasListeners;
}

RCT_EXPORT_MODULE();

- (NSArray<NSString *> *)supportedEvents {
  return @[TapGesture];
}

// 在添加第一个监听函数时触发
-(void)startObserving {
  hasListeners = YES;
  // Set up any upstream listeners or background tasks as necessary
  [NSNotificationCenter.defaultCenter addObserver:self
                                         selector:@selector(sendTapGestureNotification:)
                                             name:TapGesture
                                           object:nil];
}

// Will be called when this module's last listener is removed, or on dealloc.
-(void)stopObserving {
  hasListeners = NO;
  // Remove upstream listeners, stop unnecessary background tasks
  [NSNotificationCenter.defaultCenter removeObserver:self];
}

-(void)sendTapGestureNotification:(NSNotification*)notification {
  if (hasListeners) {
    [self sendEventWithName:TapGesture body:nil];
  }
}

@end

期初发现安卓和iOS在响应链上存在差异,最早的panResponder在安卓上是可用的,后来发现也需要原生手势比较好:

public class MainActivity extends ReactActivity {

    private GestureDetector detector;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        detector = new GestureDetector(this, new GestureHandler());
    }

    //这里的事件似乎是被子控件消费掉了,看看以后能不能想办法覆盖掉,目前以下代码不起任何作用
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        detector.onTouchEvent(event);
        return super.onTouchEvent(event);
    }

    //控制触控事件分发的时机
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getPointerCount() == 2) {
            sendBroadcast();
        }
        return super.dispatchTouchEvent(ev);
    }

    private final String NORMAL_ACTION = "TapGesture";
    public void sendBroadcast() {
        Intent intent = new Intent(NORMAL_ACTION);
        getApplicationContext().sendBroadcast(intent);
    }
}

class GestureHandler extends GestureDetector.SimpleOnGestureListener {

    private String getActionName(int action) {
        String name = "";
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                name = "ACTION_DOWN";
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                name = "ACTION_MOVE";
                break;
            }
            case MotionEvent.ACTION_UP: {
                name = "ACTION_UP";
                break;
            }
            default:
                break;
        }
        return name;
    }
}

总而言之,RN的手势蛋疼无比,如果有特殊需求,请先考虑使用原生手势。

6. Text:

要承认的是,es6的字符串模板的确很方便,Swift要等到4才有,OC用点语法糖才能做个差不多,像这样:

NSString* str = @"AAAAA"
                @"BBBBBB"
                @"CCCCC";

但是一谈到富文本,RN的<Text>嵌套简直是灾难(但是官方不这么认为),我顿时怀念YYText了。

TextInput组件也有问题,主要是在中文输入法情况下,你如果输入一些字符串没有点击回车,而是单纯的让Input失去焦点,候选的输入内容不会被输入,类似的反应有很多,像这个:

Possible bug with TextInput and Chinese input method on iOS · Issue #12599 · facebook/react-native

不过也有好消息,官方有望在0.47版本里面修复这个Bug:

7. NPM:

1)NPM与Cocoapods、Gradle、Maven相比,似乎Bug多了那么一些,在升级到5.X版本时,终于增加了package-lock.json,但是会导致你修改package.json来install失效,这时候请试着删掉package-lock.json再试试。

2)RN升级也是一种痛苦,经历过0.39 -> 0.40升级的诸位相信一定也有类似的体会。

3)只要你依赖的项目涉及跨平台的一些特性,或者用到了node-gyp,那么有很高几率在不同平台编译不通过,多数情况是在Mac可以通过,在Windows上却不行。在使用Realm、LeanCloud等SDK时都遇到过这种情况。

4)由于RN迭代速度很快,一些不经常更新的三方库可能干脆就跑不了了(虽然Swift的一些三方库也有这个问题,但是Swift迭代速度没有RN那么丧病),我现在仍然可以看到有些公司的iOS客户端仍然在用已经很久没有维护的ASI,但是没有见过有人用RN 0.2x版本时的package。

8. CI:

把一个平台的CI从写脚本到跑通对我来说大概需要一到两天的时间,然而你需要跑三个平台。额,应该不是乘以三倍的时间……

如果你做的是个开源的RN项目,用Travis做CI,可以看看这篇文章,然后自己试着搞一下,大概就能体会RN CI的痛苦:

React Native App CI

目前就想到这些,欢迎大家补充。

如果你们看了以后,仍然觉得解决RN的Bug很快乐(你们真的很有开源精神),可以再试试把项目完全切换到RN,否则,还是考虑一下原生+RN的方式吧。