ReactNative的架構設計

jopen 8年前發布 | 63K 次閱讀 JavaScript開發 ReactNative

原文 http://segmentfault.com/a/1190000004161358

ReactNative的架構設計

請注意,本篇寫的是react native的架構設計,如果你用react來開發web程序,本篇文章只能僅供參考,問題都沒有在web上去考慮過。

本篇較長,前面是目前flux開源框架的一些分析,后面是架構設計過程。您可以直奔主題。

用RN最大的難題是設計思想的轉變,以前的設計方法論已經不太適用了。而RN僅僅提供了view的框架,構建完整app的架構并沒有直接提供。

考慮目前遇到的如下問題,希望架構給出解決方案。

  1. 交互:如何解決組件間通信【父子、子父、兄弟等,特別是跨層or反向數據流動等】;用state還是接口操作組件;

    </li>

  2. 職責:組件狀態放哪,業務邏輯放哪,數據放哪,因為太靈活了,怎么做都可以實現功能,但是怎么做才是最好的,才是最正確的呢?

    </li> </ol>

    `

    todo一個問題:由于react是面向狀態編程,相當于react的組件只關注數據的最終狀態,數據是怎么產生的并不關心,但是某些場景下,數據如何產生的是會影響到組件的一些行為的【比如一個新增行要求有動畫效果,查詢出的行就不需要等】,這在RN中很難描述。。。。。

    `

    </div>

    RN架構就是為解決上述問題提供的指導和方法論,是通盤考慮整個開發、測試、運維的狀況,做出的考慮最全面的抉擇,或者為抉擇提供依據。

    目前為react服務的架構也有一些了,如Flux,Reflux,Redux,Relay,Marty。

    Flux

    flux是官方提供的架構,目的是分層解耦,職責劃分清晰,誰負責干啥很明確。具體描述可以參考官方文檔,這里不詳述。

    1. action 封裝請求

      </li>

    2. dispatcher 注冊處理器、分發請求

      </li>

    3. store 是處理器,處理業務邏輯,保存數據

      </li>

    4. view 根據store提供的數據進行展現;接受用戶的輸入并發出action請求。

      </li> </ol>

      ?

      數據流動:Action-> Dispatcher -> Store -> Component

      但我覺得解耦的太細了,干一個事,要做太多太多的額外工作了。

      光注冊監聽動作就2次,一次是store注冊到dispatcher,一次是view注冊到store中。

      而且,注冊到dispatcher的監聽應該都不叫注冊,架構完全沒有提供任何封裝,直接暴露一個統一的回調方法,里面自行if else路由不同的store。

      Reflux

      結構上與flux架構基本一致,去掉了flux的一些冗余操作【比如沒有了dispatcher】,架構更加簡潔和緊湊,用到了一些約定大于配置的理念。

      基本上將flux的架構冗余都簡化了,可以說是flux的去冗余提升版,但是沒有本質的變化。

      ╔═════════╗       ╔════════╗       ╔═════════════════╗
      ║ Actions ║──────>║ Stores ║──────>║ View Components ║
      ╚═════════╝       ╚════════╝       ╚═════════════════╝
           ^                                      │
           └──────────────────────────────────────┘
      1. 更容易的監聽。listenables和約定以on開頭的方法。等。

      2. 去掉了dispatcher。

      3. action可以進行aop編程。

      4. 去掉了waitfor。store可以監聽store。

      5. component提供了一系列mixin,方便注冊\卸載到store的監聽和與store交互等。

      Redux

      社區內比較受推崇,因為用起來相對比較簡單

      ?

      特性:

      1. 分層設計,職責清晰。

      2. 要求store reducer都是頁面單例,易于管理。

      3. action為請求dto對象,是請求類型,請求數據的載體。

      4. reducer是處理請求的方法。不允許有狀態,必須是純方法。必須嚴格遵守輸入輸出,中間不允許有異步調用。不允許對state直接進行修改,要想修改必須返回新對象。

      5. store

        1. 維持應用的state;

        2. 提供 getState() 方法獲取 state;

        3. 提供 dispatch(action) 方法分發請求來更新 state;門面模式,要求所有的請求滿足統一的格式【可以進行路由、監控、日志等】,統一的調用方式。

        4. 通過 subscribe(listener) 注冊監聽器監聽state的變化。

      6. 官方文檔寫的較為詳細,從設計到開發都有,比flux要好

      痛處如下,看能否接受或者解決:

      1. redux的原則1:state不能被修改。

        1. 其實這個用react的state也會有同樣的問題,最好把state設計的沒有冗余,盡量少出這種情況

        2. 解決方案:參考官方:因為我們不能直接修改卻要更新數組中指定的一項數據,這里需要先把前面和后面都切開。如果經常需要這類的操作,可以選擇使用幫助類 React.addons.update,updeep,或者使用原生支持深度更新的庫 Immutable。最后,時刻謹記永遠不要在克隆 state 前修改它。

      2. 單一的龐大的reducer的拆分

        1. 這塊設計也不好做,會讓人疑惑

        2. 官方給的demo中直接按state的內容區分,我覺得這樣做不好,如果后期有跨內容的情況,就比較奇怪了。官方給的combineReducers方案,也只是減少代碼量,本質沒有變化,state還是拆分處理,路由還是業務邏輯自己來做。

        3. 解決方案:還是處理一整個state,可以按照約定寫reducer類而不是方法,類里按照actionType建方法,架構自動路由并調用。

        4. 以前做java架構,路由一定是架構來調用的,目前感覺各大flux框架都是解決問題不徹底。

      3. 官方建議設計模式:頂層容器組件才對redux有依賴,組件間通過props來傳遞數據。按照這樣設計還是沒有解決組件間交互和數據傳遞的問題。官方react設計建議:react的設計建議: http://camsong.github.io/redux-in-chinese/docs/basics/UsageWithReact.htm

      4. 使用connect將state綁定到component。此處有些黑盒了。

      5. 異步action用來請求服務端數據,利用middleware增強createStore的dispatch后即支持。

      Relay

      沒有時間,沒做研究

      Marty

      沒有時間,沒做研究

      結論

      開源架構封裝的簡單的flux會產生較多的冗余代碼。

      開源架構封裝的復雜的redux,其和RN綁定封裝了一些東西,是一個黑盒,不易理解和維護。

      介于上述兩者之間的開源架構reflux,文檔較上述2個少,不知道其可持續性如何。如果一定要用開源架構的話,我覺得他稍加封裝是一個較為推薦的選擇。

      不是特復雜的程序【一般spa的程序會更復雜一些,而RN并不是spa】,這些概念只會增加你的開發難度,并且對后面維護的人要求更高。

      我們繼續頭腦風暴,繼續抽象總結一下flux系列框架, flux系列框架干了什么,沒干什么,針對開篇提出的問題。

      1. 【解決職責】解耦,分層,誰該干什么就干什么,不許干別的,讓代碼讀起來更有預測性和一致性,方便維護

      2. 【解決通信】繼續解耦,采用事件機制解決各層之間通信,采用props傳遞解決各組件之間通信。

      事件系統是關鍵

      flux系列架構解決通信問題的方法是使用事件系統,事件系統中的回調函數是業務邏輯,redux是【store action reducer】,flux是【action dispacher store】。

      我們真的需要事件系統嗎?

      事件系統的好處:

      1. 一個事件可以注冊多個回調函數

      2. 各回調函數間沒有耦合。

      關于1

      需要注冊多個的這種情況并不多見,不信你去翻看你已經寫好的代碼,是不是大部分都是注冊一個。

      關于2

      解耦確實很徹底,但是當我需要控制執行順序,需要等a執行完在執行b,怎么辦?ok你可以先注冊a在注冊b啊。那a要是一個fetch或ajax操作呢?這時候只能乖乖的在a的請求結束回調函數中進行調用b了。又變成a依賴b了。當然,你可以繼續dispatch(b),這就沒有耦合了。但是你要知道注冊一個事件是要有成本的,要寫action,而且大部分情況根本就不需要注冊多個回調,而且這種dispatch的方式,真的不太適合人類的閱讀,dispatch一下,下一步都有誰來執行都不知道,這哪有直接調用來的爽快。

      好吧說到這,最后的結論也出來了,不使用開源架構,借助其好的思想,替換其事件系統為面向對象結構,自行封裝架構。

      架構設計

      再次強調:目前僅考慮如何應用于react native

      先扣題,針對開篇問題的解決方案如下

      交互

      1. 組件對外發布:組件對外只允許使用props來暴露功能,不允許使用接口及其它一切方式

      2. 父子組件間:組件的子組件通過父組件傳遞的接口來與父組件通信

      3. 兄弟組件間:

        1. 方案1:假設a要調用b,參考第一條的話,其實就是a要改變b的props,那么a只要改b的props的來源即可,b的props的來源一般就是根組件的state。那么根組件就要有組織和協調的能力。

        2. 方案2:利用事件機制,基本同flux架構。略復雜,且我們并不需要事件的特性,本架構設計不推薦。

      職責

      1. root-存放state,組織子view組件,組織業務邏輯對象等

      2. 子view組件-根據this.props渲染view。

      3. 業務邏輯對象-提供業務邏輯方法

      根據以上推導,我將其命名為面向對象的ReactNative架構設計,它與flux系列架構的最大的不同之處在于,用業務邏輯對象來代替了【store action dispatcher】or【store reducer】的事件系統。業務邏輯對象就是一組對象,用面向對象的設計理念設計出的n個對象,其負責處理整個頁面的業務邏輯。

      以上為推導過程,干貨才開始。。。。

      面向對象的ReactNative組件\頁面架構設計

      1. 一個獨立完整的組件\頁面一般由以下元素構成

        1. root組件,1個,

            1. 負責初始化state
            2. 負責提供對外props列表
            2. 負責組合子view組件形成頁面效果
            3. 負責注冊業務邏輯對象提供的業務邏輯方法
            4. 負責管理業務邏輯對象
        2. view子組件,0-n個,

          1. 根據props進行視圖的渲染
        3. 業務邏輯對象,0-n個,

          2. 提供業務邏輯方法
      2. root組件,中包含:

        1. props-公有屬性

        2. state-RN體系的狀態,必須使用Immutable對象

        3. 私有屬性

        4. 業務邏輯對象的引用-在componentWillMount中初始化

        5. 私有方法-以下劃線開頭,內部使用or傳遞給子組件使用

        6. 公有方法【不推薦】,子組件和外部組件都可以用,但不推薦用公有方法來對外發布功能,破壞了面向狀態編程,盡可能的使用props來發布功能

      3. 子view組件,中包含:

        1. props-公有屬性

        2. 私有屬性-如果你不能理解下面的要求,建議沒有,統一放在父組件上

        3. 絕對不允許和父組件的屬性or狀態有冗余。無論是顯性冗余還是計算結果冗余,除非你能確定結算是性能的瓶頸。

        4. 此屬性只有自己會用,父組件和兄弟組件不會使用,如果你不確定這點,請把這個組件放到父組件上,方便組件間通信

        5. 私有方法-僅作為渲染view的使用,不許有業務邏輯

        6. 公有方法【不推薦,理由同root組件】

      4. 業務邏輯對象,中包含:

        1. root組件對象引用-this.root

        2. 構造器-初始化root對象,初始化私有屬性

        3. 私有屬性

        4. 公有方法-對外提供業務邏輯

        5. 私有方法-以下劃線開頭,內部使用

        todo補充架構圖

      通用型組件只要求盡量滿足上述架構設計

      通用型組件一般為不包含任何業務的純技術組件,具有高復用價值、高定制性、通常不能直接使用需要代碼定制等特點。

      可以說是一個系統的各個基礎零件,比如一個蒙板效果,或者一個模態彈出框。

      架構的最終目的是保證系統整體結構良好,代碼質量良好,易于維護。一般編寫通用型組件的人也是經驗較為豐富的工程師,代碼質量會有保證。而且,作為零件的通用組件的使用場景和生命周期都和普通組件\頁面不同,所以,僅要求通用組件編寫盡量滿足架構設計即可。

      view子組件復用問題

      拋出一個問題,設計的過程中,子組件是否需要復用?子組件是否需要復用會影響到組件設計。

      1. 需復用,只暴露props,可以內部自行管理state【盡量避免除非業務需要】

      2. 不需復用,只暴露props,內部無state【因為不會單獨使用,不需要setState來觸發渲染】

      其實, 一般按照不需復用的情況設計,除非復用很明確,但這時候應該抽出去,變成獨立的組件存在就可以了,所以這個問題是不存在的。

      面向對象的ReactNative架構設計優缺點--todo

      按場景分析、驗證架構設計--todo

      觸發view改變的場景【即需要setState的場景】

      1. 回調函數觸發【異步】

        1. 組件生命周期事件中or用戶操作觸發 啟動請求服務器

        2. 組件生命周期事件中or用戶操作觸發,啟動定時任務or注冊了其它回調函數【比如交互管理器的動畫結束事件】

      2. 用戶操作觸發

        1. view的直接改變【同步】

        2. 僅僅注冊回調函數【異步,參考上面一條】

      demo代碼

      此demo仿照redux提供的todolist demo編寫。

      redux demo 地址: http://camsong.github.io/redux-in-chinese/docs/basics/ExampleTodoList.html

      demo截圖:

      ?

      'use strict'
      
      
      let React=require('react-native');
      let Immutable = require('immutable');
      let {
          AppRegistry,
          Component,
          StyleSheet,
          Text,
          View,
          Navigator,
          TouchableHighlight,
          TouchableOpacity,
          Platform,
          ListView,
          TextInput,
          ScrollView,
          }=React;
      
      //root組件開始-----------------
      
      let  Root =React.createClass({
      
          //初始化模擬數據,
          data:[{
              name:'aaaaa',
              completed:true,
          },{
              name:'bbbbb',
              completed:false,
          },{
              name:'ccccc',
              completed:false,
          }
          ,{
              name:'ddddd',
              completed:true,
          }],
      
      
          componentWillMount(){
              this.addTodoObj=new AddTodoObj(this);
              this.todoListObj=new TodoListObj(this);
              this.filterObj=new FilterObj(this);
          },
      
      
          getInitialState(){
            return {
                data:Immutable.fromJS(this.data),//模擬的初始化數據
                todoName:'',//新任務的text
                curFilter:'all',//過濾條件 all no ok
            }
          },
      
      
      
          render(){
              return (
                  <View style={{marginTop:40,flex:1}}>
      
                      <AddTodo todoName={this.state.todoName}
                              changeText={this.addTodoObj.change.bind(this.addTodoObj)}
                               pressAdd={this.addTodoObj.press.bind(this.addTodoObj)} />
      
                      <TodoList todos={this.state.data}
                                onTodoPress={this.todoListObj.pressTodo.bind(this.todoListObj)} />
      
                      <Footer curFilter={this.state.curFilter}
                          onFilterPress={this.filterObj.filter.bind(this.filterObj)} />
      
                  </View>
              );
          },
      
      
      
      });
      
      
      
      
      
      
      //業務邏輯對象開始-------------------------可以使用OO的設計方式設計成多個對象
      
      
      class AddTodoObj{
      
          constructor(root){
              this.root=root;
          }
      
      
          press(){
              if(!this.root.state.todoName)return;
              let list=this.root.state.data;
              let todo=Immutable.fromJS({name:this.root.state.todoName,completed:false,});
              this.root.setState({data:list.push(todo),todoName:''});
          }
      
          change(e){
              this.root.setState({todoName:e.nativeEvent.text});
          }
      
      }
      
      
      class TodoListObj{
      
          constructor(root){
              this.root=root;
          }
      
      
          pressTodo(todo){
      
              let data=this.root.state.data;
      
              let i=data.indexOf(todo);
      
              let todo2=todo.set('completed',!todo.get('completed'));
      
              this.root.setState({data:data.set(i,todo2)});
          }
      }
      
      
      class FilterObj{
      
          constructor(root){
              this.root=root;
          }
      
      
          filter(type){
      
              let data=this.root.state.data.toJS();
              if(type=='all'){
                  data.map((todo)=>{
                      todo.show=true;
                  });
              }else if(type=='no'){
                  data.map((todo)=>{
                      if(todo.completed)todo.show=false;
                      else todo.show=true;
                   });
              }else if(type=='ok'){
                  data.map((todo)=>{
                      if(todo.completed)todo.show=true;
                      else todo.show=false;
                  });
              }
      
      
              this.root.setState({curFilter:type,data:Immutable.fromJS(data)});
          }
      
      
      
      }
      
      
      //view子組件開始---------------------------
      
      
      let Footer=React.createClass({
      
          render(){
      
              return (
      
      
                  <View style={{flexDirection:'row', justifyContent:'flex-end',marginBottom:10,}}>
      
                      <FooterBtn {...this.props} title='全部' name='all'  cur={this.props.curFilter=='all'?true:false} />
                      <FooterBtn {...this.props} title='未完成' name='no' cur={this.props.curFilter=='no'?true:false} />
                      <FooterBtn {...this.props} title='已完成' name='ok' cur={this.props.curFilter=='ok'?true:false} />
      
                  </View>
      
      
      
              );
          },
      
      
      });
      
      
      let FooterBtn=React.createClass({
      
          render(){
      
              return (
      
                  <TouchableOpacity onPress={()=>this.props.onFilterPress(this.props.name)}
                                    style={[{padding:10,marginRight:10},this.props.cur?{backgroundColor:'green'}:null]} >
                      <Text style={[this.props.cur?{color:'fff'}:null]}>
                          {this.props.title}
                      </Text>
                  </TouchableOpacity>
      
              );
          },
      
      
      });
      
      
      let AddTodo=React.createClass({
      
          render(){
      
              return (
      
      
                  <View style={{flexDirection:'row', alignItems:'center'}}>
      
      
                      <TextInput value={this.props.todoName}
                          onChange={this.props.changeText}
                          style={{width:200,height:40,borderWidth:1,borderColor:'e5e5e5',margin:10,}}></TextInput>
      
      
                      <TouchableOpacity onPress={this.props.pressAdd}
                          style={{backgroundColor:'green',padding:10}} >
                          <Text style={{color:'fff'}} >
                              添加任務
                          </Text>
                      </TouchableOpacity>
      
                  </View>
      
      
      
              );
          },
      
      
      });
      
      
      
      let Todo=React.createClass({
      
          render(){
              let todo=this.props.todo;
              return (
                  todo.get("show")!=false?
                  <TouchableOpacity  onPress={()=>this.props.onTodoPress(todo)}
                      style={{padding:10,borderBottomWidth:1,borderBottomColor:'#e5e5e5'}}>
                      <Text style={[todo.get('completed')==true?{textDecorationLine:'line-through',color:'#999'}:null]} >
                          {todo.get('completed')==true?'已完成   ':'未完成   '} {todo.get('name')}
                      </Text>
                  </TouchableOpacity>
                   :null
              );
          },
      
      
      });
      
      
      let TodoList=React.createClass({
          render(){
              return (
                  <ScrollView style={{flex:1}}>
                      {this.props.todos.reverse().map((todo, index) => <Todo {...this.props} todo={todo} key={index}  />)}
                  </ScrollView>
              );
          },
      });
      
      
      
      
      module.exports=Root;
     本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!