本文從屬於筆者的Web前端入門與最佳實踐中的前端工程化之狀態管理實踐系列文章。下面列舉了幾篇關於Redux與MobX的文章或者討論,有興趣的可以閱讀下。本文並不是希望安利或者作MobX的佈道者,也不是單純地為了吐槽Functional Programming至上主義,只是筆者提出自己再前端實踐中的一些困惑與思考以供批判討論。
Redux是完全的函數語言程式設計思想踐行者(如果你對於Redux還不夠理解,可以參考下筆者的深入理解Redux:10個來自專家的Redux實踐建議),其核心技術圍繞遵循Pure Function的Reducer與遵循Immutable Object的Single State Tree,提供了Extreme Predictability與Extreme Testability,相對應的需要大量的Boilerplate。而MobX則是Less Opinioned,其脫胎於Reactive Programming,其核心思想為Anything that can be derived from the application state, should be derived. Automatically,即避免任何的重複狀態。Redux使用了Uniform Single State Tree,而在後端開發中習慣了Object Oriented Programming的筆者不由自主的也想在前端引入Entity,或者說在設計思想上,譬如對於TodoList的增刪改查,筆者希望能夠包含在某個TodoList物件中,而不需要將所有的操作拆分為Creator、Reducer與Selector三個部分,我只是想簡單的展示個列表而已。筆者上大學學的第一節課就是講OOP,包括後面在C#、Java、Python、PHP等等很多後端領域的實踐中,都深受OOP思想的薰陶與灌輸。不可否認,可變的狀態是軟體工程中的萬惡之源,但是,OOP對於業務邏輯的描述與程式碼組織的可讀性、可理解性的保證相較於宣告式的,略為抽象的FP還是要好一點的。我認可函數語言程式設計的思想成為專案構建組織的不可分割的一部分,但是是否應該在任何專案的任何階段都先談程式設計思想,而後看業務需求?這無疑有點政治正確般的耍流氓了,筆者覺得文字唯一宣告秉持的思想就是在不同級別/需求的專案發展的不同階段我們應該使用不同的技術棧與技術搭配,而本文要討論的核心即是如何尋求一種儘可能平穩與提供碎片化程式碼的能夠貫徹專案整個生命週期的程式碼組織或者框架選用方案。
Redux Really Do The Right Thing!Redux Always Do The Best Thing?
Redux的DevTools與Time Travel是如此的優雅與酷炫,再也不用擔心應用莫名其妙崩潰而找不到原因,還能方便地同構直出了呢。不過就像筆者在2016年裡做前端是怎樣一種體驗一文中所描述的,我只是想簡單的從服務端獲取個列表資料然後展示出來,你卻告訴我要去學習TypeScript+Webpack+SystemJS+Babel+React+Redux等等,怎麼看都還是jQuery的時代簡單明瞭啊。以最基礎的新增使用者功能來說,在jQuery的時代我們只需要呼叫Ajax然後等待結果即可,這樣當然是有問題的,專案的程式碼混亂度會隨著需求的增加而幾何倍數增長。而Redux則以較為嚴格的規範幫我們實現了Single Responsibility,這樣我們至少需要寫Reducer、ActionCreator、Store、Selector等等幾個檔案,當然,在有些最佳實踐裡會把這幾個部分歸納到一個檔案中,不過自從筆者發現某個上千行的檔案我連看都不想看的時候,覺得還是拆開來好:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
const initialState = { users: [ { name: 'Dan' }, { name: 'Michel' } ] }; // reducer function users(state = initialState, action) { switch (action.type) { case 'USER_ADD': return { ...state, users: [ ...state.users, action.user ] }; default: return state; } } // action { type: 'USER_ADD', user: user }; |
然後你需要在需要呼叫該Action的地方執行以下操作:
1 |
dispatch({ type: 'USER_ADD', user: user }); |
筆者最近的兩個專案中都使用了Redux作為核心的狀態管理工具中,之前是覺得只要有合適的腳手架Webpack-React-Redux-Boilerplate就可以了,不過很多時候看著從Action Creator到Reducer,再到Selector這一系列的僅僅為了實現某個小功能而需要寫的N多的Boilerplate,就覺得這並不是我所想要的。就好像現在View層人們經常會討論對比Vue與React,雖然Redux目前在狀態管理領域一騎絕塵,但是也有越來越多的人關注MobX。筆者之前翻譯過一篇文章,你並不需要Redux,這是一篇來自Redux作者的對於Redux濫用情況的吐槽。從上面的例子中也可以看出,如果你使用Redux的話,你必須要遵循如下規範:
- 必須使用基本物件與陣列來描述應用狀態
- 必須使用基本的物件來描述系統變化
- 必須使用純函式來處理系統中的業務邏輯
而Dan推薦的適用Redux的情況典型的有:
- 方便地能夠將應用狀態儲存到本地並且重啟動時能夠讀取恢復狀態
- 方便地能夠在服務端完成初始狀態設定,並且完成狀態的服務端渲染
- 能夠序列化記錄使用者操作,能夠設定狀態快照,從而方便進行Bug報告與開發者的錯誤重現
- 能夠將使用者的操作或者事件傳遞給其他環境而不需要修改現有程式碼
- 能夠新增重放或者撤銷功能而不需要重構程式碼
- 能夠在開發過程中實現狀態歷史的回溯,或者根據Action的歷史重現狀態
- 能夠為開發者提供全面透徹的審視和修改現有開發工具的介面,從而保證產品的開發者能夠根據他們自己的應用需求打造專門的工具
- 能夠在複用現在大部分業務邏輯的基礎上構造不同的介面
筆者覺得,Redux適合於需要強專案健壯度與多人協調規範的大中型團隊,對於很多中小型創業性質,專案需求迭代異常快的團隊則往往可能起到適得其反的作用。如果你真的喜歡Redux,那麼更應該在合適的專案,合適的階段去接入Redux,而不是在需求尚未成型之處就花費大量精力搭建複雜的腳手架,說不準客戶的需求圖紙都畫反了呢。
瞭解下MobX
MobX中核心的概念即是Observable,相信接觸過響應式程式設計的肯定非常熟悉,從後端的典型代表RxJava到Android/iOS開發中的各種響應式框架都各領風騷。這裡我們以構建簡單的TODOList為例,程式碼參考了筆者的mobx-react-webpack-boilerplate這個庫。首先,以典型的OOP的思想來考慮,我們需要構建ToDo的實體類:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import {observable} from 'mobx'; export default class TodoModel { store; id; @observable title; @observable completed; constructor(store, id, title, completed) { this.store = store; this.id = id; this.title = title; this.completed = completed; } ... //還有一些功能函式 } |
這裡@observable註解標註某個變數為被觀測值,一旦某個被觀測的變數發生了變化,即可以觸發觀測值相對應的響應。在寫好了模型類之後,我們需要編寫Store,這裡的Store同時包含了資料儲存與對資料的操作,和Redux中的Single State Tree差別還是較大的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
import {observable, computed, reaction} from 'mobx'; import TodoModel from '../models/TodoModel' import * as Utils from '../utils'; export default class TodoStore { @observable todos = []; @computed get activeTodoCount() { return this.todos.reduce( (sum, todo) => sum + (todo.completed ? 0 : 1), 0 ) } @computed get completedCount() { return this.todos.length - this.activeTodoCount; } subscribeServerToStore() { reaction( () => this.toJS(), todos => fetch('/api/todos', { method: 'post', body: JSON.stringify({ todos }), headers: new Headers({ 'Content-Type': 'application/json' }) }) ); } subscribeLocalstorageToStore() { reaction( () => this.toJS(), todos => localStorage.setItem('mobx-react-todomvc-todos', todos) ); } addTodo (title) { this.todos.push(new TodoModel(this, Utils.uuid(), title, false)); } toggleAll (checked) { this.todos.forEach( todo => todo.completed = checked ); } clearCompleted () { this.todos = this.todos.filter( todo => !todo.completed ); } ... } |
這裡有使用@computed註解,這裡的@computed註解即是表示該變數是可以從被觀測值中推導而出,而不需要你手動觸發判斷的。最後在我們的View層,同樣可以將其設定為Observer來響應狀態的變換:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
@observer export default class TodoApp extends React.Component { render() { const {todoStore, viewStore} = this.props; return ( <div> <DevTool /> <header className="header"> <h1>todos</h1> <TodoEntry todoStore={todoStore} /> </header> <TodoOverview todoStore={todoStore} viewStore={viewStore} /> <TodoFooter todoStore={todoStore} viewStore={viewStore} /> </div> ); } componentDidMount() { var viewStore = this.props.viewStore; var router = Router({ '/': function() { viewStore.todoFilter = ALL_TODOS; }, '/active': function() { viewStore.todoFilter = ACTIVE_TODOS; }, '/completed': function() { viewStore.todoFilter = COMPLETED_TODOS; } }); router.init('/'); } } TodoApp.propTypes = { viewStore: React.PropTypes.object.isRequired, todoStore: React.PropTypes.object.isRequired }; |
我們需要狀態管理嗎?
筆者在我的前端之路這篇綜述中提過,前端一直在從隨意化到工程化的變革,而筆者認為的工程化的幾個特徵,即檢視元件化、功能模組化與狀態管理。筆者覺得,在構建前端專案,乃至於編寫簡單的HTML頁面時,能夠考慮狀態管理這個概念,並且為以後引入專門的狀態管理預留一定的介面空間是件很有意義的事情。而狀態管理也並不意味著你就需要Redux或者MobX這樣專門的框架,就像你不一定需要Redux中所說的,Local State is Fine,有何不可呢?技術應該服務於業務,服務於產品,那狀態管理給予了我們什麼樣的便捷?建議先閱讀下筆者的Web開發中所謂狀態淺析:Domain State&UI State,對於某個前端應用,其狀態大體可以分為UI State與Domain State兩大類:
而當我們考量某個狀態管理框架時,我們往往希望其能夠提供以下的特徵或者介面:
- 不同的元件之間能夠共享狀態。這一點應該算是將元件內狀態提取到外部的重要原因之一,早期的React中如果你不同元件之間需要共享狀態,只能一層一層地Props傳遞或者通過公共父節點來傳遞。雖然現在React引入了Context,不過筆者認為其還是更適合於用作一些全域性配置的傳遞。
- 狀態能夠在任意地方被訪問,這是為了方便我們在純粹的業務邏輯函式中也能夠操作狀態。
- 元件能夠修改狀態。
- 元件能夠修改其他元件的狀態。
Redux與MobX都能滿足上述幾個需求,能夠允許你將狀態儲存於檢視之外,並且允許更改與通知檢視重繪,這裡我們不糾結於具體的GUI應用程式架構模式,有興趣深入瞭解的可以參考筆者的GUI應用程式架構的十年變遷:MVC,MVP,MVVM,Unidirectional,Clean。總而言之,當我們的介面希望獲取或者更改某個資料狀態時,其有明確的介面供其使用。對於理想的狀態管理工具,筆者認可其應該具備以下特徵:
(1)Predictable View Rendering:可預測的檢視渲染,Redux提出的概念是Deterministic View Render,即檢視狀態完全脫離於檢視存在,最終呈現的檢視永遠由輸入的狀態所決定。筆者是堅定的介面元件化的支持者,我們應該將純介面展示與資料剝離開來,這樣可以保證我們程式碼的職責分割與可測試性,並且能夠在下文所述的專案衍化過程中儘可能保證程式碼的可用性與複用性。
(2)Pure Business Logic:純函式方式編寫的核心業務邏輯,這一點主要是為了保證核心業務邏輯的可測試性與未來的遷移性。
High Order Component
筆者在React設計模式:深入理解React&Redux原理套路一文中探討了我們在React/Redux開發中常用的一些模式,而筆者較為推崇的將狀態管理工具引入元件中的方式即HOC模式。無論Redux還是MobX都是採用了這種模式,我們所熟知的在Redux中的應用方式為:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; ... function mapStateToProps(state, props) { const { id } = props; const user = state.users[id]; return { user, }; } function mapDispatchToProps(dispatch) { return { onUpdateUser: bindActionCreators(actions.updateUser, dispatch), }; } const UserProfileContainer = connect(mapStateToProps, mapDispatchToProps)(UserProfile); |
而類似的在MobX中的應用方式為:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { observer, inject } from 'mobx-react'; ... const UserProfileContainer = inject( 'userStore' )(observer(({ id, userStore, }) => { return ( <UserProfile user={userStore.getUser(id)} onUpdateUser={userStore.updateUser} /> ); })); |
Immutable State Tree in Single Store
Redux 有一個很不錯的特性就是Undo/Redo,這樣會幫助我們在除錯時重現之前的狀態。那麼該特性主要是基於Immutable Data實現的,我們同樣的也可以在MobX的Store中,通過強行定義所有的資料操作為Immutable操作,也能實現類似的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
export default class SlidesStore { // Observable history array @observable history = Immutable.from([{ currentSlideIndex: 0, slides: [{ // Default first slide }] }]) // Start state at the first entry in history @observable historyIndex = 0; } addToHistory(snapshot) { this.history = this.history.concat([Immutable.from(snapshot)]); this.historyIndex += 1; } |
專案增長過程中的狀態管理方案衍化
王國維先生說過人生有三個境界,我覺得根據專案的需求不同或者,。技術應該是為業務需求所服務,僅僅為了使用新的技術而罔顧實際的業務需求就是耍流氓。筆者在思索自己應該使用的狀態管理框架時,有一個重要的考慮點就是專案儘可能地小代價的演進與迭代。譬如在立項之初,需求並不明確,功能邏輯尚不復雜的時候,我們可以直接從View層構造,儘可能地先實現Stateless與Fractal的檢視元件。筆者認為是需要有獨立的API/Model層存在的,其意義在於:
(1)可重用的測試程式碼。
(2)多個Endpoint的組合。
(3)適當的容錯與業務處理。
原型:Local State
這個階段我們可能直接將資料獲取的函式放置到componentDidMount中,並且將UI State與Domain State都利用setState
函式存放在LocalState中。這種方式的開發效率最高,畢竟程式碼量最少,不過其可擴充套件性略差,並且不利於檢視之間共享狀態。
1 2 |
// component <button onClick={() => store.users.push(user)} /> |
這裡的store僅僅指純粹的資料儲存或者模型類。
專案增長:External State
隨著專案逐漸複雜化,我們需要尋找專門的狀態管理工具來進行外部狀態的管理了:
1 2 3 4 5 6 7 |
// component <button onClick={() => store.addUser(user)} /> // store <a href="http://www.jobbole.com/members/Francesco246437">@action</a> addUser = (user) => { this.users.push(user); } |
這個時候你也可以直接在元件內部修改狀態,即還是使用第一個階段的程式碼風格,直接操作store物件,不過也可以通過引入Strict模式來避免這種不良好的實踐:
1 2 3 4 |
// root file import { useStrict } from 'mobx'; useStrict(true); |
多人協作/嚴格規範/複雜互動:Redux
隨著專案體量進一步的增加與參與者的增加,這時候使用宣告式的Actions就是最佳實踐了,也應該是Redux閃亮登場的時候了。這時候Redux本來最大的限制,只能通過Action而不能直接地改變應用狀態也就凸顯出了其意義所在(Use Explicit Actions To Change The State)。
1 2 |
// reducer (state, action) => newState |
前端之路,從無止境,本文只是筆者旅行期間的一篇隨筆,歡迎指導討論。