渐进的状态管理

状态管理概述与技术选型

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 还是要好一点的。我认可函数式编程的思想成为项目构建组织的不可分割的一部分,但是是否应该在任何项目的任何阶段都先谈编程思想,而后看业务需求?这无疑有点政治正确般的耍流氓了,笔者觉得文本唯一声明秉持的思想就是在不同级别/需求的项目发展的不同阶段我们应该使用不同的技术栈与技术搭配,而本文要讨论的核心即是如何寻求一种尽可能平稳与提供碎片化代码的能够贯彻项目整个生命周期的代码组织或者框架选用方案。

无论是 MobX 还是 Redux,它们都非局限于某个界面层框架所使用的状态管理工具。

Redux 与 MobX 相比非常抽象,并且学习曲线也更为复杂;特别是对于大型应用而言,我们需要考虑

综上所述,笔者认为 MobX 更适合于实时系统、控制台、文本编辑器、展示型软件等非时间驱动型的应用,而 Redux 适用于商业应用或者交互复杂的游戏等等场景。

渐进的状态管理

复杂单页应用的状态设计

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 等等几个文件,当然,在有些最佳实践里会把这几个部分归纳到一个文件中,不过自从笔者发现某个上千行的文件我连看都不想看的时候,觉得还是拆开来好:

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 的地方执行以下操作:

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 的情况典型的有:

笔者觉得,Redux 适合于需要强项目健壮度与多人协调规范的大中型团队,对于很多中小型创业性质,项目需求迭代异常快的团队则往往可能起到适得其反的作用。如果你真的喜欢 Redux,那么更应该在合适的项目,合适的阶段去接入 Redux,而不是在需求尚未成型之处就花费大量精力搭建复杂的脚手架,说不准客户的需求图纸都画反了呢。

了解下 MobX

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:纯函数方式编写的核心业务逻辑,这一点主要是为了保证核心业务逻辑的可测试性与未来的迁移性。

High Order Component

笔者在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}
      />
    );
  })
);

Immutable State Tree in Single Store

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)适当的容错与业务处理。

原型 | Local State

这个阶段我们可能直接将数据获取的函数放置到 componentDidMount 中,并且将 UI State 与 Domain State 都利用setState函数存放在 LocalState 中。这种方式的开发效率最高,毕竟代码量最少,不过其可扩展性略差,并且不利于视图之间共享状态。

// component
<button onClick={() => store.users.push(user)} />

这里的 store 仅仅指纯粹的数据存储或者模型类。

项目增长 | External State

随着项目逐渐复杂化,我们需要寻找专门的状态管理工具来进行外部状态的管理了:

// component
<button onClick={() => store.addUser(user)} />

// store
@action addUser = (user) => {
  this.users.push(user);
}

这个时候你也可以直接在组件内部修改状态,即还是使用第一个阶段的代码风格,直接操作 store 对象,不过也可以通过引入 Strict 模式来避免这种不良好的实践:

// root file
import { useStrict } from "mobx";

useStrict(true);

多人协作/严格规范/复杂交互:Redux

随着项目体量进一步的增加与参与者的增加,这时候使用声明式的 Actions 就是最佳实践了,也应该是 Redux 闪亮登场的时候了。这时候 Redux 本来最大的限制,只能通过 Action 而不能直接地改变应用状态也就凸显出了其意义所在(Use Explicit Actions To Change The State)。

// reducer
(state, action) => newState;
上一页
下一页