本篇較長,前面是目前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;