ReactNative的組件架構設計

還有一篇較早的文章,也是分析的過程,能夠對本篇文章進行一個補全:RN組件狀態設計思考:http://segmentfault.com/a/1190000004180955html

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

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

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

考慮目前遇到的以下問題,但願架構給出解決方案。git

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

  2. 職責:組件狀態放哪,業務邏輯放哪,數據放哪,由於太靈活了,怎麼作均可以實現功能,可是怎麼作纔是最好的,纔是最正確的呢?github

todo一個問題:因爲react是面向狀態編程,至關於react的組件只關注數據的最終狀態,數據是怎麼產生的並不關心,可是某些場景下,數據如何產生的是會影響到組件的一些行爲的【好比一個新增行要求有動畫效果,查詢出的行就不須要等】,這在RN中很難描述。。。。。web

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

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

Flux

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

  1. action 封裝請求

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

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

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

clipboard.png

數據流動:
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

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

clipboard.png

特性:

  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.html

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

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

Relay

沒有時間,沒作研究

Marty

沒有時間,沒作研究

結論

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

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

介於上述二者之間的開源架構reflux,文檔較上述2個少,不知道其可持續性如何。若是必定要用開源架構的話,我以爲他稍加封裝是一個較爲推薦的選擇。

不是特複雜的程序【通常spa的程序會更復雜一些,而RN並非spa】,這些概念只會增長你的開發難度,而且對後面維護的人要求更高。

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

  1. 【解決職責】flux系列框架都作到了解耦,分層,誰該幹什麼就幹什麼,不準幹別的,讓代碼讀起來更有預測性和一致性,方便維護

  2. 【解決通訊】繼續解耦,flux系列框架採用事件機制解決各層之間通訊,採用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. root組件,1個,

    1. 負責初始化state

    2. 負責提供對外props列表

    3. 負責組合子view組件造成頁面效果

    4. 負責註冊業務邏輯對象提供的業務邏輯方法

    5. 負責管理業務邏輯對象

  2. view子組件,0-n個,

    1. 根據props進行視圖的渲染

  3. 業務邏輯對象,0-n個,

    1. 提供業務邏輯方法

root組件

root組件由如下元素組成:

  1. props-公有屬性

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

  3. 私有屬性

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

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

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

clipboard.png

注意:定義root組件的state的時候,若是使用es6的方式,要把state的初始化放到componentWillMount中,若是在構造器中this.props爲空。

子view組件

子view組件中包含:

  1. props-公有屬性

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

    1. 絕對不容許和父組件的屬性or狀態有冗餘。不管是顯性冗餘仍是計算結果冗餘,除非你能肯定結算是性能的瓶頸。

    2. 此屬性只有本身會用,父組件和兄弟組件不會使用,若是你不肯定這點,請把這個組件放到父組件上,方便組件間通訊

  3. 私有方法-僅做爲渲染view的使用,不準有業務邏輯

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

clipboard.png

業務邏輯對象

業務邏輯對象由如下元素組成:

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

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

  3. 私有屬性

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

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

clipboard.png

ps1:通用型組件只要求儘可能知足上述架構設計

通用型組件通常爲不包含任何業務的純技術組件,具備高複用價值、高定製性、一般不能直接使用須要代碼定製等特色。

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

架構的最終目的是保證系統總體結構良好,代碼質量良好,易於維護。通常編寫通用型組件的人也是經驗較爲豐富的工程師,代碼質量會有保證。並且,做爲零件的通用組件的使用場景和生命週期都和普通組件\頁面不一樣,因此,僅要求通用組件編寫儘可能知足架構設計便可。

ps2:view子組件複用問題

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

  1. 需複用,只暴露props,能夠內部自行管理state【儘可能避免除非業務須要】

  2. 不需複用,只暴露props,內部無state【由於不會單獨使用,不須要setState來觸發渲染】

其實, 通常按照不需複用的狀況設計,除非複用很明確,但這時候應該抽出去,變成獨立的組件存在就能夠了,因此這個問題是不存在的。

適用場景分析

flux系列框架

flux系列框架的適用場景我以爲應具備如下特色:

一個頁面中組件較多,組件之間較爲獨立,可是重疊使用模型,模型的變化會致使不少組件的展示和行爲。

好比,開發一個相似qq的聊天頁面,左側是聯繫人列表,右側是與某人的消息對話框,當收到一個消息以後,1要刷新左側聯繫人列表的最近聯繫人,2要右側的消息對話框中顯示這個消息,3要頁面title要提示新消息。這就是典型的一個新消息到來事件【消息模型發生了變化】觸發三個無關聯的組件都有行爲和展示的變化。若是用事件系統來開發就作到了解耦的極致,將來若是還要加入第4種處理也不用修改原來的邏輯,就直接註冊一下就能夠了,知足了開閉原則。

須要對app運行過程進行監控,數據採樣等

flux系列框架是一個典型的門面模式,業務動做都要經過統一的門面dispatch進行,天生具備良好的監控解決方案。

面向對象的RN組件架構

面向對象的RN組件架構的使用場景特色我沒有總結出來,我以爲全部場景均可以用,只要你業務邏輯對象設計的好,都不是問題。

還拿上面聊天界面舉例子,面向對象的RN組件架構其實也能夠解耦的寫出寫上述場景,你徹底能夠將業務邏輯對象之間的交互設計成一個小的事件系統,只是架構沒有直接約束這種解耦,flux系列架構直接在架構中就強制編碼人員作到了解耦,可是若是我不須要解耦的時候就至關於增長了複雜度,得不償失了。

因此面向對象的RN組件架構要更靈活,也更簡單更容易讓人理解,更容易預測代碼的執行流向,但同時由於靈活對業務邏輯對象設計者的要求也較高,針對較爲複雜or重要頁面建議進行詳細設計並leader檢查來保證質量。

如何作監控

由於面向對象的RN架構中去掉了統一的業務邏輯調用facade入口dispatch,那咱們如何來作監控呢。

方案1:在須要監控的地方人爲加入監控點。

這個方案對業務代碼和監控代碼的耦合確實有點大,是最差的解決方案了。不推薦。

方案2:在基類BaseLogicObj的構造器中對對象的全部方法進行代理-todo待驗證

這個方案對業務代碼透明,可是還只是個想法,未進行代碼測試和驗證。

方案3.....尚未想出別的方案,有沒有同窗給點思路?

架構之美

最後在分享demo代碼以前,摘抄了天貓前端架構師團隊對架構的認識,我的以爲十分認同。

簡單:
簡單的東西才能長久,HTML、CSS和JavaScript之因此可以活到如今,而其餘相似的很牛的方案都死掉了,緣由之一是簡單,纔有那麼多人用它,因此咱們須要把技術和產品方案朝着簡單的思路發展,簡單纔是本質,複雜必定是臨時的會過期的。天貓的前端技術架構爲何都基於Kissy,爲何是兩層架構,就是朝着簡單的方式去思考。看起來簡單,用起來簡單。

高效:
簡單是高效的前提,複雜的高效都是臨時的會過期的,技術架構必定要提升團隊的工做效率,不然必定會被拋棄,所以把簡單的規則自動化,把精確的重複的事情讓機器去作,前端這麼多年爲何開發環境不夠成熟,就是自動化的工具太少,前端又不多能力駕馭編寫工具的語言,而Nodejs的出現是一個史無前例的機會。

靈活:
高效每每和靈活是對立的,就像移動上Native和Web的關係,而咱們就須要思考如何作到二者兼顧,既高效又靈活,因此要不斷把事情作簡單,思考本質、看到本質,基於本質去實現。好比Apple爲何勇於把鼠標和鍵盤去掉,是由於確信人直接和界面打交道比藉助一箇中間硬件更可以表達人機交互的本質。

新鮮:
面向將來,前端須要不停地更新本身,不管是思想仍是技術。好比整個天貓基於Kissy,那麼就使用最新的Kissy版本,基礎設施可以升級是一種能力,若是有一天基礎設施升不了啦,那麼這套技術架構就老去了。好比發現Gulp比Grunt更可以表明將來,那麼咱們絕不猶豫地整個團隊開始進行升級。

完整demo代碼

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

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

demo截圖:

clipboard.png

todolist頁面:

'use strict'


let React=require('react-native');
let Immutable = require('immutable');
var BbtRN=require('../../../bbt-react-native');


var {
    BaseLogicObj,
    }=BbtRN;


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);

        //下面能夠繼續作一些組件初始化動做,好比請求數據等.
        //固然了這些動做最好是業務邏輯對象提供的,這樣root組件將很是乾淨.
        //例如這樣:this.todoListObj.queryData();
    },


    //狀態初始化
    getInitialState(){
      return {
          data:Immutable.fromJS(this.data),//模擬的初始化數據
          todoName:'',//新任務的text
          curFilter:'all',//過濾條件 all no ok
      }
    },



    //這裏組合子view組件 並 註冊業務邏輯對象提供的方法到各個子view組件上
    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的設計方式設計成多個對象

//業務邏輯對象要符合命名規範:以Obj結尾
//BaseLogicObj是架構提供的基類,裏面封裝了構造器和一些經常使用取值函數
class AddTodoObj extends BaseLogicObj{

    press(){
        if(!this.getState().todoName)return;
        let list=this.getState().data;
        let todo=Immutable.fromJS({name:this.getState().todoName,completed:false,});
        this.setState({data:list.push(todo),todoName:''});
    }

    change(e){
        this.setState({todoName:e.nativeEvent.text});
    }

}


class TodoListObj extends BaseLogicObj {




    pressTodo(todo){

        let data=this.getState().data;

        let i=data.indexOf(todo);

        let todo2=todo.set('completed',!todo.get('completed'));

        this.setState({data:data.set(i,todo2)});
    }
}


class FilterObj extends BaseLogicObj {


    filter(type){

        let data=this.getState().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.setState({curFilter:type,data:Immutable.fromJS(data)});
    }



}


//view子組件開始---------------------------


//子view對象中僅僅關注:從this.props轉化成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;

業務邏輯對象基類BaseLogicObj:

'use strict'

class BaseLogicObj{


    constructor(root){
        if(!root){
            console.error('實例化BaseLogicObj必須傳入root組件對象.');
        }
        this.root=root;
    }

    getState(){
        return this.root.state;
    }

    setState(s){
        this.root.setState(s);
    }

    getRefs(){
        return this.root.refs;
    }

    getProps(){
        return this.root.props;
    }

}

module.exports=BaseLogicObj;
相關文章
相關標籤/搜索