单向用户接口结构_UNIDIRECTIONAL_USER_INTERFACE_ARCHITECHURES

A unidirectional architecture is said to be fractal if subcomponents are structured in the same way as the whole is.

Flux

Dispatcher

an event bus for all actions,每个应用中只有一个单例Dispatcher, Dispather收到actions后,发送它们给已经注册到Dispather中的stores,每一个store会收到每一个action。

Because this is an event bus, it’s a singleton. Many Flux variants remove the need for a dispatcher, and other unidirectional architectures don’t have an equivalent to the dispatcher.

Stores

Store就是应用中存储数据的地方。store会向Dispather注册以接收actions。 store中的数据只能在对action做出反应时才能被修改。store中不能有public的修改数据的方法,只能有获取数据的方法。store决定哪种action它想有所反应。每当store的数据改变了,它一定要发出一个“change”的事件。一个应用中会有很多个store。

Actions

actions里面定义应用的内部api。它们描绘了如何与你的应用进行交互。它们是包含一个“type”的字段和一些数据的简单对象。actions应该是对那些发生的动作进行的语义化和描述化。actions里面不应该描述如何应对这些动作。用“delete-user”而不是“delete-user-id”,“clear-user-data”,“refresh-credentials”这一连串。

Views

stores中的数据被展现在views中。views可以使用你喜欢的任何一种框架。一个view能展现store中的数据的前提是它必须在那个store中订阅了change的事件。每当store中的数据改变了,view就能获得新的数据并且重新绘制。典型的情况是,当一个用户和你的应用进行交互时,actions就从views中dispatcher出去了。

Only View has composable components. Hierarchical composition happens only among React components, not with Stores neither with Actions. A React component is a UI program, and is usually not written as a Flux architecture internally. Hence Flux is not fractal(分形), where the orchestrators(协调器) are the Dispatcher and the Stores.

User event handlers are declared in the rendering. In other words, the render() function of React components handles both directions of interaction with the user: rendering and user event handlers (e.g. onClick={this.clickHandler}).

Flow of data

  1. Views发送actions到Dispatcher
  2. Dispather发送actions到每一个注册的store中
  3. Stores发送修改的数据到Views

Redux

Redux is a predictable state container for javascript apps. It can be used standalone or in connection with libraries, like React and Angular, to manage state in javascript apps.

Redux = Reducer + Flux

Action(s)

an action in Redux is a javascript object, it has a type and an optional payload.

Reducer(s)

a reducer is a pure function. It always produces the same output when the input stays the same. It has no side effects. A reducer has two inputs: state and action. The state is always the global state object from the Redux store.

(prevState, action) => newState

Redux Store singleton

manages state and has a dispatch(action) function. the store holds one global state object.

A Store is created using the createStore() factory function, taking a composition of reducer functions as argument. There is also a meta-factory applyMiddleware() function which takes middleware functions as arguments. Middlewares are mechanisms of overriding the dispatch() function of a store with additional chained functionality.

1
2
3
4
5
6
7
8
9
10
11
12
import { createStore, applyMiddleware, combineReducers } from "redux";
const store = createStore(combineReducers({
counters,
todos: undoableTodos
}), initState, applyMiddleware(logger));

store.dispatch({type:"TODO_ADD", todo:{id:0, name:"xxx", completed: false}});

store.getState();

const unsubscribe = store.subscribe(()=>{console.log(store.getState());});
unsubscribe();

Provider

a subscriber to the Store which interfaces with some “View” framework like React or Angular

Redux is unopinionated with regards to the “View” framework used to make the UI program. It can be used with React or Angular or others. In the context of this architecture, “View” is a UI program. Like Flux, Redux is not (by design) fractal and the Store is an orchestrator.

1
2
import { Provider } from "react-redux";
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById("root"));

In a real application you want to avoid the following bad practices:

  1. Re-rendering every component.
  2. Using the store instance directly. The store should be injected somehow into your component tree to make it accessible for components that need to have access to the store.
  3. Making the store globally available.

Redux can be described in three fundamental principles:

  1. Single source of truth
  2. State is read-only
  3. Changes are made with pure functions

Technically, a container component is just a React component that uses store.subscribe() to read a part of the Redux state tree and supply props to a presentational component it renders.

1
connect(mapStateToProps(state, ownProps?), mapDispatchToProps(dispatch, ownProps?))(Component)

redux-thunk

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

thunks are a functional programming technique used to delay computation. Instead of performing some work now, you produce a function body or unevaluated expression (the “thunk”) which can optionally be used to perform the work later.

In React / Redux, thunks enable us to avoid directly causing side effects in our actions, action creators, or components. Instead, anything impure will be wrapped in a thunk. Later, that thunk will be invoked by middleware to actually cause the effect. By transferring our side effects to running at a single point of the Redux loop (at the middleware level), the rest of our app stays relatively pure. Pure functions and components are easier to reason about, test, maintain, extend, and reuse.

Redux-Promise
Redux-Promise-Middleware
Redux-Saga, based on generator functions
Redux-Observable, based on RxJS observables
Redux-Loop, modeled after Elm’s effect system

react-redux - connect()

// connect() is a function that injects Redux-related props into your component.
// You can inject data and callbacks that change that data by dispatching actions.
function connect(mapStateToProps, mapDispatchToProps) {
  // It lets us inject component as the last step so people can use it as a decorator.
  // Generally you don't need to worry about it.
  return function (WrappedComponent) {
    // It returns a component
    return class extends React.Component {
      render() {
        return (
          // that renders your component
          <WrappedComponent
            {/* with its props  */}
            {...this.props}
            {/* and additional props calculated from Redux store */}
            {...mapStateToProps(store.getState(), this.props)}
            {...mapDispatchToProps(store.dispatch, this.props)}
          />
        )
      }

      componentDidMount() {
        // it remembers to subscribe to the store so it doesn't miss updates
        this.unsubscribe = store.subscribe(this.handleChange.bind(this))
      }

      componentWillUnmount() {
        // and unsubscribe later
        this.unsubscribe()
      }

      handleChange() {
        // and whenever the store state changes, it re-renders.
        this.forceUpdate()
      }
    }
  }
}

// This is not the real implementation but a mental model.
// It skips the question of where we get the "store" from (answer: <Provider> puts it in React context)
// and it skips any performance optimizations (real connect() makes sure we don't re-render in vain).

// The purpose of connect() is that you don't have to think about
// subscribing to the store or perf optimizations yourself, and
// instead you can specify how to get props based on Redux store state:

const ConnectedCounter = connect(
  // Given Redux state, return props
  state => ({
    value: state.counter,
  }),
  // Given Redux dispatch, return callback props
  dispatch => ({
    onIncrement() {
      dispatch({ type: 'INCREMENT' })
    }
  })
)(Counter)

BEST Behavior-Event-State-Tree

State

JSON-like declaration of initial state

Tree

a declarative hierarchical composition of components

Event

event listeners (on tree) that mutate state

Behavior

dynamic properties (of tree) dependent on the state

Peculiarities:

Multi-paradigm. State and Tree are fully declarative. Event is imperative(命令式). Behavior is functional. Some parts are reactive(反应式), other parts are passive(被动式) (e.g. Behavior reacts to State, and Tree is passive to the Behavior).

Behavior. Not seen in any other architecture in this post, the Behavior separates UI rendering (Tree) from its dynamic properties. These are allegedly different concerns: Tree is comparable to HTML, Behavior is comparable to CSS.

User event handlers are declared separately from rendering. BEST is one of the few unidirectional architectures that do not attach user event handlers in the rendering. User event handlers belong to Event, not to Tree.

In the context of this architecture, “View” is a Tree, and a “Component” is a Behavior-Event-Tree-State tuple. Components are UI programs. BEST is a fractal architecture.

Model-View-Update

Model

a type defining the structure of state data

View

a pure function transforming state into rendering

Actions

a type defining user events sent through mailboxes

Update

a pure function from previous state and an action to new state

Peculiarities:

Hierarchical composition everywhere. The previous architectures had hierarchical composition only in their “View”, however in the MVU architecture such composition is also found in Model and Update. Even Actions may contain nested Actions.

Components are exported as pieces. Because of the hierarchical composition everywhere, a “component” in the Elm Architecture is a tuple of: a Model type, an initial Model instance, a View function, an Action type, and an Update function. There cannot be components which deviate from this structure throughout the whole architecture. Each component is a UI program, and this architecture is fractal.

Model-View-Intent

Intent

function from Observable of user events to Observable of “actions”

Model

function from Observable of actions to Observable of state

View

function from Observable of state to Observable of rendering

Custom element

subsection of the rendering which is in itself a UI program. May be implemented as MVI, or as a Web Component. Is optional to use in a View.

Peculiarities:

Heavily based on Observables. The outcome of each part of the architecture is expressed as an Observable event stream. Because of this, it is hard or impossible to express any “data flow” or “change” without using Observables.

Intent. Roughly comparable to Event in BEST, user event handlers are declared in the Intent, separately from rendering. Unlike BEST, Intent produces Observable streams of actions, which are like those in Flux, Redux, and Elm. Unlike Flux and others, though, actions in MVI are not directly sent to a Dispatcher or a Store. They are simply available for the Model to listen.

Fully reactive. The user’s rendering reacts to the View’s output, which reacts to the Model’s output, which reacts to the Intent’s output (actions), which reacts to user events.

A MVI tuple is a UI program. This architecture is fractal if and only if all custom elements are implemented with MVI.

Nested Dialogues

a Dialogue is a function taking an Observable of user events as input (the input of Intent) and outputting an Observable of renderings (the output of View). Therefore a Dialogue is a UI program.

We generalize the definition of a Dialogue to allow other targets beyond the user, with an input Observable and an output Observable for each target. For example, if a Dialogue interfaces with a user and a server over HTTP, the Dialogue would take two Observables as input: Observable of user events and Observable of HTTP responses. Then, it would output two Observables as output: Observable of renderings and Observable of HTTP requests. This is the concept of Drivers in Cycle.js.

Nested Dialogues is in fact a meta-architecture: it has no convention for the internal structure of a component, allowing us to embed any of the aforementioned architectures into a Nested Dialogue component. The only convention regards the interface of a Dialogue’s extremes: input must be a (collection of) Observable(s), output also must be a (collection of) Observable(s). If a UI program structured as Flux or Model-View-Update or others can have its output and inputs expressed as Observables, then that UI program can be embedded into a Nested Dialogues program as a Dialogue function.

This architecture is therefore fractal (with regards to the Dialogue interface only) and general.

参考链接:

  1. https://staltz.com/unidirectional-user-interface-architectures.html