当前位置:首页 > 实时新闻 > 正文

ReactCocoa源码分析1

摘要: ReactCocoa源码分析1最佳答案53678位专家为你答疑解惑ReactCocoa源码分析1问题1在看代码的时候,发现代码中...

ReactCocoa源码分析1

最佳答案 53678位专家为你答疑解惑

ReactCocoa源码分析1

问题1

在看代码的时候,发现代码中使用到了

- (void)viewDidload{    [super viewDidload];    [self bindData];}  - (void)bindData{    [[RACObserve(self, propertyA) ignore:nil]                                  subscribeNext:^(NSArray *dataA) {        NSLog(@"use dataA");    }];}

但是在这个类的propertyA是在init之后去设置的,在viewDidload之前。也就是在使用RAC订阅属性变化信号之前,但是use dataA打印出来了。猜测RACObserve宏生成信号在调用subscribeNext中,直接就调用了dataA的block的逻辑。但是感觉比较奇怪,不应该是propertyA变化的时候才会调用dataA的block的逻辑吗。

现在具体看一下,一个信号的创建和订阅的源码:

信号创建:
+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {     return [RACDynamicSignal createSignal:didSubscribe]; }+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {     RACDynamicSignal *signal=[[self alloc] init];     signal->_didSubscribe=[didSubscribe copy];     return [signal setNameWithFormat:@"+createSignal:"];}

在创建一个信号的时候,会传进来一个叫didSubscribe的block,该信号会把它存下来。

信号订阅

RACSignal的subscribeNext方法:

- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock {    NSCParameterAssert(nextBlock !=NULL);    RACSubscriber *o=[RACSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL];    return [self subscribe:o];}

在singal的subscribeNext中,生成了一个subscriber。

+ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed {     RACSubscriber *subscriber=[[self alloc] init];      subscriber->_next=[next copy];     subscriber->_error=[error copy];     subscriber->_completed=[completed copy];     return subscriber;}

subscriber保存了nextBlock,errorBlock,completedBlock等数据信息

接着看signal的subscribe方法,改方法的参数是subscribeNext方法中生成的subscriber对象

- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {     NSCParameterAssert(subscriber !=nil);     RACCompoundDisposable *disposable=[RACCompoundDisposable compoundDisposable];     subscriber=[[RACPassthroughSubscriber alloc] initWithSubscriber:subscriber signal:self disposable:disposable];      if (self.didSubscribe !=NULL) {         RACDisposable *schedulingDisposable=[RACScheduler.subscriptionScheduler schedule:^{             RACDisposable *innerDisposable=self.didSubscribe(subscriber);             [disposable addDisposable:innerDisposable];         }];          [disposable addDisposable:schedulingDisposable];     }     return disposable; }
- (RACDisposable *)schedule:(void (^)(void))block {    NSCParameterAssert(block !=NULL);     if (RACScheduler.currentScheduler==nil) return [self.backgroundScheduler schedule:block];     block();     return nil; }

在signal的subscribe方法中,调用了RACScheduler.subscriptionScheduler schedule 方法,直接就将传入的block调用了,最终调用了signal的didSubscribe block,将subscriber传入。

再看一下RACObserve在生成一个signal的时候,传入的didSubscribe block逻辑的怎样的,以下是RACObserve相关源码:

#define RACObserve(TARGET, KEYPATH) \     ({ \         _Pragma("clang diagnostic push") \         _Pragma("clang diagnostic ignored \"-Wreceiver-is-weak\"") \         __weak id target_=(TARGET); \         [target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self];         \         _Pragma("clang diagnostic pop") \     })

在NSObject的RACPropertySubscribing分类中定义rac_valuesForKeyPath:observer:self:方法

继续:

- (RACSignal *)rac_valuesForKeyPath:(NSString *)keyPath observer:(__weak     NSObject *)observer {     return [[[self         rac_valuesAndChangesForKeyPath:keyPath options:NSKeyValueObservingOptionInitial observer:observer]         map:^(RACTuple *value) {             // -map: because it doesn't require the block trampoline that -reduceEach: uses             return value[0];         }]         setNameWithFormat:@"RACObserve(%@, %@)", self.rac_description, keyPath]; }

继续:

- (RACSignal *)rac_valuesAndChangesForKeyPath:(NSString *)keyPath options:(    NSKeyValueObservingOptions)options observer:(__weak NSObject *)    weakObserver {     NSObject *strongObserver=weakObserver;     keyPath=[keyPath copy];     NSRecursiveLock *objectLock=[[NSRecursiveLock alloc] init];     objectLock.name=@"    org.reactivecocoa.ReactiveCocoa.NSObjectRACPropertySubscribing";     __weak NSObject *weakSelf=self;     RACSignal *deallocSignal=[[RACSignal         zip:@[             self.rac_willDeallocSignal,             strongObserver.rac_willDeallocSignal ?: [RACSignal never]         ]]         doCompleted:^{             // Forces deallocation to wait if the object variables are currently             // being read on another thread.             [objectLock lock];             @onExit {                 [objectLock unlock];             };         }]; //重点关注这里,createSignal之后的参数就是该信号的didSubscribe block逻辑了。    return [[[RACSignal         createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {             // Hold onto the lock the whole time we're setting up the KVO             // observation, because any resurrection that might be caused by our             // retaining below must be balanced out by the time -dealloc returns             // (if another thread is waiting on the lock above).             [objectLock lock];             @onExit {                 [objectLock unlock];             };             __strong NSObject *observer __attribute__((objc_precise_lifetime))   =weakObserver;             __strong NSObject *self __attribute__((objc_precise_lifetime))=   weakSelf;             if (self==nil) {                 [subscriber sendCompleted];                 return nil;             }              return [self rac_observeKeyPath:keyPath options:options observer:            observer block:^(id value, NSDictionary *change, BOOL                 causedByDealloc, BOOL affectedOnlyLastComponent) {                 [subscriber sendNext:RACTuplePack(value, change)];             }];         }]         takeUntil:deallocSignal]         setNameWithFormat:@"%@ -rac_valueAndChangesForKeyPath: %@ options: %lu         observer: %@", self.rac_description, keyPath, (unsigned long)options,         strongObserver.rac_description]; }

可以看到在RACObserver宏定义的signal的didSubscriber block中又调用了rac_observeKeyPath:keyPath options: observer: block

继续(太长了只贴重点)

- (RACDisposable *)rac_observeKeyPath:(NSString *)keyPath options:(    NSKeyValueObservingOptions)options observer:(__weak NSObject *)    weakObserver block:(void (^)(id, NSDictionary *, BOOL, BOOL))block {     NSCParameterAssert(block !=nil);     NSCParameterAssert(keyPath.rac_keyPathComponents.count > 0);     //省略数十行    // Call the block with the initial value if needed.     if ((options & NSKeyValueObservingOptionInitial) !=0) {         id initialValue=[self valueForKeyPath:keyPath];         NSDictionary *initialChange=@{             NSKeyValueChangeKindKey: @(NSKeyValueChangeSetting),             NSKeyValueChangeNewKey: initialValue ?: NSNull.null,         };         block(initialValue, initialChange, NO, keyPathHasOneComponent);     }         //省略数十行 }

说明一下,options是NSKeyValueObservingOptions属于NS_OPTIONS

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {     NSKeyValueObservingOptionNew=0x01,     NSKeyValueObservingOptionOld=0x02,     NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0)=0x04,     NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0)=0x08};

在以上方法中,它判断了,传入的options是否是NSKeyValueObservingOptionInitial类型,而在调用rac_observeKeyPath: options: observer: block:的时候,option就是传的NSKeyValueObservingOptionInitial,所以会直接调用传进来的block,在rac_valuesAndChangesForKeyPath: options: observer:中调用rac_observeKeyPath: options: observer: block:的时候传入block里面的逻辑是这样:

[subscriber sendNext:RACTuplePack(value, change)]; 

综上所述,RACObserver生成的signal在调用subscribeNext方法订阅该信号的时候,会直接调用一次订阅信号之后next block的逻辑,所以即便是属性变化之后订阅属性变化信号,它也会默认先调用一次next block的逻辑。

正常kvo检测转化成信号的逻辑:

在RACObserver初始化的过程中,

- (RACDisposable *)rac_observeKeyPath:(NSString *)keyPath options:(    NSKeyValueObservingOptions)options observer:(__weak NSObject *)    weakObserver block:(void (^)(id, NSDictionary *, BOOL, BOOL))block {     NSCParameterAssert(block !=nil);     NSCParameterAssert(keyPath.rac_keyPathComponents.count > 0);     //省略数十行     NSKeyValueObservingOptions trampolineOptions=(options |         NSKeyValueObservingOptionPrior) & ~NSKeyValueObservingOptionInitial;     RACKVOTrampoline *trampoline=[[RACKVOTrampoline alloc] initWithTarget:    self observer:strongObserver keyPath:keyPathHead options:trampolineOptions     block:^(id trampolineTarget, id trampolineObserver, NSDictionary *change) {         // If this is a prior notification, clean up all the callbacks added to the         // previous value and call the callback block. Everything else is deferred         // until after we get the notification after the change.         if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {             [firstComponentDisposable() dispose];             if ((options & NSKeyValueObservingOptionPrior) !=0) {                 block([trampolineTarget valueForKeyPath:keyPath], change, NO,                     keyPathHasOneComponent);             }            return;         }         // From here the notification is not prior.         NSObject *value=[trampolineTarget valueForKey:keyPathHead];          // If the value has changed but is nil, there is no need to add callbacks to         // it, just call the callback block.         if (value==nil) {            block(nil, change, NO, keyPathHasOneComponent);             return;        }         // From here the notification is not prior and the value is not nil.          // Create a new firstComponentDisposable while getting rid of the old one at         // the same time, in case this is being called concurrently.         RACDisposable *oldFirstComponentDisposable=[        firstComponentSerialDisposable swapInDisposable:[RACCompoundDisposable         compoundDisposable]];         [oldFirstComponentDisposable dispose];         addDeallocObserverToPropertyValue(value);          // If there are no further key path components, there is no need to add the         // other callbacks, just call the callback block with the value itself.         if (keyPathHasOneComponent) {             block(value, change, NO, keyPathHasOneComponent);             return;         }         // The value has changed, is not nil, and there are more key path components         // to consider. Add the callbacks to the value for the remaining key path         // components and call the callback block with the current value of the full         // key path.         addObserverToValue(value);         block([value valueForKeyPath:keyPathTail], change, NO, keyPathHasOneComponent);     }];     // Stop the KVO observation when this one is disposed of.     [disposable addDisposable:trampoline];      //省略数十行 }

在该方法中生成了一个RACKVOTrampoline中间对象,看它的源码

- (id)initWithTarget:(__weak NSObject *)target observer:(__weak NSObject *)observer keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(RACKVOBlock)block {     NSCParameterAssert(keyPath !=nil);     NSCParameterAssert(block !=nil);     NSObject *strongTarget=target;     if (strongTarget==nil) return nil;     self=[super init];     if (self==nil) return nil;    _keyPath=[keyPath copy];     _block=[block copy];     _weakTarget=target;     _unsafeTarget=strongTarget;     _observer=observer;     [RACKVOProxy.sharedProxy addObserver:self forContext:(__bridge void *)self];     [strongTarget addObserver:RACKVOProxy.sharedProxy forKeyPath:self.keyPath     options:options context:(__bridge void *)self];     [strongTarget.rac_deallocDisposable addDisposable:self];     [self.observer.rac_deallocDisposable addDisposable:self];    return self;} - (void)dealloc {     [self dispose]; } #pragma mark Observation - (void)dispose {     NSObject *target;     NSObject *observer;     @synchronized (self) {         _block=nil;          // The target should still exist at this point, because we still need to         // tear down its KVO observation. Therefore, we can use the unsafe         // reference (and need to, because the weak one will have been zeroed by         // now).         target=self.unsafeTarget;         observer=self.observer;            _unsafeTarget=nil;         _observer=nil;     }     [target.rac_deallocDisposable removeDisposable:self];     [observer.rac_deallocDisposable removeDisposable:self];     [target removeObserver:RACKVOProxy.sharedProxy forKeyPath:self.keyPath     context:(__bridge void *)self];     [RACKVOProxy.sharedProxy removeObserver:self forContext:(__bridge void *)self];}  //系统的代理方法,其实是由RACKVOProxy.sharedProxy对象转发的,RACKVOProxy.sharedProxy才是真正处理系统消息的对象。- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(    NSDictionary *)change context:(void *)context {     if (context !=(__bridge void *)self) {         [super observeValueForKeyPath:keyPath ofObject:object change:change         context:context];         return;     }     RACKVOBlock block;     id observer;     id target;     @synchronized (self) {         block=self.block;         observer=self.observer;         target=self.weakTarget;     }     if (block==nil || target==nil) return;     block(target, observer, change); } 

可以看到RACKVOTrampoline对象替代原来使用KVO的对象,作为系统的代理,实现了代理方法。实际上,真正调用系统KVO注册的方法的时候,是往一个叫RACKVOProxy.sharedProxy的全局单例对象注册的。RACKVOTrampoline是具体处理KVO消息的对象,在RACKVOPorxy.shareProxy对象中注册了所有使用RAC KVO的系统消息,再由它转发给具体的RACKVOTrampoline进行处理,而在RACKVOTrampoline处理的时候,调用了RACKVOtrampoline初始化的时候传进来的block。之后在RACKVOTrampoline参数block调用过程中就会调用sendNext方法了,往外面发信号数据。

以下是RACKVOProxy.sharedProxy

@interface RACKVOProxy() @property (strong, nonatomic, readonly) NSMapTable *trampolines; @property (strong, nonatomic, readonly) dispatch_queue_t queue;  @end @implementation RACKVOProxy  + (instancetype)sharedProxy {     static RACKVOProxy *proxy;     static dispatch_once_t onceToken;        dispatch_once(&onceToken, ^{         proxy=[[self alloc] init];     });        return proxy; } - (instancetype)init {     self=[super init];     if (self==nil) return nil;        _queue=dispatch_queue_create("org.reactivecocoa.ReactiveCocoa.RACKVOProxy    ", DISPATCH_QUEUE_SERIAL);     _trampolines=[NSMapTable strongToWeakObjectsMapTable];        return self; } - (void)addObserver:(__weak NSObject *)observer forContext:(void *)context {     NSValue *valueContext=[NSValue valueWithPointer:context];        dispatch_sync(self.queue, ^{         [self.trampolines setObject:observer forKey:valueContext];     }); } - (void)removeObserver:(NSObject *)observer forContext:(void *)context {     NSValue *valueContext=[NSValue valueWithPointer:context];        dispatch_sync(self.queue, ^{         [self.trampolines removeObjectForKey:valueContext];     }); } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(    NSDictionary *)change context:(void *)context {     NSValue *valueContext=[NSValue valueWithPointer:context];     __block NSObject *trueObserver;        dispatch_sync(self.queue, ^{         trueObserver=[self.trampolines objectForKey:valueContext];     });       if (trueObserver !=nil) {         [trueObserver observeValueForKeyPath:keyPath ofObject:object change:        change context:context];     } }

RACKVOProxy.sharedProxy管理了整个RAC 中KVO的处理系统KVO消息的中间对象和系统KVO消息的转发。

综合上面的代码可以看出,正是由于各种中间对象替用户实现了代理方法起了代理对象的作用,用户才能把代码写的更加紧凑清晰。

问题2

看以下代码,假设combineLatest之后得到的信号是A

[[RACSignal combineLatest:@[[RACObserve(self, propertyA) ignore:nil], [RACObserve(self, propertyB) ignore:nil]]] subscribeNext:^(RACTuple *tuple) {  }];

1.使用combineLatest的时候,第一次订阅会不会触发subscribeNext后面的block

2.combineLatest中的信号,是同时调用了sendNext之后会触发A调用sendNext,还是只需要其中有一个信号调用了sendNext会触发A调用sendNext

看一下combineLatest源码:

+ (RACSignal *)combineLatest:(id<NSFastEnumeration>)signals {     return [[self join:signals block:^(RACSignal *left, RACSignal *right) {         return [left combineLatestWith:right];     }] setNameWithFormat:@"+combineLatest: %@", signals];}

继续 join: block:

+ (instancetype)join:(id<NSFastEnumeration>)streams block:(RACStream * (^)(id,     id))block {  //第一段    RACStream *current=nil;    // Creates streams of successively larger tuples by combining the input     // streams one-by-one.     for (RACStream *stream in streams) {         // For the first stream, just wrap its values in a RACTuple. That way,         // if only one stream is given, the result is still a stream of tuples.         if (current==nil) {             current=[stream map:^(id x) {                 return RACTuplePack(x);             }];            continue;        }         current=block(current, stream);     }    if (current==nil) return [self empty];//第二段    return [current map:^(RACTuple *xs) {         // Right now, each value is contained in its own tuple, sorta like:         //         // (((1), 2), 3)         //         // We need to unwrap all the layers and create a tuple out of the result.         NSMutableArray *values=[[NSMutableArray alloc] init];         while (xs !=nil) {            [values insertObject:xs.last ?: RACTupleNil.tupleNil atIndex:0];             xs=(xs.count > 1 ? xs.first : nil);        }         return [RACTuple tupleWithObjectsFromArray:values];    }];}

这部分代码分2段,第一段是将两个信号合并的逻辑,具体的合并逻辑是由外面传进来的block确定的。第二段是通过map将信号的值重新做了处理,第一段得到的信号属于signal of signals的类型,第二段将它打平。

再看一下combineLatestWith:方法

- (RACSignal *)combineLatestWith:(RACSignal *)signal {    NSCParameterAssert(signal !=nil);     return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {         RACCompoundDisposable *disposable=[RACCompoundDisposable compoundDisposable];         __block id lastSelfValue=nil;         __block BOOL selfCompleted=NO;         __block id lastOtherValue=nil;         __block BOOL otherCompleted=NO;         void (^sendNext)(void)=^{             @synchronized (disposable) {                 if (lastSelfValue==nil || lastOtherValue==nil) return;                 [subscriber sendNext:RACTuplePack(lastSelfValue, lastOtherValue)];             }         };         RACDisposable *selfDisposable=[self subscribeNext:^(id x) {             @synchronized (disposable) {                 lastSelfValue=x ?: RACTupleNil.tupleNil;                 sendNext();             }         } error:^(NSError *error) {             [subscriber sendError:error];         } completed:^{             @synchronized (disposable) {                 selfCompleted=YES;                 if (otherCompleted) [subscriber sendCompleted];             }         }];         [disposable addDisposable:selfDisposable];         RACDisposable *otherDisposable=[signal subscribeNext:^(id x) {             @synchronized (disposable) {                 lastOtherValue=x ?: RACTupleNil.tupleNil;                 sendNext();             }         } error:^(NSError *error) {             [subscriber sendError:error];         } completed:^{             @synchronized (disposable) {                 otherCompleted=YES;                 if (selfCompleted) [subscriber sendCompleted];             }         }];         [disposable addDisposable:otherDisposable];        return disposable;     }] setNameWithFormat:@"[%@] -combineLatestWith: %@", self.name, signal];} 

在以上代码中,调用了当前信号的subscribeNext方法,同时也调用了需要合并的信号的subscribeNext方法。subscribeNext方法block中调用了sendNext block,这个block是在combineLatestWith中定义,判断两个信号是否已经调用过sendNext,如果都同时掉用过sendNext就会触发combineLatest信号调用didSubscribe block,最终触发订阅combineLatest信号的传入的subscribeNext后的block。

综合上面的分析,类似于以下的使用方式

[[RACSignal combineLatest:@[[RACObserve(self, propertyA) ignore:nil], [RACObserve(self, propertyB) ignore:nil]]] subscribeNext:^(RACTuple *tuple) { }];

第一次订阅就会触发subscribeNext之后的block逻辑,并且是RACObserve这种类型的combineLatest才会,最上面已经分析了RACObserver生成的信号在第一次订阅调用的时候信号就会调用sendNext。

耗时三年,向 React 迁移的利与弊

2017 年,可汗学院( https://blog.khanacademy.org )开始在 iOS 和 Android 应用中使用 React Native。到了今年,这一迁移工程终于来到了终点,可汗学院应用中的所有页面都转到了 React Native 上。

2017 年,可汗学院之所以开始这一实验,主要是基于以下考虑:

iOS 和 Android 应用的设计几乎是一致的,交互设计、功能和内容都没什么区别。维护两个代码库是很困难的事情,尤其是这两个代码库还具有不同的数据设计、错误和开发新功能时的考虑要素。学院的移动开发团队很小,因此迁移工作不会涉及大批工程师。学院的网站已经在使用 React 了,因此学院的开发团队具备相应的专业知识,以及帮助迁移的概念和工具。

下面具体讲一下维护两个代码库所要面临的挑战。

不同的平台上会出现不同的错误。当然,React Native 也会有这种情况,但是出现概率就小得多了。开发新功能需要针对两个平台分别考虑设计、工程和测试工作。这意味着你至少需要两名工程师(iOS 和 Android),并且他们最好共同工作——对于像可汗学院这样小规模的团队来说这是很大的负担。功能和设计一旦构建好后就很难调整,因为每次调整都需要在两个平台上各来一次。平台之间的架构差异很大。可汗学院的 iOS 代码库比 Android 代码库大四岁。iOS 有 Swift、ReactiveCocoa、Cartography 和 CoreData。Android 则有自己的一组依赖项和数据流设计。这些差异累加起来,让平台之间的功能复用变得很复杂,对比两个平台的代码也不容易,于是团队相当于被平台分割成了两部分。

开始迁移

整个迁移过程基本上分为三个阶段:探索、跨越和灭绝。

在探索阶段(2017 年初),团队开始在原生代码和 Javascript 代码之间架起桥梁,并开始使用 React Native 构建一些页面。几乎所有的网络、数据、业务逻辑和所有“客户端后端”内容都是通过桥梁传递的。这一阶段的工作涉及很多样板,因此非常繁琐。

跨越阶段(2017 年中至 2018 年中)自然是最困难的。学院团队正式决定转向 React Native,但离终点还有很远的距离。这时候,团队需要考虑三件事:原生 iOS、原生 Android 和 React 原生代码。工程师要做出一项更改时需要考虑两个(或更多)范式,并且有很多东西要学习!

灭绝阶段(2018 年中至 2020 年中)从学院的“流主题树”项目开始,该项目历时数月,将内容数据库(学院的许多课程、视频、文章和练习内容)从大型原生数据库(例如 CoreData)完全迁移到了轻量级 、 用 Javascript 编写的缓存库。至此,学院的客户端内容数据库转入了 React Native,于是不需要在构建的桥梁上传递那么多内容了,并且可以开始删除许多原生代码。到今年的 v7.0 版本,应用的最后一个原生页面也会升级到 React Native,同时统一了手机和平板电脑的导航设计。

原生和 React Native 的混合体

开发团队将 React Native 用于“页面内容”,而将原生代码用于这些页面的导航用途。这是为了物尽其用,并让应用尽量提供原生级的体验。

应用的绝大多数“业务逻辑”都存在于页面内容中(例如“首页”标签中卡片的内容,或“书签”标签中的下载规则)。相比之下,标签栏或导航栏的内容在很大程度上不依赖业务逻辑。

但是,用 React Native 编写的导航栏和导航控制器和原生版本还是有一些差异,这些差异加起来还是很可观的。原生导航控制器提供了正确的滑动后退手势,以及用于推 / 弹出动画的正确动画 timing。原生导航栏可轻松处理带刘海的 iPhone。在原生 UIViewController 中包装的页面也更符合开发习惯。(有很多库试图模仿系统的标准导航栏,但它们都无法满足可汗学院团队的需求。)

这个迁移项目的一条原则是迁移后的应用依旧要有原生级的体验,当然小问题总是会有的,但以此为代价,团队在许多方面为应用带来了明显的改进。

国际化和本地化

移动应用的翻译工作主要有两部分组成:内容文本和平台文本。前者来自学院的内容管理系统,包括互动练习中的问题、视频的内容和字幕或文章中的文字。(许多内容文本是前面提到的“流主题树”的一部分。)平台文本是在移动应用的代码库中定义的,如“注册”按钮中的文字、标签栏项目的标题或“设置”页面中显示的文字。

可汗学院有一些优秀的自制基础架构来维护平台文本。在 JavaScript 中定义一个字符串,然后就可以使用开发团队编写的,将字符串复制到原生 iOS 和 Android 字符串的脚本。这个工具很好用,团队可以轻松地在原生和 React Native 上重用字符串,这在“跨越”迁移阶段有很大帮助。

从 2015 年至 2019 年,可汗学院的应用仅支持六个语言版本,但现在已达到了 19 个!共享的 iOS 和 Android 实现帮助团队构建了流主题树,这意味着添加其他语言不会让应用的体积大幅增加。此外,团队能够在 React Native 中设计轻松处理非拉丁字符的组件。应用的语言版本数量不再有技术瓶颈,而只取决于学院有多少翻译库可用。这极大地鼓舞了学院的国际倡导者,让他们更好地为世界各地的用户宣传可汗学院。

React Native 的体验

迁移到 React Native 并不是一帆风顺的,途中有很多坎坷,例如学习新语言、新的组件生命周期等。“跨越”时期尤其具有挑战性。原生代码和 React Native 代码之间的桥梁有很多烦人的样板代码。

团队成员很想念 Swift 的关联值枚举、方便的初始化器、命名参数以及轻松向结构和类添加函数和变量的特性。

但 React Native 也有很多好处。

React Native 比 UIKit 更具延展性。调整和重构代码的体验很不错。例如,为 UICollectionView 编写的代码与 UITableView 和 UIStackView 是不一样的,但是在 React Native 中并不用操心这一点?在大多数情况下,你可以在重构时剪切并粘贴代码,将某些内容从网格更改为列表是非常简单的。开发工具非常好用。方便的 VSCode 插件、linter 和自动修复器,大大减轻了开发人员和代码审核人员的负担。就算开发人员并不怎么熟悉 Android 应用开发,也可以使用 React Native 编写 Android 应用。熟悉 React Native 后,应用开发团队也有信心参与 Web 前端的开发工作了。

现在的成果

可汗学院的 iOS 和 Android 应用现在共享一个代码库,开发团队可以专注于应用的功能开发,用不着关心平台的差异。这意味着随着时间的推移,应用的功能质量会越来越好,并且团队可以对功能进行渐进式改进了。

共享的基础架构使团队更容易迁移到 GraphQL,从而简化了服务端向 Go 的迁移工作。应用的体积明显缩小了。从大型内容库数据库切换到流式数据库,极大地减少了应用体积。应用的开发团队与网站团队联系更紧密,两边的设计和功能也得以互通,整个组织的协作水平得到了提升。应用中仍然有一些原生代码,必要时团队可以继续使用 Xcode 或 Android Studio 实现特定于平台的功能,例如 iPad 多任务处理。团队创建了强大的设计系统,可帮助可汗学院的设计师和工程师快速协调产品的改进方案。

下一步计划

可汗学院目前的主要工程项目是 Goliath ,内容是将后端重新设计为一系列 Go 服务。统一的移动基础架构在这一迁移过程中起到了作用:虽然移动团队只有六名成员,也还是能继续构建新功能和修复程序。团队改善应用的功能、本地化、质量和性能的能力比以前强了很多。

本文最初发布于 khanacademy.org 网站,经网站授权由 InfoQ 中文站翻译并分享。

原文链接: https://blog.khanacademy.org/our-transition-to-react-native/

关注我并转发此篇文章,私信我“领取资料”,即可免费获得InfoQ价值4999元迷你书!

发表评论