本文主要以XXX的html5版本爲藍本,討論結合狀態機開發的思路和實踐方式。狀態機選型使用statechart.js。
起步知識
- 狀態機介紹,請參考Statecharts and Angular.js
- statechart.js的基本使用方法,請參考statechart.js
特別是狀態機介紹,內容非常好,強烈推薦。
適用場景
- 主要用於某個具體業務的複雜頁面流控制
- 簡單的業務流程是不需要的。例如只有一兩個頁面(列表+詳情)
- 適用於多步驟多頁面(包括彈出框)、各種跳轉的場景
如何定義狀態?
根據頁面流、步驟來定義狀態。可以參考以下步驟:
對照保真、流程圖,劃分每個獨立頁面
以個人營銷活動爲例,主要頁面包括活動頁面、選檔次頁面、獎品頁面、獎品包選擇頁面、繳費頁面、發票頁面。
那麼可以考慮定義爲list、level、reward、giftpack、charge、invoice對於有多種彈出窗口的情況,可以考慮定義子狀態
以推薦業務爲例,在菜單頁面上,可能會彈出反饋窗口,或者產品訂購窗口。
那麼可以考慮定義爲menu/index、menu/feedback、menu/prod,這樣的話,通過下面的頁面控制,就可以讓在值狀態的情況下,菜單頁面一直顯示。
<div ng-show="fsm.isCurrent('/menu')" ng-include="'app/partials/recommended/recommended_menu.html'"></div>
<div ng-show="fsm.isCurrent('/menu/prod')" ng-include="'app/partials/recommended/recommended_orderprod.html'"></div>
<div ng-show="fsm.isCurrent('/menu/feedback')" ng-include="'app/partials/recommended/recommended_feedback.html'"></div>
- 對於頁面顯示,有較多共性的頁面,可以考慮定義子狀態,方便共享邏輯和事件處理
以上述的個人營銷活動爲例,獎品頁面、獎品包選擇頁面的頁面很類似,功能操作也比較實現,可以定義成子狀態,如order/reward、order/giftpack
<div ng-show="fsm.isCurrent('/list')" ng-include="'app/partials/personalMarketCamp/personalMarketCamp_list.html'"></div>
<div ng-show="fsm.isCurrent('/level')" ng-include="'app/partials/personalMarketCamp/personalMarketCamp_level.html'"></div>
<div ng-show="fsm.isCurrent('/order/reward')" ng-include="'app/partials/personalMarketCamp/personalMarketCamp_reward.html'"></div>
<div ng-show="fsm.isCurrent('/order/giftpack')" ng-include="'app/partials/personalMarketCamp/personalMarketCamp_giftpack.html'"></div>
<div ng-show="fsm.isCurrent('/invoice')" ng-include="'app/partials/personalMarketCamp/personalMarketCamp_invoice.html'"></div>
<div ng-show="fsm.isCurrent('/charge')" ng-include="'app/partials/personalMarketCamp/personalMarketCamp_charge.html'"></div>
如何控制頁面的顯示、如何響應頁面操作?
- 頁面顯示與否,通過狀態機的狀態,而不是數據的狀態。這裏用的是isCurrent方法
- 頁面操作,通過狀態機的事件發送,而不是直接使用綁定在$scope的方法。這裏用的是send方法
- 頁面跳轉,通過狀態機的狀態變化來驅動。這裏用的是goto方法,是在send方法之後的event邏輯中處理的。
頁面顯示與否,例子上面已經說了。而對於ng-click這種事件觸發,直接用send方法即可。
<div class="Feedback-btn">
<a ng-click="fsm.send('feedback', 'hesitate')" class="accept-btn"><span>考慮</span></a>
<a ng-click="fsm.send('feedback', 'refuse')" class="refuse-btn"><span>拒絕</span></a>
</div>
可以看到,事件也可以捎帶參數的,這樣可以在該狀態的event中進行處理,如下:
# 反饋
@state 'feedback', ->
# 進行反饋操作
@event 'feedback', (operationtype) ->
product = $scope.viewModel.product
if product.opertype == '1'
new Toast(
context: $('body')
message: "該產品不可推薦"
).show();
return
qryfeedbackService.event
"userseq": $scope.viewModel.product.userseq
"servnumber": $scope.telnum
"operationtype": operationtype
.then (ok) =>
@goto '/menu/index'
, (err) ->
new Toast(
context: $('body')
message: err
).show()
需要注意的是,event是掛靠在某個狀態下的,如果你是子狀態的話的,event會先在子狀態中找,如果沒有找到會在父狀態上找。
通過這種方式,就可以實現多個子狀態共享event,例如獎品頁面、獎品包頁面都有選擇功能,就可以把這個操作放到父狀態的event中去。
更多狀態機的細節
很多狀態機都實現了某些特殊狀態,如進入狀態,退出狀態這種事件。statechart也實現了,對應的是enter和exit,代碼大體上是:
@state 'menu', ->
@enter ->
#TODO
@exit ->
#TODO
但是需要注意的是,重複進入這個狀態的話,是會重複執行的。所以對於A -> B -> C這樣的業務流程,從A到B和C回退到B,都會執行這個enter,
就無法區分這種情況了。因爲通常,從A到B是進行初始化,而從C回到B得保留原來B的數據狀態。所以實際上我很少使用這些特殊事件,除非:
- 無需區分的情況,這樣寫會讓代碼風格更統一。
- 沒有接口交互,本地操作的話,因爲這種消耗小很多。
- 存在直接跳轉到該狀態的情況(例如由另外的業務跳轉過來),這種特殊情況下前面的步驟都被忽略。而且這種情況下,需要通常需要接口交互(例如補充某些必要信息),而爲了區分回退的情況,我通常會根據業務特性考慮一些數據緩存處理。