如何评价 React Native?

write native apps with React.js?
关注者
8,194
被浏览
1,505,407

89 个回答

在写这个回答之前,我犹豫了很久,到底要不要唱反调呢,毕竟我也是一个也正在用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的方式吧。

先说结论:必有作为,但绝不会是一家独大,甚至很难成为主流。

用过 React 会知道,React 的核心概念是「DOM Representation」,在开发者和 DOM 中间构建一个中间件,然后通过高效的算法来 diff 两次 Virtual DOM 渲染的差异,然后在最小范围内更新 DOM,在大部分情况下——注意是大部分不是所有——这种做法都是足够高效的,但是对于精细的需求、动画控制等——比如在移动设备上做一个跟随 touchmove 的元素,还要各种 transition 等等——场景 React 会显得力不从心,或者很笨拙。

但是抛开这些太过复杂的需求,React 是有能力满足大部分的业务场景的。

再说 React Native,这几天不停看见媒体用「Web 开发要 XXX」一类的题目来发稿,真是吐槽无力。React Native 根本都不算 Web 开发好不好——Webview 都没了还 Web 个 bird 啊...

React Native 继承了 React 在 JavaScript 的扩展语法 JSX 中直接以声明式的方式来描述 UI 结构的机制,并实现了一个 CSS 的子集,这把「DOM Representation」的概念外扩成了「UI Representation」,由于不是操作真实 UI,就可以放到非 UI 线程来进行 render——所有做客户端 UI 开发的都应该知道 UI 线程是永远的痛,无论你怎么 render,render 的多低效,这些 render 都不会直接影响 UI,而要借由 React Native 来将改变更新回 UI 线程。

由于目前没有任何示例代码,也看不到更细节的实现机制介绍,所以以下部分为猜测。如果 React Native 沿袭 React 的机制,就会同样是把两次 render 的 diff 结果算出来,然后把 diff 结果传递回主线程,在最小范围内更新 UI。

所以,核心是以下三点:

1. 在非 UI 线程渲染 UI Representation

2. 高效的 diff 算法保证 UI update 的高效

3. 没错,由于中间件的机制,React 很有可能成为一个跨平台的统一 UI 解决方案,可以理解为 UI 开发的虚拟机?

声明式 UI 开发,简单快捷,必然大有作为。精细控制无力,复杂应用场景无法很好满足,必然受限。

最后再说一句...不是能写 JavaScript 就叫 Web 开发...

============================================

看了

@Rix Tox

的答案后来补充。

这个答案作为补充或扩展来回答「React + Flux 模型」是非常好的,但问题是「React Native」。React Native 的亮点是解决了在 Native 中使用声明式来开发 UI 的渲染效率问题,而不是软件架构和工程模型的问题,无论是 iOS 还是 Android 固有的模型也是非常好的。为啥 FE 会在乎这个?因为 FE 最习惯用声明式来开发 UI,而这么多年想参与到 Native 开发中的目标都没能达成,就是受制于最终的运行效率。

React 作为一个 View Component 封装解决方案来讲,同 Polymer 以及 AngularJS 中 Directive 并没有本质区别,只是用不同的思路来封装 View 而已,用 React 也不一定非得用 Flux 模型,React 替换 Backbone.View 组件,用纯朴的 MVC 模型来描述也是 okay 的。但是当 component 很多且互相嵌套时,就需要有一个合理的模型来描述通信机制,优雅且高效,那就是 Flux 模型了。前年 React 刚发布,还没有提出 Flux 时,我们在终端产品中开始小范围尝试 React 就遇到了 component 之间通信麻烦或者不合理的问题,当时的解决方案是全局实例化了一个继承 Backbone Event 的对象作为 event hub,所有的 component 都在其上来 reg 和 trigger 事件。现在看来,就是简化版的 Flux 模型。

不过我确实有一点遗漏的内容,React 的 DOM Representation 或者 React Native 的 UI Representation 的每个 component 都有一个 state 用来描述状态,而 component 某种意义上来说就是一个状态机,因此渲染结果是幂等的。

近些年 Web 前端的开发越来越多的受到工程复杂度上升导致整体性能下降的困扰,所以最近几年的新型框架大多有一些独特的机制来提升性能,而这些机制大多是从 Native UI 或者游戏开发中借鉴来的,比如 AngularJS 中的数据 dirty check 机制,比如 React.js 中 Virtual DOM 的 diff 机制,这些特点同以前的前端框架或库相比,真的是非常特殊且先进的改进,往往也会成为这个框架或库的亮点之一,对 FE 来讲当然就是新鲜玩意啦。

最后,Facebook 在 PHP 中直接写 HTML Tag 那东西叫 XHP,对应是 JSX 扩展语法,和 React 关系也不大...