ReactNative的元件架構設計
本文轉載自:
https://segmentfault.com/a/1190000004161358
請注意,本篇寫的是react native的架構設計,如果你用react來開發web程式,本篇文章只能僅供參考,問題都沒有在web上去考慮過。
本篇較長,前面是目前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
社群內比較受推崇,因為用起來相對比較簡單
特性:
-
分層設計,職責清晰。
-
要求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框架都是解決問題不徹底。
-
-
使用connect將state繫結到component。此處有些黑盒了。
-
非同步action用來請求服務端資料,利用middleware增強createStore的dispatch後即支援。
Relay
沒有時間,沒做研究
Marty
沒有時間,沒做研究
結論
開源架構封裝的簡單的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
先扣題,針對開篇問題的解決方案如下
互動
-
元件對外發布:元件對外只允許使用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個,
-
負責初始化state
-
負責提供對外props列表
-
負責組合子view元件形成頁面效果
-
負責註冊業務邏輯物件提供的業務邏輯方法
-
負責管理業務邏輯物件
-
-
view子元件,0-n個,
-
根據props進行檢視的渲染
-
-
業務邏輯物件,0-n個,
-
提供業務邏輯方法
-
root元件
root元件由以下元素組成:
-
props-公有屬性
-
state-RN體系的狀態,必須使用Immutable物件
-
私有屬性
-
業務邏輯物件的引用-在componentWillMount中初始化
-
私有方法-以下劃線開頭,內部使用or傳遞給子元件使用
-
公有方法【不推薦】,子元件和外部元件都可以用,但不推薦用公有方法來對外發布功能,破壞了面向狀態程式設計,儘可能的使用props來發布功能
注意:定義root元件的state的時候,如果使用es6的方式,要把state的初始化放到componentWillMount中,如果在構造器中this.props為空。
子view元件
子view元件中包含:
-
props-公有屬性
-
私有屬性-如果你不能理解下面的要求,建議沒有,統一放在父元件上
-
絕對不允許和父元件的屬性or狀態有冗餘。無論是顯性冗餘還是計算結果冗餘,除非你能確定結算是效能的瓶頸。
-
此屬性只有自己會用,父元件和兄弟元件不會使用,如果你不確定這點,請把這個元件放到父元件上,方便元件間通訊
-
-
私有方法-僅作為渲染view的使用,不許有業務邏輯
-
公有方法【不推薦,理由同root元件】
業務邏輯物件
業務邏輯物件由以下元素組成:
-
root元件物件引用-this.root
-
構造器-初始化root物件,初始化私有屬性
-
私有屬性
-
公有方法-對外提供業務邏輯
-
私有方法-以下劃線開頭,內部使用
ps1:通用型元件只要求儘量滿足上述架構設計
通用型元件一般為不包含任何業務的純技術元件,具有高複用價值、高定製性、通常不能直接使用需要程式碼定製等特點。
可以說是一個系統的各個基礎零件,比如一個蒙板效果,或者一個模態彈出框。
架構的最終目的是保證系統整體結構良好,程式碼質量良好,易於維護。一般編寫通用型元件的人也是經驗較為豐富的工程師,程式碼質量會有保證。而且,作為零件的通用元件的使用場景和生命週期都和普通元件\頁面不同,所以,僅要求通用元件編寫儘量滿足架構設計即可。
ps2:view子元件複用問題
丟擲一個問題,設計的過程中,子元件是否需要複用?子元件是否需要複用會影響到元件設計。
-
需複用,只暴露props,可以內部自行管理state【儘量避免除非業務需要】
-
不需複用,只暴露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編寫。
demo截圖:
todolist頁面:
<button href="javascript:void(0);" _xhe_href="javascript:void(0);" class="copyCode btn btn-xs" data-clipboard-text="" "="" data-toggle="tooltip" data-placement="top" title="" style="color: rgb(255, 255, 255); font-style: inherit; font-variant: inherit; font-stretch: inherit; font-size: 12px; line-height: 1.5; font-family: inherit; margin: 0px 0px 0px 5px; overflow: visible; cursor: pointer; vertical-align: middle; border: 1px solid transparent; white-space: nowrap; padding-right: 5px; padding-left: 5px; border-radius: 3px; -webkit-user-select: none; box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 2px; background-image: none; background-color: rgba(0, 0, 0, 0.74902);">複製
'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;
}