基于ReSwift和App Coordinator的iOS架构

本文首发于InfoQ:

http://www.infoq.com/cn/articles/ios-arch-based-on-reswift-and-app-coordinator

iOS架构漫谈

当我们在谈iOS应用架构时,我们听到最多的是MVC,MVVM,VIPER这三个Buzz Word,他们的逻辑一脉相承,不断的从ViewController中把逻辑拆分出去。从苹果官方推荐的MVC:

图片来自:http://t.cn/R4vP8Ko

随着系统的复杂,把功能进行细化,把整合View展示数据的逻辑独立出来形成ViewModel模块,架构风格就变成了MVVM:

图片来自:http://t.cn/R4vP8Ko

随着系统的更加复杂,把路由的职责,获取数据的职责也独立出去,架构风格就变成了VIPER:

图片来自:http://t.cn/R4vP8Ko

本文则想从另一个角度和大家探讨一个新的iOS应用架构方案,架构的本质是管理复杂性,在讨论具体的架构方案前,我们首先应该明确一个iOS应用的开发,其复杂性在哪里?

iOS应用的开发复杂度

对于一个iOS应用来说,其开发的复杂性主要体现在三个方面:

复杂界面设计的实现和样式管理

iOS App最终呈现给用户的是一组组的UI界面,而对于一个特定的App来说,其UI的设计元素(如配色,字体大小,间距等)基本上是固定的,另外,组成该App的基础组件(如Button种类,输入框种类等)也是有限的。但是如何管理、组合、重用组件则是架构师需要考虑的问题,尤其是一些App在开发过程中可能出现大量的UI样式重构,更需要清晰的控制住重构的影响范围。这儿的复杂性本质上是UI组件自身设计实现的复杂性,多UI组件之间的组合方式和UI组件的重用机制。

路由设计

对于一个大型的iOS应用,通常会把其功能按Feature拆分,经过这样的拆分之后,其可能出现的路由有以下几种:

APP间路由: 从其它App调起当前App,并进入一个很深层次的页面(图示1)。

APP内路由:

  1. 启动进入App的Home页面(图示2)
  2. 从Home页面到进Feature Flow(图示3)
  3. Feature内按流程的页面的路由(图示4)
  4. 各Feature之间的页面跳转(图示5)
  5. 各Feature共享的单点信息页的跳转(图示6)

根据Apple官方的MVC架构,这些复杂的各种跳转逻辑,以及跳转前的ViewController的准备工作等逻辑缠绕在AppDelegate的初始化,ViewController的UI逻辑中。这儿的复杂性主要是UI和业务之间缠绕不清的相互耦合。

应用状态管理

一个iOS应用本质上就是一个状态机,从一个状态的UI由User Action或者API调用返回的Data Action触发达到下一个状态的UI。为了准确的控制应用功能,开发者需要能够清楚的知道:

  • 应用的当前UI是由哪些状态决定的?
  • User Action会影响哪些应用状态?如何影响的?
  • Data Action会影响哪些应用状态?如何影响的?

在MVC,MVVM,VIPER的架构中,应用的状态分散在Model或者Entity中,甚至有些状态直接保存在View Controller中,在跟踪状态时经常需要跨越多个Model,很难获取到一个全貌的应用状态。另外,对于Action会如何影响应用的状态跟踪起来也比较困难,尤其是当一个Action产生的影响路径不同,或最终可能导致多个Model的状态发生改变时。这儿的复杂性主要体现在治理分散的状态,以及管理不统一的状态改变机制带来的复杂性。

如何管理这些复杂度

前面明确了iOS应用开发的复杂性所在,那么从架构层面上应该如何去管理这些复杂性呢?

使用Atomic Design和Component Driven Development管理界面开发的复杂度

UI界面的复杂度本质上是一个点上的复杂度,其复杂性集中在系统的某些小细节处,不会增加系统整体规划的复杂度,所以控制其复杂度的主要方式是隔离,避免一个UI组件之间的相互交织,变成一个面上的复杂度,导致复杂度不可控。在UI层,最流行的隔离方式就是组件化,在笔者之前的一篇文章《前端组件化方案》中详细解释了前端组件化方案的实施细节,这儿就不再赘述。

使用App Coordinator统一管理应用路由

应用的路由主要分为App间路由和App内路由,对它们需要分别处理

App间路由

对于APP之间的路由,主要通过两种方式实现:

一种是URL Scheme 通过在当前App中配置进行相应的设置,即可从别的APP跳转到当前APP。进入当前App之后,直接在AppDelegate中的方法:

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool

转换进App内的路由。

另一种是Universal Links,同样的通过在当前App中进行配置,当用户点击URL就会跳转到当前的App里。进入当前APP之后,直接在AppDelegate中的方法:

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool

中转进App内路由。

所以App间的路由逻辑相对简单,就是一个把外部URL映射到内部路由中。这部分只需要增加一个URL Scheme或Universal Link对应到App内路由的处理逻辑即可。

App内路由

对于内部路由,我们可以引入App Coordinator来管理所有路由。App Coordinator是Soroush Khanlou在2015年的NSSpain演讲上提出的一个模式,其本质上是Martin Fowler在《Patterns of Enterprise Application Architecture》中描述的Application Controller模式在iOS开发上的应用。其核心理念如下:

  1. 抽象出一个Coordinator对象概念。
  2. 由该Coordinator对象负责ViewController的创建和配置。
  3. 由该Coordinator对象来管理所有的ViewController跳转。
  4. Coordinator可以派生子Coordinator来管理不同的Feature Flow。

经过这层抽象之后,一个复杂App的路由对应关系就会如下:

从图中可以看出,应用的UI和业务逻辑被清晰的拆分开,各自有了自己清晰的职责。ViewController的初始化,ViewController之间的链接逻辑全部都转移到App Coordinator的体系中去了,ViewController则彻底变成了一个个独立的个体,其只负责:

  1. 自己界面内的子UIView组织;
  2. 接收数据并把数据绑定到对应的子UIView展示;
  3. 把界面上的user action转换为业务上的user intents,然后转入App Coordinator中进行业务处理。

通过引入AppCoordinator之后,UI和业务逻辑被拆分开,各自处理自己负责的逻辑。在iOS应用中,路由的底层实现还是UINavigationController提供的present,push,pop等函数,在其之上,iOS社区出了各种封装库来更好的封装ViewController之间的跳转接口,如JLRoutesroutable-iosMGJRouter等,在这个基础上我们来进一步思考App Coordinator,其概念核心是把ViewController跳转和业务逻辑一起抽象为user intents(用户意图),对于开发者具体使用什么样的方式实现的跳转逻辑并没有限制,而路由的实现方式在一个应用中的影响范围非常广,切换路由的实现方式基本上就是一次全App的重构(做过React应用的react-router0.13升级的朋友应该深有体会)。所以在App Coordinator的基础之上,还可以引入Protocol-Oriented Programming的概念,在App Coordinator的具体实现和ViewController之间抽象一层Protocols,把UI和业务逻辑的实现彻底抽离开。经过这层抽象之后,路由关系变化如下:

经过App Coordinator统一处理路由之后,App可以得到如下好处:

  1. ViewController变得非常简单,成为了一个概念清晰的,独立的UI组件。这极大的增加了其可复用性。
  2. UI和业务逻辑的抽离也增加了业务代码的可复用性,在多屏时代,当你需要为当前应用增加一个iPad版本时,只需要重新做一套iPad UI对接到当前iPhone版的App Coordinator中就完成了。
  3. App Coordinator定义与实现的分离,UI和业务的分离让应用在做A/B Testing时变得更加容易,可以简单的使用不同实现的Coordinator,或者不同版本的ViewController即可。

使用ReSwift管理应用状态

前面提到引入App Coordinator之后,ViewController剩下的职责之一就是“接收数据并把数据绑定到对应的子UIView展示”,这儿的数据来源就是应用的状态。它山之石,可以攻玉,不只是iOS应用有复杂状态管理的问题,在越来越多的逻辑往前端迁移的时代,所有的前端都面临着类似的问题,而目前Web前端最火的Redux就是为了解决这个问题诞生的状态管理机制,而ReSwift则把这套机制带入了iOS的世界。这套机制中主要有一下几个概念:

  • App State: 在一个时间点上,应用的所有状态. 只要App State一样,应用的展现就是一样的。
  • Store: 保存App State的对象,其还负责发送Action更新App State。
  • Action: 表示一次改变应用状态的行为,其本身可以携带用以改变App State的数据。
  • Reducer: 一个接收当前App State和Action,返回新的App State的小函数。

在这个机制下, 一个App的状态转换如下:

  • 启动初始化App State -> 初始化UI,并把它绑定到对应的App State的属性上
  • 业务操作 -> 产生Action -> Reducer接收Action和当前App State产生新的AppState -> 更新当前State -> 通知UI AppState有更新 -> UI显示新的状态 -> 下一个业务操作……

在这个状态转换的过程中,需要注意,业务操作会有两类:

  • 无异步调用的操作,如点击界面把界面数据存储到App State上;这类操作处理起来非常简单,按照上面提到的状态转换流程走一圈即可。
  • 有异步调用的操作。如点击查询,调用API,数据返回之后再存储到App State上。这类操作就需要引入一个新的逻辑概念(Action Creators)来处理,通过Action Creators来处理异步调用并分发新的Action。

整个App的状态变换过程如下:

无异步调用操作的状态流转

有异步调用操作的状态流转

经过ReSwift统一管理应用状态之后,App开发可以得到如下好处:

  1. 统一管理应用状态,包括统一的机制和唯一的状态容器,这让应用状态的改变更容易预测,也更容易调试。
  2. 清晰的逻辑拆分,清晰的代码组织方式,让团队的协作更加容易。
  3. 函数式的编程方式,每个组件都只做一件小事并且是独立的小函数,这增加了应用的可测试性。
  4. 单向数据流,数据驱动UI的编程方式。

整理后的iOS架构

经过上面的大篇幅介绍,我们来归纳下结合了App Coordinator和ReSwift的一个iOS App的整体架构图:

架构实战

上面已经讲解了整体的架构原理,”Talk is cheap”, 接下来就以Raywendlich上面的这个App为例来看看如何实践这个架构。

(图片来自:http://t.cn/RCO2Sa0

第一步:构建UI组件

在构建UI组件时,因为每个组件都是独立的,所以团队可以并发的做多个UI页面,在做页面时,需要考虑:

  1. 该ViewController包含多少子UIView?子UIView是如何组织在一起的?
  2. 该ViewController需要的数据及该数据的格式?
  3. 该ViewController需要支持哪些业务操作?

以第一个页面为例:

    class SearchSceneViewController: BaseViewController {    
      //定义业务操作的接口    
      var searchSceneCoordinator:SearchSceneCoordinatorProtocol?    
      //子组件    
      var searchView:SearchView?

      //该UI接收的数据结构    
      private func update(state: AppState) {        
        if let searchCriteria = state.property.searchCriter   {            
        searchView?.update(searchCriteria: searchCriteria)        }    }?    
      //支持的业务操作    
      func searchByCity(searchCriteria:SearchCriteria) {        
        searchSceneCoordinator?.searchByCity(searchCriteria: searchCriteria)    
        }?    
      func searchByCurrentLocation() {        
          searchSceneCoordinator?.searchByCurrentLocation()    
      }    
       //子组件的组织    
      override func viewDidLoad() {        
        super.viewDidLoad()        
        searchView = SearchView(frame: self.view.bounds)        
        searchView?.goButtonOnClick = self.searchByCity        
        searchView?.locationButtonOnClick = self.searchByCurrentLocation        
        self.view.addSubview(searchView!)    
      }
    }

注:子组件支持的操作都以property的形式从外部注入,组件内命名更组件化,不应包含业务含义。

其它的几个ViewController也依法炮制,完成所有UI组件,这步完成之后,我们就有了App的所有UI组件,以及UI支持的所有操作接口。下一步就是把他们串联起来,根据业务逻辑完成User Journey。

第二步:构建App Coordinators串联所有的ViewController

首先,在AppDelegate中加入AppCoordinator,把路由跳转的逻辑转移到AppCoordinator中。

var appCoordinator: AppCoordinator!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  window = UIWindow()
  let rootVC = UINavigationController()
  window?.rootViewController = rootVC
  appCoordinator = AppCoordinator(rootVC)
  appCoordinator.start()
  window?.makeKeyAndVisible()
  return true
}

然后,在AppCoordinator中实现首页SeachSceneViewController的加载

    class AppCoordinator {
        var rootVC: UINavigationController
        init(_ rootVC: UINavigationController){
            self.rootVC = rootVC
        }
        func start() {
            let searchVC = SearchSceneViewController();
            let searchSceneCoordinator = SearchSceneCoordinator(self.rootVC)
            searchVC.searchSceneCoordinator = searchSceneCoordinator
            self.rootVC.pushViewController(searchVC, animated: true)
        }
    }

在上一步中我们已经为每个ViewController定义好对应的CoordinatorProtocol,也会在这一步中实现

    protocol SearchSceneCoordinatorProtocol {    
      func searchByCity(searchCriteria:SearchCriteria)    
      func searchByCurrentLocation()
    }

    ​class SearchSceneCoordinator: AppCoordinator, SearchSceneCoordinatorProtocol {    
      func searchByCity(searchCriteria:SearchCriteria) {        
        self.pushSearchResultViewController()    
      }        
      func searchByCurrentLocation() {        
        self.pushSearchResultViewController()    
      }        
      private func pushSearchResultViewController() {        
          let searchResultVC = SearchResultSceneViewController();        
          let searchResultCoordinator = SearchResultsSceneCoordinator(self.rootVC)        
          searchResultVC.searchResultCoordinator = searchResultCoordinator        
          self.rootVC.pushViewController(searchResultVC, animated: true)    
      }
    }

以同样的方式完成SearchResultSceneCoordinator. 从上面的的代码中可以看出,我们跳转逻辑中只做了两件事:初始化ViewController和装配该ViewController对应的Coordinator。这步完成之后,所有UI之间就已经按照业务逻辑串联起来了。下一步就是根据业务逻辑,让用App State在UI之间流转起来。

第三步:引入ReSwift架构构建Redux风格的应用状态管理机制

首先,跟着ReSwift官方指导选取你喜欢的方式引入ReSwift框架,笔者使用的是Carthage。

定义App State

然后,需要根据业务定义出整个App的State,定义State的方式可以从业务上建模,也可以根据UI需求来建模,笔者偏向于从UI需求建模,这样的State更容易和UI进行绑定。在本例中主要的State有:

    struct AppState: StateType {
        var property:PropertyState
        ...
    }
    struct PropertyState {
        var searchCriteria:SearchCriteria?
        var properties:[PropertyDetail]?
        var selectedProperty:Int = -1
    }
    struct SearchCriteria {
        let placeName:String?
        let centerPoint:String?
    }
    struct PropertyDetail {
        var title:String
        ...
    }

定义好State的模型之后,接着就需要把AppState绑定到Store上,然后直接把Store以全局变量的形式添加到AppDelegate中。

    let mainStore = Store<AppState>(    
      reducer: AppReducer(),    
      state: nil
      )

把App State绑定到对应的UI上

注入之后,就可以把AppState中的属性绑定到对应的UI上了,注意,接收数据绑定应该是每个页面的顶层ViewController,其它的子View都应该只是以property的形式接收ViewController传递的值。绑定AppState需要做两件事:订阅AppState

      override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            mainStore.subscribe(self) { state in state }
        }
        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
            mainStore.unsubscribe(self)
        }

和实现StoreSubscriber的newState方法

    class SearchSceneViewController: StoreSubscriber {    
      ......    
      override func newState(state: AppState) {        
        self.update(state: state)        
        super.newState(state: state)    
      }    
      ......
    }

经过绑定之后,每一次的AppState修改都会通知到ViewController,ViewController就可以根据AppState中的内容更新自己的UI了。

定义Actions和Reducers实现App State更新机制

绑定好UI和AppState之后,接下来就应该实现改变AppState的机制了,首先需要定义会改变AppState的Action们

    struct UpdateSearchCriteria: Action {    
      let searchCriteria:SearchCriteria
    }
    ......

然后,在AppCoordinator中根据业务逻辑把对应的Action分发出去, 如果有异步请求,还需要使用ActionCreator来请求数据,然后再生成Action发送出去

    func searchProperties(searchCriteria: SearchCriteria, _ callback:(() -> Void)?) -> ActionCreator {
            return { state, store in
                store.dispatch(UpdateSearchCriteria(searchCriteria: searchCriteria))
                self.propertyApi.findProperties(
                    searchCriteria: searchCriteria,
                    success: { (response) in
                        store.dispatch(UpdateProperties(response: response))
                        store.dispatch(EndLoading())
                        callback?()
                },
                    failure: { (error) in
                        store.dispatch(EndLoading())
                        store.dispatch(SaveErrorMessage(errorMessage: (error?.localizedDescription)!))
                }
                )
                return StartLoading()
            }
        }

Action分发出去之后,初始化Store时注入的Reducer就会接收到相应的Action,并根据自己的业务逻辑和当前App State的状态生成一个新的App State

    func propertyReducer(_ state: PropertyState?, action: Action) -> PropertyState {
            var state = state ?? PropertyState()
            switch action {
            case let action as UpdateSearchCriteria:
                state.searchCriteria = action.searchCriteria
            ...
            default:
                break
            }
            return state
        }

最终Store以Reducer生成的新App State替换掉老的App State完成了应用状态的更新。

以上三步就是一个完整的架构实践步骤,该示例的所有源代码可以在笔者的Github上找到。

总结

以解决掉Massive ViewController的iOS应用架构之争持续多年,笔者也参与了公司内外的多场讨论,架构本无好坏,只是各自适应不同的上下文而已。本文中提到的架构方式使用了多种模式,它们各自解决了架构上的一些问题,但并不是一定要捆绑在一起使用,大家完全可以根据需要裁剪出自己需要的模式,希望本文中提到的架构模式能够给你带来一些启迪。


更多精彩洞见,请关注微信公众号:思特沃克

Share

智能时代的移动应用

对于一个企业,它做移动应用的本质目的是什么?是连接性,让企业的业务价值在时间与空间两个维度上与用户保持无缝的连通,让业务的闭环不再受到时间和地点的制约。随着智能时代的到来,通过移动设备来连接用户的方式正在发生巨大的变革,移动应用开始以一种新的形态出现在公众的视野中。

应用疲劳

以iPhone带动的智能手机的普及风潮,让移动应用开发成为了一波软件淘金热,经过9年的高速发展,现在各个应用市场已充斥了数百万款各式各样的应用。当所有淘金热都退去之后,移动应用生态圈开始呈现出一种“应用疲劳”的状态。

我们来看两组数据:

图1 Comscore调查:(美国)智能手机用户每月下载的移动应用数量

图1来自Comscore的一项调查,49%的美国手机用户在一个月内一个应用都没下。App Annie的报告中也指出,在过去3年中,iOS应用的下载几乎没有增长,Android应用方面的下载虽有增长,但都是来自印度,巴西等新兴市场。用户开始对入侵自己生活的移动应用进行抵制。

图2 AppBoy报告:用户安装完应用之后的打开频次

图2来自AppBoy的报告,用户安转完一个应用之后,第二天再次打开的概率是25%,一周之后再次打开的概率锐减到11%,而3个月之后再次打开的概率会降到3%左右。也就是说,企业即便能通过爆点效应或者巨额补贴来提升安装量,也很难留住用户。

其实不仅仅是用户,移动应用开发者也非常疲惫,他们面对着移动应用市场的低效率审核、分发,以及越来越昂贵的推广费用。

这样的现状给想切入移动业务的企业带来了极大的挑战。

消息应用的深度平台化

相比其它移动应用的疲劳状,消息类应用(微信、Facebook Messenger、WhatsApp等)却呈现出完全不同的状态。我们看看下面的两组数据:

图3 Statista报告:微信月活跃用户数

图3是微信的月活跃用户数,每个季度都在增长,16年Q4更是达到惊人的8.89亿,几乎每一位中国手机用户都安装了微信。微信开始完全替代短信和电话,成为了中国手机用户的第一入口。

图4 KPCB报告:全球使用率最高的移动应用

图4展示了使用率最高的移动应用,前10名中有6款是消息应用。即消息应用不仅拥有最巨量的用户,还占据着最多的用户时间。

成为用户的入口之后,各大消息应用开始不断扩张自己的端到端价值交付(产品和服务发现、推荐、购买、支付等),让用户不用离开消息应用即可完成业务闭环。同时它们还致力于打造自己的生态系统,把自己变成一个开放平台,让第三方开发者,企业可以进驻共享其庞大的用户群体:

  • Facebook宣布将Messenger剥离为独立应用,并将其升级为Messenger Platform,让企业可以非常方便的把自己的业务和Facebook用户连接起来。
  • 微信发布了微信公众平台,并组成了以微信三方服务入口--订阅号、服务号、小程序的开放服务体系,帮助企业更好的通过微信平台服务自己的用户。
  • Slack构建了自己的Bot平台,让服务商可以把自己的服务通过ChatBot的形式接入Slack,供Slack用户使用。
  • Telegram也构建了自己的Bot Platform,让用户可以在会话中方便的访问到第三方服务。 Snapchat,WhatsApp等其他的消息应用也都在规划着自己开放平台计划。

消息应用的深度平台化让其逐渐变成一种新的应用分发和运行平台。

图5 消息应用正成为新型平台

这为面临移动挑战的企业提供了一种新的、成本更低的连接用户的思路。

会话式应用崛起

随着消息平台功能的进一步完善,以及更多的企业进驻消息平台。用户和企业之间进行交互的方式正在发生着变化,用户开始习惯以会话的形式来获取服务,而会话式应用也赋予用户新的移动应用体验:

  • 更集中:一个应用处理多种不同的服务
  • 更便利:使用自己最熟悉的消息平台享受服务,无需安装新应用,可以在多个服务之间随心切换
  • 更整洁:无需担心应用在手机桌面的排列组织

越来越多的用户直接在消息平台上消费自己想要的服务,比如:通过微信打滴滴专车、通过招行微信服务号查询信用卡信息、通过微信提示信息确认手机是否联网等等。

用户的聚集和使用又反向促进了整个会话式应用生态圈的繁荣,除了前面提到的正积极构建开放平台的各大消息应用,会话式应用的基础设施也在快速发展,逐渐形成了包含渠道、探索机制、构建工具、AI增强以及应用监控在内的全生命周期生态系统。

图6 Chatbots 一览

会话式应用在2016年呈现出旺盛的生命力,开始涉足到企业业务的各个角落。

AI引爆

会话式应用在2016年崛起,但它并不是一个新鲜事物。已存在多年的电话银行服务、10086服务,用亲切的声音告诉我们”个人业务请按1,企业业务请按2……“,这就是会话式应用,即以会话的形式完成服务。这类应用在客服领域得到大量部署,但是收效甚微,用户都是被动转入该系统,而大部分用户进入该系统的目标就是等待转人工服务。在其它领域,会话式应用则几乎没有发展。但是在2016年,会话式应用重新爆发,并且被应用在了更加广泛的场景下。其中一个重要的引爆因素就是人工智能的发展。

会话式应用可以分为两大类:

  • 基于规则的应用:这类应用是类命令式系统,定义输入规则,根据规则连接用户输入和企业业务。因为只能按照定义的逻辑执行,无法随着与用户的交流进行演化,所以系统的适用范围被限制在了规则清晰、输入明确的场景下。前面提到的电话语音服务,以及现在很流行的微信服务号回复关键字获取服务等都属于这类应用。
  • 基于人工智能的应用:以语音识别、NLP/NLU(Natural Language Programming/Natural Language Understanding,自然语言处理/自然语言理解)、Deep Learning(深度学习)等技术为基础,能够更人性化的理解用户需求,给用户更准确的需求响应,且具备演进功能,能够随着交流的深入变得更加理解用户,变成一个专门为该用户定制的移动应用。苹果的Siri,Amazon的Echo等都属于这类应用。

人工智能对会话式应用的意义主要在于:

  • NLP/NLU的高速发展让应用能够更人性化的识别用户需求,降低用户的使用门槛,让用户以更自然的、更个人化的方式表达需求。
  • 语音识别、图像识别能力的提升,扩展了用户的交流形式和会话式应用的适用场景,不管用户是在图书馆、KTV或是厨房都有合适的方式进行会话,以获取服务。
  • Deep Learning等技术让应用具有自演进能力,用户使用应用越久,需求匹配会越精准,用户使用起来会更加顺手,这会极大的增强应用的用户粘性。
  • 大量的开放式“AI as a service”平台(api.ai, wit.a, Microsoft Luis等)把学术界的高深算法以工程化的方式开放给了工业界,让小企业,甚至个人开发者都可以享受到大企业才具备的能力。开发者的智力和想象力得到释放,激活了整个会话式应用市场。

人工智能引爆了会话式应用,同时也是会话式应用的核心竞争力,其对用户意图的解析准确度决定了会话式应用的用户体验。

未来的移动应用形态?

图7 未来的移动应用形态

在软件行业不长的历史中,出现了几次行业变革点,在变革的过程中也诞生了多个伟大企业,同时也有很多曾经伟大的企业被抛入故纸堆。刚刚过去的2016年会是行业变革的又一个起点吗?

未来,以连接用户和企业业务为目的的移动应用形态是否会发生如下改变:

  • 以操作系统(iOS,Android)为平台 转变为 以消息应用(微信,Facebook,Slack等)为平台;
  • 以用户流程体验为核心竞争力 转变为 以人工智能为核心竞争力;
  • 流程控制应用 转变为意图响应应用

让我们拭目以待!


更多精彩商业洞见,请关注微信公众号:ThoughtWorks商业洞见

Share

React Native性能之谜

在PhoneGap、RubyMotion、Xamarin、Ionic一众跨平台开发工具中,React Native能够杀出一条血路,获得目前这么大的影响力,除了React社区生态圈的加持和Facebook的大力推广以外,另外一个最主要的原因就是其在开发效率和应用性能方面取得了一个比较好的平衡:

  • 开发效率通过JS工程实践,逻辑跨平台复用得到极大提升
  • 性能则通过全Native的UI层得到满足

不过,虽说框架提供了这个平衡能力,平衡点的选择却掌握在开发者手中,本文将从React Native的性能角度来看看应该如何掌握这个平衡点。

React Native的工作原理

在React Native的应用中,存在着两个不同的技术王国:JS王国和Native王国。应用在启动时会先进行双向注册,搭好桥,让两个王国知道彼此的存在,以及定义好彼此合作的方式:

(图片来源:https://tadeuzagallo.com/blog/react-native-bridge/

然后,在应用的实际运行过程中,两个技术王国通过搭好的桥,彼此合作完成用户功能:

(图片来源:http://www.jianshu.com/p/978c4bd3a759)

因此,React Native的本质是在两个技术王国之间搭建双向桥梁,让他们可以相互调用和响应。那么就可以把上图简化一下:

React Native的性能瓶颈

经过上面的分析,我们就可以把一个React Native应用分成三个部分:Native王国、Bridge、JS王国。当应用运行时,Native王国和JS王国各自运行在自己独立的线程中:

Native王国:

  • 运行在主线程上(可能会有些独立的后台线程处理运算,当前讨论中可忽略)
  • iOS平台上运行Object-C/Swift代码,Android平台上运行Java/Kotlin代码
  • 负责处理UI的渲染,事件响应。

JS王国:

  • 运行在JS引擎的JS线程上
  • 运行JS代码
  • 负责处理业务逻辑,还包括了应该显示哪个界面,以及如何给页面加样式。

在Native王国中,经过谷歌、苹果公司多年的优化调整,Native代码能够非常快速的运行在设备上。在JS王国中,JS代码作为脚本语言,也能够很快速的运行在JS引擎上,这两边独立来看都不会有性能问题。性能的瓶颈只会出现在从一个王国转入另一个王国时,尤其是频繁的在两个王国之间切换时,两个王国之间不能直接通信,只能通过Bridge做序列化和反序列化,查找模块,调用模块等各种逻辑,最终反应到应用上,就是UI层用户可感知的卡顿。 因此,对React Native的性能控制就主要集中在如何尽量减少Bridge需要处理的逻辑上。

那么,什么情况下会需要Bridge处理逻辑呢?

  1. UI事件响应: 所有的UI事件都发生在Native侧,会以事件的形式传递到JS侧。这个过程非常简单,也不会涉及大量的数据转移。在React Native应用中,业务逻辑,应用状态,数据都在JS侧,所以UI事件只是一个触发器,不会有性能问题。
  2. UI更新:前面已经说过JS负责决定应该展示哪个界面,以及如何样式化界面,因此UI更新的发起方是JS侧,更新时会向Native侧同步大量的UI结构和数据,这类更新经常出现性能问题,尤其是在界面复杂、变动数据大,或者做动画、变动频繁时。
  3. UI事件响应和UI更新同时出现:在UI更新时,结构变化不大,则性能问题不大;但是如果这时又有UI事件触发JS侧逻辑处理,而该逻辑处理又比较复杂,耗时较长,导致JS侧没有时间片处理与Native侧数据同步时,也会发生性能问题。

React Native的性能优化措施

前面已经解释了React Native的性能瓶颈会在什么地方,React Native官方也知道这些,其在React Native中提供了一些性能优化措施帮助开发者克服这些性能问题:

  1. 框架自带的React基于Virtual Dom的Diff算法保证了UI变动时传递的只是变化的UI部分,尽量减少需要同步的数据。
  2. 通过Direct Manipulation的方式直接在底层更新了Native组件的属性,从而避免渲染组件结构和同步太多视图变化所带来的大量开销。这样的确会带来一定的性能提升,同时也会使代码逻辑难以理清,而且并没有解决从JS侧到Native侧的数据同步开销问题。因此这个方式官方都不再推荐,更推荐的做法是合理使用setState()和shouldComponentUpdate()方法解决这类问题。
  3. 在遇到动画性能问题时,可以使用Annimated类的库,一次性把如何变化的声明发送到Native侧,Native侧根据接收到的声明自己负责接下来的UI更新。不需要每帧的UI变化都同步一次数据。
  4. Native和JS混编,把会大量变化的组件做成Native组件,这样UI的变更数据直接在Native侧自己处理了,无需通过Bridge,而不变的内部组件因为没有数据更新需要同步,所以也不会使用到Bridge。框架提供的NavigatorIOS相对于Navigator的性能提升就是这种做法。
  5. 遇到事件响应和UI更新同时发生导致的性能问题时,可以使用Interaction Manager把那些耗时较长的工作安排到所有互动或动画完成之后再进行。

探求性能和效率平衡的套路

在了解了React Native的性能瓶颈和优化措施之后,就可以大概总结一个探寻React Native开发效率和性能平衡点的套路:

第一步: 全JS实现, 从一开始在技术选型上用React Native就是为了保证开发的效率,在没有遇到性能问题之前,最大化效率是团队的一致追求。

第二步: 从JS侧进行性能优化

  • 对于那些明显会涉及Bridge、需大量处理逻辑的场景,比方说动画,复杂的手势操作响应等,尝试使用经过优化过的库(比方说:Animated),一次传递动画或者数据整个数据的描述给Native,Native侧自己会按照声明执行下去。
  • 使用InteractionManager把耗时操作递延到UI响应之后,处理那些存在因为耗时的JS操作导致的UI响应性能问题。

第三步:在真机上实测,检查性能问题点。不要过早优化,找到问题点再一一处理。

第四步:如果经过JS端的优化策略之后,在设备上还是有性能问题,可以把有问题的部分以Native方式实现,这也是为什么要推荐React Native团队中有10%左右的Native Developer。在这个步骤中,需要注意问题的隔离方式,假设一个场景:在移动一个Container时,Container的UI同时发生变化,但是Container内部的内容并没有发生变化,这种情况下,只需要用Native实现Container,Container内部的组件还是以JS实现。


更多精彩洞见,请关注微信公众号:思特沃克

Share

前端组件化开发方案及其在React Native中的运用

文/刘先宁

本文首发于InfoQ:http://www.infoq.com/cn/articles/front-end-component-develop-and-application-in-react-native

随着SPA,前后端分离的技术架构在业界越来越流行,前端(注:本文中的前端泛指所有的用户可接触的界面,包括桌面,移动端)需要管理的内容,承担的职责也越来越多。再加上移动互联网的火爆,及其带动的Mobile First风潮,各大公司也开始在前端投入更多的资源。

这一切,使得业界对前端开发方案的思考上多了很多,以React框架为代表推动的组件化开发方案就是目前业界比较认可的方案,本文将和大家一起探讨一下组件化开发方案能给我们带来什么,以及如何在React Native项目的运用组件化开发方案

一、为什么要采用组件化开发方案?

在讲怎么做之前,需要先看看为什么前端要采用组件化开发方案,作为一名程序员和咨询师,我清楚地知道凡是抛开问题谈方案都是耍流氓。那么在面对随着业务规模的增加,更多的业务功能推向前端,以及随之而来的开发团队扩张时,前端开发会遇到些什么样的问题呢?

1. 前端开发面临的问题

  1. 资源冗余:页面变得越来越多,页面的交互变得越来越复杂。在这种情况下,有些团队成员会根据功能写自己的CSS、JS,这会产生大量的新的CSS或JS文件,而这些文件中可能出现大量的重复逻辑;有些团队成员则会重用别人的逻辑,但是由于逻辑拆分的粒度差异,可能会为了依赖某个JS中的一个函数,需要加载整个模块,或者为了使用某个CSS中的部分样式依赖整个CSS文件,这导致了大量的资源冗余。
  2. 依赖关系不直观:当修改一个JS函数,或者某个CSS属性时,很多时候只能靠人力全局搜索来判断影响范围,这种做法不但慢,而且很容易出错。
  3. 项目的灵活性和可维护性差:因为项目中的交叉依赖太多,当出现技术方案变化时,无法做到渐进式的、有节奏地替换掉老的代码,只能一次性替换掉所有老代码,这极大地提升了技术方案升级的成本和风险。
  4. 新人进组上手难度大:新人进入项目后,需要了解整个项目的背景、技术栈等,才能或者说才敢开始工作。这在小项目中也许不是问题,但是在大型项目中,尤其是人员流动比较频繁的项目,则会对项目进度产生非常大的影响。
  5. 团队协同度不高:用户流程上页面间的依赖(比方说一个页面强依赖前一个页面的工作结果),以及技术方案上的一些相互依赖(比方说某个文件只能由某个团队修改)会导致无法发挥一个团队的全部效能,部分成员会出现等待空窗期,浪费团队效率。
  6. 测试难度大:整个项目中的逻辑拆分不清晰,过多且杂乱的相互依赖都显著拉升了自动化测试的难度。
  7. 沟通反馈慢:业务的要求,UX的设计都需要等到开发人员写完代码,整个项目编译部署后才能看到实际的效果,这个反馈周期太长,并且未来的任何一个小修改又需要重复这一整个流程。

2.组件化开发带来的好处

组件化开发的核心是“业务的归业务,组件的归组件”。即组件是一个个独立存在的模块,它需要具备如下的特征:

(图片来自:http://serenity.bh/wp-content/)

  • 职责单一而清晰:开发人员可以很容易了解该组件提供的能力。
  • 资源高内聚: 组件资源内部高内聚,组件资源完全由自身加载控制。
  • 作用域独立: 内部结构密封,不与全局或其他组件产生影响。
  • 接口规范化: 组件接口有统一规范。
  • 可相互组合: 组装整合成复杂组件,高阶组件等。
  • 独立清晰的生命周期管理:组件的加载、渲染、更新必须有清晰的、可控的路径。

而业务就是通过组合这一堆组件完成User Journey。下一节中,会详细描述采用组件化开发方案的团队是如何运作的。

在项目中分清楚组件和业务的关系,把系统的构建架构在组件化思想上可以:

  1. 降低整个系统的耦合度:在保持接口不变的情况下,我们可以把当前组件替换成不同的组件实现业务功能升级,比如把一个搜索框,换成一个日历组件。
  2. 提高可维护性:由于每个组件的职责单一,在系统中更容易被复用,所以对某个职责的修改只需要修改一处,就可获得系统的整体升级。独立的,小的组件代码的更易理解,维护起来也更容易。
  3. 降低上手难度:新成员只需要理解接口和职责即可开发组件代码,在不断的开发过程中再进一步理解和学习项目知识。另外,由于代码的影响范围仅限于组件内部,对项目的风险控制也非常有帮助,不会因为一次修改导致雪崩效应,影响整个团队的工作。
  4. 提升团队协同开发效率:通过对组件的拆分粒度控制来合理分配团队成员任务,让团队中每个人都能发挥所长,维护对应的组件,最大化团队开发效率。
  5. 便于自动化测试:由于组件除了接口外,完全是自治王国,甚至概念上,可以把组件当成一个函数,输入对应着输出,这让自动化测试变得简单。
  6. 更容易的自文档化:在组件之上,可以采用Living Style Guide的方式为项目的所有UI组件建立一个‘活’的文档,这个文档还可以成为业务,开发,UX之间的沟通桥梁。这是对‘代码即文档’的另一种诠释,巧妙的解决了程序员不爱写文档的问题。
  7. 方便调试:由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题。另外,Living Style Guide除了作为沟通工具,还可以作为调试工具,帮助开发者调试UI组件。

二、组件化开发方案下,团队如何运作?

前面大致讲了下组件化开发可以给项目带来的好处,接下来聊一聊采用组件化开发方案的团队是应该如何运作?

在ThoughtWorks,我们把一个项目的生命周期分为如下几个阶段:

组件化开发方案主要关注的是在迭代开发阶段的对团队效率的提升。 它主要从以下几个方面提升了开发效率:

1. 以架构层的组件复用降低工作量

在大型应用的后端开发中,为了分工、复用和可维护性,在架构层面将应用抽象为多个相对独立的模块的思想和方法都已经非常成熟和深入人心了。

但是在前端开发中,模块化的思想还是比较传统,开发者还是只有在需考虑复用时才会将某一部分做成组件,再加上当开发人员专注在不同界面开发上时,对于该界面上哪些部分可以重用缺乏关注,导致在多个界面上重复开发相同的UI功能,这不仅拉升了整个项目的工作量,还增加了项目后续的修改和维护成本。

在组件化开发方案下,团队在交付开始阶段就需要从架构层面对应用的UI进行模块化,团队会一起把需求分析阶段产生的原型中的每一个UI页面抽象为一颗组件树,UI页面自己本身上也是一个组件。如下图:

通过上面的抽象之后,我们会发现大量的组件可以在多个UI界面上复用,而考虑到在前端项目中,构建各个UI界面占了80%以上的工作量,这样的抽象显著降低了项目的工作量,同时对后续的修改和维护也会大有裨益。

在这样的架构模式下,团队的运作方式就需要相应的发生改变:

  1. 工程化方面的支持,从目录结构的划分上对开发人员进行组件化思维的强调,区分基础组件,业务组件,页面组件的位置,职责,以及相互之间的依赖关系。
  2. 工作优先级的安排,在敏捷团队中,我们强调的是交付业务价值。而业务是由页面组件串联而成,在组件化的架构模式下,必然是先完成组件开发,再串联业务。所以在做迭代计划时,需要对团队开发组件的任务和串联业务的任务做一个清晰的优先级安排,以保证团队对业务价值的交付节奏。

2.以组件的规范性保障项目设计的统一性

在前端开发中,因为CSS的灵活性,对于相同的UI要求(比如:布局上的靠右边框5个像素),就可能有上十种的CSS写法,开发人员的背景,经历的不同,很可能会选择不同的实现方法;甚至还有一些不成熟的项目,存在需求方直接给一个PDF文件的用户流程图界面,不给PSD的情况,所有的设计元素需要开发人员从图片中抓取,这更是会使得项目的样式写的五花八门。因为同样的UI设计在项目中存在多种写法,会导致很多问题,第一就是设计上可能存在不一致的情况;第二是UI设计发生修改时,出现需要多种修改方案的成本,甚至出现漏改某个样式导致bug的问题。

在组件化开发方案下,项目设计的统一性被上拉到组件层,由组件的统一性来保障。其实本来所有的业务UI设计就是组件为单位的,设计师不会说我要“黄色”,他们说得是我要“黄色的按钮……”。是开发者在实现过程中把UI设计下放到CSS样式上的,相比一个个,一组组的CSS属性,组件的整体性和可理解性都会更高。再加上组件的资源高内聚特性,在组件上对样式进行调整也会变得容易,其影响范围也更可控。

在组件化开发方案下,为了保证UI设计的一致性,团队的运作需要:

  1. 定义基础设计元素,包括色号、字体、字号等,由UX决定所有的基础设计元素。
  2. 所有具体的UI组件设计必须通过这些基础设计元素组合而成,如果当前的基础设计元素不能满足需求,则需要和UX一起讨论增加基础设计元素。
  3. UI组件的验收需要UX参与。

3. 以组件的独立性和自治性提升团队协同效率

在前端开发时,存在一个典型的场景就是某个功能界面,距离启动界面有多个层级,按照传统开发方式,需要按照页面一页一页的开发,当前一个页面开发未完成时,无法开始下一个页面的开发,导致团队工作的并发度不够。另外,在团队中,开发人员的能力各有所长,而页面依赖降低了整个项目在任务安排上的灵活性,让我们无法按照团队成员的经验,强项来合理安排工作。这两项对团队协同度的影响最终会拉低团队的整体效率。

在组件化开发方案下,强调业务任务和组件任务的分离和协同。组件任务具有很强的独立性和自治性,即在接口定义清楚的情况下,完全可以抛开上下文进行开发。这类任务对外无任何依赖,再加上组件的职责单一性,其功能也很容易被开发者理解。

所以在安排任务上,组件任务可以非常灵活。而业务任务只需关注自己依赖的组件是否已经完成,一旦完成就马上进入Ready For Dev状态,以最高优先级等待下一位开发人员选取。

在组件化开发方案下,为了提升团队协同效率,团队的运作需要:

  1. 把业务任务和组件任务拆开,组件的归组件,业务的归业务。
  2. 使用Jira,Mingle等团队管理工具管理好业务任务对组件任务的依赖,让团队可以容易地了解到每个业务价值的实现需要的完成的任务。
  3. Tech Lead需要加深对团队每个成员的了解,清楚的知道他们各自的强项,作为安排任务时的参考。
  4. 业务优先原则,一旦业务任务依赖的所有组件任务完成,业务任务马上进入最高优先级,团队以交付业务价值为最高优先级。
  5. 组件任务先于业务任务完成,未纳入业务流程前,团队需要Living Style Guide之类的工具帮助验收组件任务。

4.以组件的Living Style Guide平台降低团队沟通成本

在前端开发时,经常存在这样的沟通场景:

  • 开发人员和UX验证页面设计时,因为一些细微的差异对UI进行反复的小修改。
  • 开发人员和业务人员验证界面流程时,因为一些特别的需求对UI进行反复的小修改。
  • 开发人员想复用另一个组件,寻找该组件的开发人员了解该组件的设计和职责
  • 开发人员和QA一起验证某个公用组件改动对多个界面上的影响

当这样的沟通出现在上一小节的提到的场景,即组件出现在距离启动界面有多个层级的界面时,按照传统开发方式,UX和开发需要多次点击,有时甚至需要输入一些数据,最后才能到达想要的功能界面。没有或者无法搭建一个直观的平台满足这些需求,就会导致每一次的沟通改动就伴随着一次重复走的,很长的路径。使得团队的沟通成本激增,极大的降低了开发效率。

在组件化开发方案下, 因为组件的独立性,构建Living Style Guide平台变得非常简单,目前社区已经有了很多工具支持构建Living Style Guide平台(比如getstorybook)

开发人员把组件以Demo的形式添加到Living Style Guide平台就行了,然后所有与UI组件的相关的沟通都以该平台为中心进行,因为开发对组件的修改会马上体现在平台上,再加上平台对组件的组织形式让所有人都可以很直接的访问到任何需要的组件,这样,UX和业务人员有任何要求,开发人员都可以快速修改,共同在平台上验证,这种“所见即所得”的沟通方式节省去了大量的沟通成本。

此外,该平台自带组件文档功能,团队成员可以从该平台上看到所有组件的UI,接口,降低了人员变动导致的组件上下文知识缺失,同时也降低了开发者之间对于组件的沟通需求。

想要获得这些好处,团队的运作需要:

  1. 项目初期就搭建好Living Style Guide平台。
  2. 开发人员在完成组件之后必须添加Demo到平台,甚至根据该组件需要适应的场景,多添加几个Demo。这样一眼就可以看出不同场景下,该组件的样子。
  3. UX,业务人员通过平台验收组件,甚至可以在平台通过修改组件Props,探索性的测试在一些极端场景下组件的反应。

5. 对需求分析阶段的诉求和产品演进阶段的帮助

虽然需求分析阶段产品演进阶段不是组件化开发关注的重点,但是组件化开发的实施效果却和这两个阶段有关系,组件化方案需要需求分析阶段能够给出清晰的Domain数据结构,基础设计元素和界面原型,它们是组件化开发的基础。而对于产品演进阶段,组件化开发提供的两个重要特性则大大降低了产品演进的风险:

  • 低耦合的架构,让开发者清楚的知道自己的修改影响范围,降低演进风险。开发团队只需要根据新需求完成新的组件,或者替换掉已有组件就可以完成产品演进。
  • Living Style Guide的自文档能力,让你能够很容易的获得现有组件代码的信息,降低人员流动产生的上下文缺失对产品演进的风险。

三、组件化开发方案在React Native项目中的实施

前面已经详细讨论了为什么和如何做组件化开发方案,接下来,就以一个React Native项目为例,从代码级别看看组件化方案的实施。

1. 定义基础设计元素

在前面我们已经提到过,需求分析阶段需要产出基本的设计元素,在前端开发人员开始写代码之前需要把这部分基础设计元素添加到代码中。在React Native中,所有的CSS属性都被封装到了JS代码中,所以在React Native项目开发中,不再需要LESS,SCSS之类的动态样式语言,而且你可以使用JS语言的一切特性来帮助你组合样式,所以我们可以创建一个theme.js存放所有的基础设计元素,如果基础设计元素很多,也可以拆分位多个文件存放。

import { StyleSheet } from 'react-native'; 
module.exports = StyleSheet.create({   
        colors: {...},   
        fonts: {...},   
        layouts: {...},   
        borders: {...},   
        container: {...}, 
  });

然后,在写具体UI组件的styles,只需要引入该文件,按照JS的规则复用这些样式属性即可。

2.拆分组件树之Component,Page,Scene

在实现业务流程前,需要对项目的原型UI进行分解和分类,在React Native项目中,我把UI组件分为了四种类型:

  • Shared Component: 基础组件,Button,Label之类的大部分其它组件都会用到的基础组件
  • Feature Component: 业务组件,对应到某个业务流程的子组件,但其不对应路由, 他们通过各种组合形成了Pag组件。
  • Page: 与路由对应的Container组件,主要功能就是组合子组件,所有Page组件最好名字都以Page结尾,便于区分。
  • Scene: 应用状态和UI之间的连接器,严格意义上它不算UI组件,主要作用就是把应用的状态和Page组件绑定上,所有的Scene组件以Scene后缀结尾。

Component和Page组件都是Pure Component,只接收props,然后展示UI,响应事件。Component的Props由Page组件传递给它,Page组件的Props则是由Scene组件绑定过去。下面我们就以如下的这个页面为例来看看这几类组件各自的职责范围:

(1)searchResultRowItem.js

export default function (rowData) {
    const {title, price_formatted, 
            img_url, rowID, onPress} = rowData;
    const price = price_formatted.split(' ')[0];
    return (
        <TouchableHighlight
          onPress={() => onPress(rowID)}
          testID={'property-' + rowID}
          underlayColor='#dddddd'>
          <View>
           <View style={styles.rowContainer}>
                  <Image style={styles.thumb} source={{ uri: img_url }}/>
              <View style={styles.textContainer}>
                    <Text style={styles.price}>{price}</Text>
                    <Text style={styles.title} numberOfLines={1}>{title}</Text>
              </View>
            </View>
            <View style={styles.separator }/>
        </View>
           </TouchableHighlight>
  );}

(2)SearchResultsPage.js

import SearchResultRowItem from '../components/searchResultRowItem';
export default class SearchResultsPage extends Component {

  constructor(props) {
    super(props);
    const dataSource = new ListView.DataSource({
      rowHasChanged: (r1, r2) => r1.guid !== r2.guid});
    this.state = {
      dataSource: dataSource.cloneWithRows(this.props.properties),
      onRowPress: this.props.rowPressed,
    };
  }

  renderRow(rowProps, sectionID, rowID) {
    return <SearchResultRowItem {...rowProps} rowID={rowID}
                onPress={this.state.onRowPress} />;
  }

  render() {
    return (
      <ListView
        style={atomicStyles.container}
        dataSource={this.state.dataSource}
        renderRow={this.renderRow.bind(this)} />
    );
  }}

(3)SearchResultsScene.js

import SearchResults from '../components/searchResultsPage';
function mapStateToProps(state) {
  const {propertyReducer} = state;
  const {searchReducer:{properties}} = propertyReducer;
  return {
    properties,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    rowPressed: (propertyIndex) => {
      dispatch(PropertyActions.selectProperty(propertyIndex));
      RouterActions.PropertyDetails();
    }
  };
}

module.exports = connect(
  mapStateToProps,
  mapDispatchToProps,)(SearchResults);

3.Living Style Guide

目前社区上,最好的支持React Native的Living Style Guide工具是getstorybook,关于如何使用getstorybook搭建React Native的Living Style Guide平台可以参见官方文档或者我的博客

搭建好Living Style Guide平台后,就可以看到如下的界面:

接下来的工作就是不断在往该平台添加UI组件的Demo。向storybook中添加Demo非常简单,下面就是一个关于SearchPage的Demo:

import React from 'react';
import {storiesOf, action} from '@kadira/react-native-storybook';
import SearchPage from '../../../../src/property/components/searchPage';
storiesOf('Property', module)
  .add('SearchPage', () => (
    <SearchPage request={{place_name:"London"}} 
        isLoading={false} search={action('Search called')}/>
));

从上面的代码可以看出,只需要简单的三步就可以完成一个UI组件的Demo:

  1. import要做Demo的UI组件。
  2. storiesOf定义了一个组件目录。
  3. add添加Demo。

在构建项目的storybook时,一些可以帮助我们更有效的开发Demo小Tips:

  1. 尽可能的把目录结构与源代码结构保持一致。
  2. 一个UI组件对应一个Demo文件,保持Demo代码的独立性和灵活性,可以为一个组件添加多个Demo,这样一眼就可以看到多个场景下的Demo状态。
  3. Demo命名以UI组件名加上Demo缀。
  4. 在组件参数复杂的场景下,可以单独提供一个fakeData的目录用于存放重用的UI组件Props数据。

4.一个完整的业务开发流程

在完成了上面三个步骤后,一个完整的React Native业务开发流程可简单分为如下几步:

  1. 使用基础设计元素构建基础组件,通过Living Style Guide验收。
  2. 使用基础组件组合业务组件,通过Living Style Guide验收。
  3. 使用业务组件组合Page组件,通过Living Style Guide验收。
  4. 使用Scene把Page组件的和应用的状态关联起来。
  5. 使用Router把多个Scene串联起来,完成业务流程。

四、总结

随着前后端分离架构成为主流,越来越多的业务逻辑被推向前端,再加上用户对于体验的更高要求,前端的复杂性在一步一步的拔高。对前端复杂性的管理就显得越来越重要了。经过前端的各种框架,工具的推动,在前端工程化实践方面我们已经迈进了很多。而组件化开发就是笔者觉得其中比较好的一个方向,因为它不仅关注了当前的项目交付,还指导了团队的运作,帮助了后期的演进,甚至在程序员最讨厌的写文档的方面也给出了一个巧妙的解法。希望对该方法感兴趣的同学一起研究,改进。


更多精彩洞见,请关注微信公众号:思特沃克

Share