本文从属于笔者的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
前端之路,从无止境,本文只是笔者旅行期间的一篇随笔,欢迎指导讨论。