ReactNative的架構設計
原文 http://segmentfault.com/a/1190000004161358
ReactNative的架構設計
請注意,本篇寫的是react native的架構設計,如果你用react來開發web程序,本篇文章只能僅供參考,問題都沒有在web上去考慮過。
本篇較長,前面是目前flux開源框架的一些分析,后面是架構設計過程。您可以直奔主題。
用RN最大的難題是設計思想的轉變,以前的設計方法論已經不太適用了。而RN僅僅提供了view的框架,構建完整app的架構并沒有直接提供。
考慮目前遇到的如下問題,希望架構給出解決方案。
-
交互:如何解決組件間通信【父子、子父、兄弟等,特別是跨層or反向數據流動等】;用state還是接口操作組件;
</li> -
職責:組件狀態放哪,業務邏輯放哪,數據放哪,因為太靈活了,怎么做都可以實現功能,但是怎么做才是最好的,才是最正確的呢?
</li> </ol>`
todo一個問題:由于react是面向狀態編程,相當于react的組件只關注數據的最終狀態,數據是怎么產生的并不關心,但是某些場景下,數據如何產生的是會影響到組件的一些行為的【比如一個新增行要求有動畫效果,查詢出的行就不需要等】,這在RN中很難描述。。。。。
`
</div>RN架構就是為解決上述問題提供的指導和方法論,是通盤考慮整個開發、測試、運維的狀況,做出的考慮最全面的抉擇,或者為抉擇提供依據。
目前為react服務的架構也有一些了,如Flux,Reflux,Redux,Relay,Marty。
Flux
flux是官方提供的架構,目的是分層解耦,職責劃分清晰,誰負責干啥很明確。具體描述可以參考官方文檔,這里不詳述。
-
action 封裝請求
</li> -
dispatcher 注冊處理器、分發請求
</li> -
store 是處理器,處理業務邏輯,保存數據
</li> -
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 ║ ╚═════════╝ ╚════════╝ ╚═════════════════╝ ^ │ └──────────────────────────────────────┘
-
更容易的監聽。listenables和約定以on開頭的方法。等。
-
去掉了dispatcher。
-
action可以進行aop編程。
-
去掉了waitfor。store可以監聽store。
-
component提供了一系列mixin,方便注冊\卸載到store的監聽和與store交互等。
Redux
社區內比較受推崇,因為用起來相對比較簡單
?
特性:
-
分層設計,職責清晰。
-
要求store reducer都是頁面單例,易于管理。
-
action為請求dto對象,是請求類型,請求數據的載體。
-
reducer是處理請求的方法。不允許有狀態,必須是純方法。必須嚴格遵守輸入輸出,中間不允許有異步調用。不允許對state直接進行修改,要想修改必須返回新對象。
-
store
-
維持應用的state;
-
提供 getState() 方法獲取 state;
-
提供 dispatch(action) 方法分發請求來更新 state;門面模式,要求所有的請求滿足統一的格式【可以進行路由、監控、日志等】,統一的調用方式。
-
通過 subscribe(listener) 注冊監聽器監聽state的變化。
-
-
官方文檔寫的較為詳細,從設計到開發都有,比flux要好
痛處如下,看能否接受或者解決:
-
redux的原則1:state不能被修改。
-
其實這個用react的state也會有同樣的問題,最好把state設計的沒有冗余,盡量少出這種情況
-
解決方案:參考官方:因為我們不能直接修改卻要更新數組中指定的一項數據,這里需要先把前面和后面都切開。如果經常需要這類的操作,可以選擇使用幫助類 React.addons.update,updeep,或者使用原生支持深度更新的庫 Immutable。最后,時刻謹記永遠不要在克隆 state 前修改它。
-
-
單一的龐大的reducer的拆分
-
這塊設計也不好做,會讓人疑惑
-
官方給的demo中直接按state的內容區分,我覺得這樣做不好,如果后期有跨內容的情況,就比較奇怪了。官方給的combineReducers方案,也只是減少代碼量,本質沒有變化,state還是拆分處理,路由還是業務邏輯自己來做。
-
解決方案:還是處理一整個state,可以按照約定寫reducer類而不是方法,類里按照actionType建方法,架構自動路由并調用。
-
以前做java架構,路由一定是架構來調用的,目前感覺各大flux框架都是解決問題不徹底。
-
-
官方建議設計模式:頂層容器組件才對redux有依賴,組件間通過props來傳遞數據。按照這樣設計還是沒有解決組件間交互和數據傳遞的問題。官方react設計建議:react的設計建議: http://camsong.github.io/redux-in-chinese/docs/basics/UsageWithReact.htm
-
使用connect將state綁定到component。此處有些黑盒了。
-
異步action用來請求服務端數據,利用middleware增強createStore的dispatch后即支持。
Relay
沒有時間,沒做研究
Marty
沒有時間,沒做研究
結論
開源架構封裝的簡單的flux會產生較多的冗余代碼。
開源架構封裝的復雜的redux,其和RN綁定封裝了一些東西,是一個黑盒,不易理解和維護。
介于上述兩者之間的開源架構reflux,文檔較上述2個少,不知道其可持續性如何。如果一定要用開源架構的話,我覺得他稍加封裝是一個較為推薦的選擇。
不是特復雜的程序【一般spa的程序會更復雜一些,而RN并不是spa】,這些概念只會增加你的開發難度,并且對后面維護的人要求更高。
我們繼續頭腦風暴,繼續抽象總結一下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
先扣題,針對開篇問題的解決方案如下
交互
-
組件對外發布:組件對外只允許使用props來暴露功能,不允許使用接口及其它一切方式
-
父子組件間:組件的子組件通過父組件傳遞的接口來與父組件通信
-
兄弟組件間:
-
方案1:假設a要調用b,參考第一條的話,其實就是a要改變b的props,那么a只要改b的props的來源即可,b的props的來源一般就是根組件的state。那么根組件就要有組織和協調的能力。
-
方案2:利用事件機制,基本同flux架構。略復雜,且我們并不需要事件的特性,本架構設計不推薦。
-
職責
-
root-存放state,組織子view組件,組織業務邏輯對象等
-
子view組件-根據this.props渲染view。
-
業務邏輯對象-提供業務邏輯方法
根據以上推導,我將其命名為面向對象的ReactNative架構設計,它與flux系列架構的最大的不同之處在于,用業務邏輯對象來代替了【store action dispatcher】or【store reducer】的事件系統。業務邏輯對象就是一組對象,用面向對象的設計理念設計出的n個對象,其負責處理整個頁面的業務邏輯。
以上為推導過程,干貨才開始。。。。
面向對象的ReactNative組件\頁面架構設計
-
一個獨立完整的組件\頁面一般由以下元素構成
-
root組件,1個,
1. 負責初始化state 2. 負責提供對外props列表 2. 負責組合子view組件形成頁面效果 3. 負責注冊業務邏輯對象提供的業務邏輯方法 4. 負責管理業務邏輯對象
-
view子組件,0-n個,
1. 根據props進行視圖的渲染
-
業務邏輯對象,0-n個,
2. 提供業務邏輯方法
-
-
root組件,中包含:
-
props-公有屬性
-
state-RN體系的狀態,必須使用Immutable對象
-
私有屬性
-
業務邏輯對象的引用-在componentWillMount中初始化
-
私有方法-以下劃線開頭,內部使用or傳遞給子組件使用
-
公有方法【不推薦】,子組件和外部組件都可以用,但不推薦用公有方法來對外發布功能,破壞了面向狀態編程,盡可能的使用props來發布功能
-
-
子view組件,中包含:
-
props-公有屬性
-
私有屬性-如果你不能理解下面的要求,建議沒有,統一放在父組件上
-
絕對不允許和父組件的屬性or狀態有冗余。無論是顯性冗余還是計算結果冗余,除非你能確定結算是性能的瓶頸。
-
此屬性只有自己會用,父組件和兄弟組件不會使用,如果你不確定這點,請把這個組件放到父組件上,方便組件間通信
-
私有方法-僅作為渲染view的使用,不許有業務邏輯
-
公有方法【不推薦,理由同root組件】
-
-
業務邏輯對象,中包含:
-
root組件對象引用-this.root
-
構造器-初始化root對象,初始化私有屬性
-
私有屬性
-
公有方法-對外提供業務邏輯
-
私有方法-以下劃線開頭,內部使用
todo補充架構圖
-
通用型組件只要求盡量滿足上述架構設計
通用型組件一般為不包含任何業務的純技術組件,具有高復用價值、高定制性、通常不能直接使用需要代碼定制等特點。
可以說是一個系統的各個基礎零件,比如一個蒙板效果,或者一個模態彈出框。
架構的最終目的是保證系統整體結構良好,代碼質量良好,易于維護。一般編寫通用型組件的人也是經驗較為豐富的工程師,代碼質量會有保證。而且,作為零件的通用組件的使用場景和生命周期都和普通組件\頁面不同,所以,僅要求通用組件編寫盡量滿足架構設計即可。
view子組件復用問題
拋出一個問題,設計的過程中,子組件是否需要復用?子組件是否需要復用會影響到組件設計。
-
需復用,只暴露props,可以內部自行管理state【盡量避免除非業務需要】
-
不需復用,只暴露props,內部無state【因為不會單獨使用,不需要setState來觸發渲染】
其實, 一般按照不需復用的情況設計,除非復用很明確,但這時候應該抽出去,變成獨立的組件存在就可以了,所以這個問題是不存在的。
面向對象的ReactNative架構設計優缺點--todo
按場景分析、驗證架構設計--todo
觸發view改變的場景【即需要setState的場景】
-
回調函數觸發【異步】
-
組件生命周期事件中or用戶操作觸發 啟動請求服務器
-
組件生命周期事件中or用戶操作觸發,啟動定時任務or注冊了其它回調函數【比如交互管理器的動畫結束事件】
-
-
用戶操作觸發
-
view的直接改變【同步】
-
僅僅注冊回調函數【異步,參考上面一條】
-
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 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享! -