本文從屬於筆者的Web 前端入門與最佳實踐中的前端工程化之狀態管理實踐系列文章。下面列舉了幾篇關於Redux與MobX的文章或者討論,有興趣的能夠閱讀下。本文並非但願安利或者做MobX的佈道者,也不是單純地爲了吐槽Functional Programming至上主義,只是筆者提出本身再前端實踐中的一些困惑與思考以供批判討論。javascript
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仍是要好一點的。我承認函數式編程的思想成爲項目構建組織的不可分割的一部分,可是是否應該在任何項目的任何階段都先談編程思想,然後看業務需求?這無疑有點政治正確般的耍流氓了,筆者以爲文本惟一聲明秉持的思想就是在不一樣級別/需求的項目發展的不一樣階段咱們應該使用不一樣的技術棧與技術搭配,而本文要討論的核心便是如何尋求一種儘量平穩與提供碎片化代碼的可以貫徹項目整個生命週期的代碼組織或者框架選用方案。webpack
Redux的DevTools與Time Travel是如此的優雅與酷炫,不再用擔憂應用莫名其妙崩潰而找不到緣由,還能方便地同構直出了呢。不過就像筆者在2016年裏作前端是怎樣一種體驗一文中所描述的,我只是想簡單的從服務端獲取個列表數據而後展現出來,你卻告訴我要去學習TypeScript+Webpack+SystemJS+Babel+React+Redux等等,怎麼看都仍是jQuery的時代簡單明瞭啊。以最基礎的添加用戶功能來講,在jQuery的時代咱們只須要調用Ajax而後等待結果便可,這樣固然是有問題的,項目的代碼混亂度會隨着需求的增長而幾何倍數增加。而Redux則以較爲嚴格的規範幫咱們實現了Single Responsibility,這樣咱們至少須要寫Reducer、ActionCreator、Store、Selector等等幾個文件,固然,在有些最佳實踐裏會把這幾個部分概括到一個文件中,不過自從筆者發現某個上千行的文件我連看都不想看的時候,以爲仍是拆開來好:git
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的地方執行如下操做:github
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的話,你必需要遵循以下規範:web
必須使用基本對象與數組來描述應用狀態編程
必須使用基本的對象來描述系統變化
必須使用純函數來處理系統中的業務邏輯
而Dan推薦的適用Redux的狀況典型的有:
筆者以爲,Redux適合於須要強項目健壯度與多人協調規範的大中型團隊,對於不少中小型創業性質,項目需求迭代異常快的團隊則每每可能起到拔苗助長的做用。若是你真的喜歡Redux,那麼更應該在合適的項目,合適的階段去接入Redux,而不是在需求還沒有成型之處就花費大量精力搭建複雜的腳手架,說不許客戶的需求圖紙都畫反了呢。
MobX中核心的概念便是Observable,相信接觸過響應式編程的確定很是熟悉,從後端的典型表明RxJava到Android/iOS開發中的各類響應式框架都各領風騷。這裏咱們以構建簡單的TODOList爲例,代碼參考了筆者的mobx-react-webpack-boilerplate這個庫。首先,以典型的OOP的思想來考慮,咱們須要構建ToDo的實體類:
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差異仍是較大的:
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來響應狀態的變換:
@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:純函數方式編寫的核心業務邏輯,這一點主要是爲了保證核心業務邏輯的可測試性與將來的遷移性。
筆者在React設計模式:深刻理解React&Redux原理套路一文中探討了咱們在React/Redux開發中經常使用的一些模式,而筆者較爲推崇的將狀態管理工具引入組件中的方式即HOC模式。不管Redux仍是MobX都是採用了這種模式,咱們所熟知的在Redux中的應用方式爲:
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中的應用方式爲:
import { observer, inject } from 'mobx-react'; ... const UserProfileContainer = inject( 'userStore' )(observer(({ id, userStore, }) => { return ( <UserProfile user={userStore.getUser(id)} onUpdateUser={userStore.updateUser} /> ); }));
Redux 有一個很不錯的特性就是Undo/Redo,這樣會幫助咱們在調試時重現以前的狀態。那麼該特性主要是基於Immutable Data實現的,咱們一樣的也能夠在MobX的Store中,經過強行定義全部的數據操做爲Immutable操做,也能實現相似的功能:
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)適當的容錯與業務處理。
這個階段咱們可能直接將數據獲取的函數放置到componentDidMount中,而且將UI State與Domain State都利用setState
函數存放在LocalState中。這種方式的開發效率最高,畢竟代碼量最少,不過其可擴展性略差,而且不利於視圖之間共享狀態。
// component <button onClick={() => store.users.push(user)} />
這裏的store僅僅指純粹的數據存儲或者模型類。
隨着項目逐漸複雜化,咱們須要尋找專門的狀態管理工具來進行外部狀態的管理了:
// component <button onClick={() => store.addUser(user)} /> // store @action addUser = (user) => { this.users.push(user); }
這個時候你也能夠直接在組件內部修改狀態,即仍是使用第一個階段的代碼風格,直接操做store對象,不過也能夠經過引入Strict模式來避免這種不良好的實踐:
// root file import { useStrict } from 'mobx'; useStrict(true);
隨着項目體量進一步的增長與參與者的增長,這時候使用聲明式的Actions就是最佳實踐了,也應該是Redux閃亮登場的時候了。這時候Redux原本最大的限制,只能經過Action而不能直接地改變應用狀態也就凸顯出了其意義所在(Use Explicit Actions To Change The State)。
// reducer (state, action) => newState
前端之路,從無止境,本文只是筆者旅行期間的一篇隨筆,歡迎指導討論。