ReactNative的組件架構設計

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

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

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

  • 交互:如何解決組件間通信【父子、子父、兄弟等,特別是跨層or反向數據流動等】;用state還是接口操作組件;
  • 職責:組件狀態放哪,業務邏輯放哪,數據放哪,因為太靈活了,怎么做都可以實現功能,但是怎么做才是最好的,才是最正確的呢?

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

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

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

Flux

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

  • action 封裝請求
  • dispatcher 注冊處理器、分發請求
  • store 是處理器,處理業務邏輯,保存數據
  • view 根據store提供的數據進行展現;接受用戶的輸入并發出action請求。

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

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

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

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

Reflux

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

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

╔═════════╗       ╔════════╗       ╔═════════════════╗
║ Actions ║──────>║ Stores ║──────>║ View Components ║
╚═════════╝       ╚════════╝       ╚═════════════════╝
     ^                                      │
     └──────────────────────────────────────┘
  • 更容易的監聽。listenables和約定以on開頭的方法。等。
  • 去掉了dispatcher。
  • action可以進行aop編程。
  • 去掉了waitfor。store可以監聽store。
  • component提供了一系列mixin,方便注冊\卸載到store的監聽和與store交互等。

Redux

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

特性:

1、分層設計,職責清晰。
2、要求store reducer都是頁面單例,易于管理。
3、action為請求dto對象,是請求類型,請求數據的載體。
4、reducer是處理請求的方法。不允許有狀態,必須是純方法。必須嚴格遵守輸入輸出,中間不允許有異步調用。不允許對state直接進行修改,要想修改必須返回新對象。
5、store

  • 維持應用的state;
  • 提供 getState() 方法獲取 state;
  • 提供 dispatch(action) 方法分發請求來更新 state;門面模式,要求所有的請求滿足統一的格式【可以進行路由、監控、日志等】,統一的調用方式。
  • 通過 subscribe(listener) 注冊監聽器監聽state的變化。

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

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

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

  • 其實這個用react的state也會有同樣的問題,最好把state設計的沒有冗余,盡量少出這種情況
  • 解決方案:參考官方,因為我們不能直接修改卻要更新數組中指定的一項數據,這里需要先把前面和后面都切開。如果經常需要這類的操作,可以選擇使用幫助類 React.addons.update,updeep,或者使用原生支持深度更新的庫 Immutable。最后,時刻謹記永遠不要在克隆 state 前修改它。

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

  • 這塊設計也不好做,會讓人疑惑
  • 官方給的demo中直接按state的內容區分,我覺得這樣做不好,如果后期有跨內容的情況,就比較奇怪了。官方給的combineReducers方案,也只是減少代碼量,本質沒有變化,state還是拆分處理,路由還是業務邏輯自己來做。
  • 解決方案:還是處理一整個state,可以按照約定寫reducer類而不是方法,類里按照actionType建方法,架構自動路由并調用。
  • 以前做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后即支持。

結論

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

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

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

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

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

  • 【解決職責】flux系列框架都做到了解耦,分層,誰該干什么就干什么,不許干別的,讓代碼讀起來更有預測性和一致性,方便維護
  • 【解決通信】繼續解耦,flux系列框架采用事件機制解決各層之間通信,采用props傳遞解決各組件之間通信。

事件系統是關鍵

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

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

事件系統的好處:

  • 一個事件可以注冊多個回調函數
  • 各回調函數間沒有耦合。

關于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:假設a要調用b,參考第一條的話,其實就是a要改變b的props,那么a只要改b的props的來源即可,b的props的來源一般就是根組件的state。那么根組件就要有組織和協調的能力。
  • 方案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個,

  • 負責初始化state
  • 負責提供對外props列表
  • 負責組合子view組件形成頁面效果
  • 負責注冊業務邏輯對象提供的業務邏輯方法
  • 負責管理業務邏輯對象

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

  • 根據props進行視圖的渲染

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

  • 提供業務邏輯方法
root組件

root組件由以下元素組成:

1、props-公有屬性
2、state-RN體系的狀態,必須使用Immutable對象
3、私有屬性
4、業務邏輯對象的引用-在componentWillMount中初始化
5、私有方法-以下劃線開頭,內部使用or傳遞給子組件使用
6、公有方法【不推薦】,子組件和外部組件都可以用,但不推薦用公有方法來對外發布功能,破壞了面向狀態編程,盡可能的使用props來發布功能

子view組件

子view組件中包含:

1、props-公有屬性
2、私有屬性-強烈不建議有,除非你能理解以下幾點,建議放在父組件or業務邏輯對象中

  • 絕對不允許和父組件的屬性or狀態有冗余。無論是顯性冗余還是計算結果冗余,除非你能確定結算是性能的瓶頸。
  • 此屬性只有自己會用,父組件和兄弟組件不會使用,如果你不確定這點,請把這個組件放到父組件上,方便組件間通信

3、私有方法-僅作為渲染view的使用,不許有業務邏輯
4、公有方法【不推薦,理由同root組件】

業務邏輯對象

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

1、root組件對象引用-this.root
2、構造器-初始化root對象,初始化私有屬性
3、私有屬性
4、公有方法-對外提供業務邏輯
5、私有方法-以下劃線開頭,內部使用

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

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

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

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

ps2:view子組件復用問題

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

需復用,只暴露props,可以內部自行管理state【盡量避免除非業務需要】
不需復用,只暴露props,內部無state【因為不會單獨使用,不需要setState來觸發渲染】
其實, 一般按照不需復用的情況設計,除非復用很明確,但這時候應該抽出去,變成獨立的組件存在就可以了,所以這個問題是不存在的。

適用場景分析

flux系列框架

flux系列框架的適用場景我覺得應具有以下特點:

一個頁面中組件較多,組件之間較為獨立,但是重疊使用模型,模型的變化會影響很多組件的展現和行為。

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

面向對象的RN組件架構

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

還拿上面聊天界面舉例子,面向對象的RN組件架構其實也可以解耦的寫出寫上述場景,你完全可以將業務邏輯對象之間的交互設計成一個小的事件系統,只是架構沒有直接約束這種解耦,flux系列架構直接在架構中就強制編碼人員做到了解耦,但是如果我不需要解耦的時候就相當于增加了復雜度,得不償失了。

所以面向對象的RN組件架構要更靈活,同時因為靈活對業務邏輯對象設計者的要求也較高,針對較為復雜or重要頁面建議進行詳細設計并leader檢查來保證質量。

如何做監控

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

方案1:在需要監控的地方人為加入監控點。

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

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

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

完整demo代碼

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

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

demo截圖:

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;

參考:http://react-china.org/t/reactnative/3486

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容