04.状态管理

状态管理

记得上次面试的时候,有人问我怎么看待全栈开发这个概念,笔者一直觉得,对于小团队与较简单的业务逻辑,全栈可以极大地提高产品开发效率。但是所谓磨刀不误砍柴工,随着对性能、清晰可维护的代码架构的需求日渐提升,类似于Meteor这样所谓的Isomorphic全栈架构反而成了一种阻碍,大大增加整个产品架构的复杂度。其中一个核心的Issue就是在于当你将前后端的状态无差别的处理,而不进行任何分割的时候,你来自于Domain/DataBase/Server/UI的状态迅猛增长,最终将你的代码变成一团乱麻。而作为全栈开发者,应该如何应对这种复杂性的陡升呢?还是需要在所谓的客户端与服务端之间划分一个明确的状态界限,并且以API ProviderConsumer的方式将客户端与服务端进行解耦,这也符合SOLID原则,每个系统内部应该尽量少的了解外部系统的细节。

What is State? |何谓状态

一言以蔽之,状态就是你应用中流动的数据。在工程师们提出了 State 这个概念之后,每个人都对它有自己独特的理解,特别是随着JavaScript富客户端应用的爆炸式增长该词也被赋予了各式各样的含义,让人们无所适从。State是对于你应用当前的属性、配置或者一些定量特征的总结。具体而言,譬如用户的提示语言,游戏中的计时器或者某个组件的可见性。另一方面,状态也代表着服务端的缓存、或者不同用户存放于数据库中的数据,这也是一种状态。

实际上,任何应用中都不会只存在某种单一的状态,状态以不同的形式出现在应用的不同层级,我们首先要做的就是搞清楚应该如何区分这些状态,并且应该如何因地制宜地处理这些状态。

很多的开发者初期会多使用本地状态,即把数据存放在组件本身,那么在业务逻辑演化地过程中,不可避免地会导致应用的复杂度增加。以评论组件为例,如果我们需要将数据从App组件移至CommentItem组件,则必须在CommentItem之前先发送至CommentList。从CommentItem引发要由App组件处理的事件时,这更加麻烦。当您尝试将另一个孩子添加到CommentItem时,情况会变得更糟。可能是CommentButton组件。这意味着当单击按钮时,您必须告诉CommentItem,它将告诉CommentList,最后是App。试图说明这一点已经让我头疼,然后考虑何时必须在实际项目中实现它。您可能会想方设法保留可能起作用的每个组件的本地状态。问题是,您将轻易地了解存在的内容以及在给定时间发生特定事件的原因。松散数据同步很容易,最终使您陷入混乱。

Component Hierarchy

Domain State |服务端状态

Domain State即是你应用服务端的状态,也就是你应用面向的某个特定领域的状态。譬如我们正在为Grocery Store开发的Web应用,可以预见的,我们会在应用中发现如下通用状态:认证、校验、错误处理等等,统一的我们也会发现很多与超级市场这一产业相关的状态。这就是所谓的领域特定业务逻辑(Domain Specific Business Logic),这些状态会应用于行业相关的业务逻辑代码。Domain State来自于服务端,并且需要根据用户的Session进行持久化存储,以便于更好地与客户端进行交互。这里以GraphQL为例展示一个简单的Domain State查询(如果你还不了解GraphQL,可以参考 GraphQL初探:RESTGraphQL,更完善的数据查询定义):

// Lets say we want to know the state of our friends list at any given time
// Lets make aGraphQL query to represent this:

user(id: "1") {
  name
  friends {
  name
  }
}

GraphQL中我们编写所谓的queries来获取Domain State,如上面的请求中我们会返回编号为1的用户的姓名与朋友信息,返回结果如下所示:

{
  "data": {
    "user": {
      "name": "Abhi Aiyer",
      "friends": [
        {
          "name": "Ben Strahan"
        },
        {
          "name": "Sashko Stubailo"
        }
      ]
    }
  }
}

如果我们需要获取更多的领域信息:

jobs(id: "32hkrv32ZKjd3jlwzhk") {
  description,
  position,
  wage {
  max
  },
  managers {
  name,
  email
  },
  status,
  published
}

这里我们从job表中希望获取更多关联信息,返回结果大概如下:

{
  "data": {
    "jobs": {
      "description": "Write cool blog posts",
      "position": "Programmer",
      "wage": {
        "max": 24
      },
      "managers": [
        {
          "name": "Larry",
          "email": "larry@stooges.com"
        },
        {
          "name": "Moe",
          "email": "moe@stooges.com"
        }
      ],
      "status": "PUBLISHED",
      "publishedAt": 1460294879
    }
  }
}

UI State

Domain State大概包含了你需要管理的核心状态中的差不多一半部分,Browser代表着另一半,并且有它自己的职责与能力。尽管现在UI开发中无状态组件的概念很流行,Browser主要负责存放用户刚刚输入的或者配置的状态信息。除此之外,Browsers还会缓存页面、设置Cookie、在LocalStorage中设置Token、载入CSS等等。除了Web Browsers之外,我们的客户端应用还有专门的状态管理工具。如果你是使用React进行界面开发并且选用了单向数据流架构,你大概会选用Flux库或者某个变种。Flux系列框架能够帮助前端开发人员管理客户端的状态,譬如某个组件的可见性控制、用户输入的获取或者响应,或者根据不同的用户尺寸展示不同的尺寸等等。这里以Reduxreducers为例:

function visibilityOfButton(state = false, action = {}) {
  switch (action.type) {
    case "TOGGLE_VISIBILITY": // return opposite of the current state
      return !state;
    default:
      return state;
  }
}

function inputFromUser(state = {}, action) {
  switch (action.type) {
    case "UPDATE_DATA": // return a new object that has data from the action
      return { ...state, ...action.data };
    default:
      return state;
  }
}

Redux中,reducers阐述了UI状态的变化之路,你可以看到当前的状态以及根据不同的Action会进行怎样的状态变化。譬如根据上面的reducers,我们的状态树大概是这个样子的:

{
  visibilityOfButton: false,
  inputFromUser: {}
}

当用户点击按钮之后,产生的Action以及对应的State变更如下所示:

// Redux utilizes a command like pattern.
// Our Store represents the receiver here
// the dispatch represents the executor
// the object passed to the dispathcer is the command
Store.dispatch({
  type: "TOGGLE_VISIBILITY",
});

("REVIOUS STATE: { visibilityOfButton: false, inputFromUser: {} }");
('ACTION -> type: "TOGGLE_VISIBILITY"');
("NEXT STATE: { visibilityOfButton: true, inputFromUser: {} }");