@adis/alt 中文文档教程

发布于 3 年前 浏览 3 项目主页 更新于 2 年前

alt

查看 API 参考以获得全面深入的文档。 如需通量的高级演练,请查看入门指南。 下面的内容仅适用于 alt 的 master 分支,而不适用于最新的发行版。 任何问题? 在 gitter 房间里问。

Gitter

NPM 版本 构建状态”></a> 
  <a href=覆盖状态 依赖状态 下载次数”></a> 
  <a href= JS.ORG

Why you should be using Alt

  • It is pure flux. Stores have no setters, the flow is unidirectional.
  • Isomorphic and works with react-native.
  • Actively maintained and being used in production.
  • Extremely flexible and unopinionated in how you use flux. Create traditional singletons or use dependency injection.
  • It is terse. No boilerplate.

What does it look like?

Alt

import Alt from 'alt';
export default new Alt();

Actions

import alt from './alt';

class TodoActions {
  updateTodo(id, text) {
    return { id, text }
  }
}

export default alt.createActions(TodoActions);

Store

import alt from './alt';
import TodoActions from './TodoActions'

class TodoStore {
  constructor() {
    this.bindListeners({
      updateTodo: TodoActions.updateTodo
    });

    this.state = {
      todos: []
    };
  }

  updateTodo(todo) {
    this.setState({ todos: this.state.todos.concat(todo) });
  }
}

export default alt.createStore(TodoStore, 'TodoStore');

View

使用 connectToStores 工具alt-utils 包(npm install alt-utils

// ES2015 (ES6)
import connectToStores from 'alt-utils/lib/connectToStores';
import { Component } from 'react';
import TodoStore from './TodoStore';

class TodoView extends Component {
  static getStores() {
    return [TodoStore];
  }

  static getPropsFromStores() {
    return TodoStore.getState();
  }

  render() {
    return (
      <ul>
        {this.props.todos.map((todo) => {
          return (
            <li key={todo.id}>{todo.text}</li>
          );
        })}
      </ul>
    );
  }
}
export default connectToStores(TodoView);

//ES2016 (ES7) using @connectToStores Decorator
import connectToStores from 'alt-utils/lib/connectToStores';
import { Component } from 'react';
import TodoStore from './TodoStore';

@connectToStores
class TodoView extends Component {
  static getStores() {
    return [TodoStore];
  }

  static getPropsFromStores() {
    return TodoStore.getState();
  }

  ...
}

In the Wild

Examples

Boilerplates

Pure Flux + More

  • Unidirectional data flow
  • Stores have no setters
  • Inversion of control
  • Single central dispatcher
  • All Stores receive the dispatch

阅读关于 通量原理

alt 的一个非常酷的方面是您可以在任何给定时间点保存整个应用程序状态的快照。 这有许多不同的用例,例如:

  • Time traveling through the state of your application. For fun and profit.
  • Being able to debug from a broken state. Have your team send you the exact state the app was in when it crashed.
  • Isomorphism. You save a snapshot that you send from the server to the client and then bootstrap back on the client.
  • Rolling back to previous stable states.

还有许多 utils 可用,它们与 alt 的接口很好:

  • ActionListener lets you listen to individual actions without having to create a store.
  • AltContainer a higher-order container component that is your swiss army knife for React.
  • AltIso addon that uses iso to render your application on both server and client.
  • atomic enables your stores for atomic transactions.
  • connectToStores a higher-order function that wraps your React components for store listening.
  • decorators a collection of useful ES7 decorators for working with alt.
  • DispatchRecorder lets you record all your dispatches and replay them back at a later time.
  • FinalStore is a Store that you can listen to that only emits when all your other stores have received all their data.
  • ImmutableUtil makes working with immutable-js easy.
  • TimeTravel enhances your stores so they are able to travel through different states in time.

Topical Guide

首先,我们通过 npm 安装 alt。 虽然 alt 也可以通过 bower 获得。

npm install alt

以下主题指南涵盖了如何以传统的通量方式将 alt 用作单例。

我们将通过使用声明的 alt 引用多次引用此代码。

const Alt = require('alt');
const alt = new Alt();

ES6

Alt 是用 ES6 编写的,并且鼓励使用 ES6。 它完全是可选的,但写起来很愉快。

您可以使用 react 附带的 es6 转译器 jstransform 或者您可以使用其他流行的 ES6 转译器之一: babeltraceur

您不需要 es6-shim,但您可以在您的 javascript 中使用它来进一步发挥作用。

Alt 确实依赖于 ES5 特性,好消息是 React 也是如此。 你可以使用 es5-shim 支持那些讨厌的旧浏览器。

Typescript Definitions and Support

alt 的打字稿定义位于 typings 目录中。 这应该包含在您的项目中,位于 typings/alt 下或您用来管理定义文件的任何文件夹中。 您可以使用 TSD 轻松导入依赖项 react 和 es6-promises。 从这里您可以像往常一样使用参考标记 来参考您的打字。 查看 alt-typescript-tutorial 了解更多信息和项目示例。

使用 Typescript 1.5,您可以使用遗留语法导入:

import Alt = require("alt");
import chromeDebug = require("alt/utils/chromeDebug");
import AltContainer = require("alt/AltContainer");

Creating Actions

操作是更新状态的方式。 他们是个大人物。

<代码>alt.createActions :: 类 -> Actions

class LocationActions {
  updateLocation(city) {
    return city;
  }
}

const locationActions = alt.createActions(LocationActions);

您从您希望发送的操作中返回数据。 如果你想在你的动作中运行异步,那么你只需返回一个函数,其中第一个参数是分派:

class LocationActions {
  updateLocationThatDoesItAsync(city) {
    return (dispatch) => {
      setTimeout(() => dispatch(city));
    };
  }
}

alt.createActions 然后返回一个包含所有定义的方法的 Object。 然后您可以直接调用您的操作。

locationActions.updateLocation('Paris');

写出直接传递数据的操作可能会非常乏味,因此有一种简写形式来编写这些本质上是 identity 函数

class LocationActions {
  constructor() {
    // for single action
    this.generateActions('updateLocation');

    // as well as for many actions
    this.generateActions('updateCity', 'updateCountry');
  }
}

const locationActions = alt.createActions(LocationActions);
locationActions.updateLocation('Las Vegas')

locationActions.updateCity('Las Vegas')
locationActions.updateCountry('US')

请记住,dispatch 只接受一个参数。 因此,如果您需要将多个参数传递到存储中,您可以使用对象。

class LocationActions {
  updateLocation(x, y) {
    return { x, y };
  }
}

const locationActions = alt.createActions(LocationActions);

locationActions.updateLocation('Miami', 'Florida');

在构造函数中创建的速记函数将作为数组传递多个参数

class LocationActions {
  constructor() {
    this.generateActions('updateLocation'); // ['South Lake Tahoe, 'California']
  }
}

const locationActions = alt.createActions(LocationActions);

locationActions.updateLocation('South Lake Tahoe', 'California');

如果您所做的只是生成一个操作列表,那么甚至还有一个速记的速记。

const locationActions = alt.generateActions('updateLocation', 'updateCity', 'updateCountry');

Stores

存储是您保留应用程序状态的一部分的地方。

您可以将商店定义为类/构造函数原型或对象。

<代码>alt.createStore :: 类,字符串 -> Store

class LocationStore {
  constructor() {
    this.bindAction(locationActions.updateLocation, this.onUpdateLocation);

    this.state = {
      city: 'Denver',
      country: 'US'
    };
  }

  onUpdateLocation(obj) {
    const { city, country } = obj
    this.setState({ city, country });
  }
}

const locationStore = alt.createStore(LocationStore);

您还可以使用常规的旧 JavaScript 对象来创建您的商店。 这更多是关于审美偏好。

const locationStore = alt.createStore({
  displayName: 'LocationStore',

  bindListeners: {
    onUpdateLocation: locationActions.updateLocation
  },

  state: {
    city: 'Denver',
    country: 'US'
  },

  onUpdateLocation(obj) {
    const { city, country } = obj
    this.setState({ city, country });
  }
});

如果您正在使用类/构造函数创建商店,那么您还可以选择将状态值直接分配给您的实例,然后您可以就地更新它们。

function LocationStore() {
  this.city = 'San Francisco';
  this.country = 'US';
}

LocationStore.prototype.onUpdateLocation = function (obj) {
  this.city = obj.city;
  this.country = obj.country;
};

alt.createStore 返回的商店实例可以通过调用 listen 来监听更新。

listen 旨在供您的 View 组件使用,以等待对每个商店所做的更改。 它返回一个函数,您可以使用它来取消收听您的商店。

locationStore.listen((data) => {
  console.log(data)
});

或者,您可以使用 unlisten 方法。 它采用与 listen 相同的功能并注销它。

另一个重要的方法是 getState,它返回当前商店状态的副本。

locationStore.getState().city === 'Denver'

Important Note

Store 类中定义的所有方法将不会在商店实例上可用。 它们可以在类中访问,但不能在返回的 通过 alt.createStore 对象。 这确保了存储没有直接设置器,并且状态仅通过保持单向流的操作保持可变。 如果你想将公共/静态函数附加到你的商店,推荐的方法是从构造函数调用 exportPublicMethods 方法:

class LocationStore {
  constructor() {
    this.exportPublicMethods({
      myPublicMethod: this.myPublicMethod
    });
  }

  myPublicMethod() {
    const internalInstanceState = this.getState();
    return internalInstanceState;
  }
}

const locationStore = alt.createStore(LocationStore);

locationStore.myPublicMethod();

另一种方法是将方法声明为 static,这将导致 alt 公开商店上的方法:

// does the same thing as above except in a more magical way
class LocationStore {
  static myPublicMethod() {
    const internalInstanceState = this.getState();
    return internalInstanceState;
  }
}

Canceling An Event

如果您不希望商店通知您可以调用的操作的视图 this.preventDefault()(或者您可以返回 false)来自动作处理程序方法。

class LocationStore {
  constructor() {
    this.bindAction(locationActions.updateCity, this.onUpdateCity);

    this.state = {
      city: 'Portland',
      country: 'US'
    };
  }

  onUpdateCity(city) {
    this.setState({ city });

    // ensure the view never finds out
    this.preventDefault();
  }
}

const locationStore = alt.createStore(LocationStore);

Constants

我以为你说没有常量? 嗯,是的,有点。 问题是,它们是自动为您创建的。 随意使用它们来绑定您的操作或使用该方法本身,无论您认为读起来更好。

class LocationStore {
  constructor() {
    this.bindAction(locationActions.UPDATE_CITY, this.onUpdateCity);

    this.state = {
      city: '',
      country: ''
    };
  }
}

const locationStore = alt.createStore(LocationStore);

Listening To Multiple Actions

class LocationActions {
  constructor() {
    this.generateActions('updateCity', 'updateCountry');
  }
}

const locationActions = alt.createActions(LocationActions);

使用 bindListeners 函数,您可以指定哪些动作处理程序属于哪些动作,这样您就可以最终控制调用和处理的内容。

bindListeners 函数是 bindAction 的逆函数。 bindListeners 将操作处理程序的对象作为键,将操作作为值。

class LocationStore {
  constructor() {
    this.bindListeners({
      handleCity: locationActions.updateCity,
      handleCountry: [locationActions.updateCountry, locationActions.updateLatLng]
    });
  }

  handleCity(data) {
    // will only be called by locationActions.updateCity()
  }

  handleCountry(data) {
    // will be called by locationActions.updateCountry() and locationActions.updateLatLng()
  }
}

或者,您可以使用快捷方式 bindActions 绑定 locationActions 中的所有

class LocationStore {
  constructor() {
    this.bindActions(locationActions);

    this.state = {
      city: 'Austin',
      country: 'US'
    };
  }

  onUpdateCity(city) {
    this.setState({ city });
  }

  onUpdateCountry(country) {
    this.setState({ country });
  }
}

const locationStore = alt.createStore(LocationStore);

操作,这些操作具有 onCamelCasedAction 方法或 actionName 将绑定商店中可用的方法。 在此示例中,locationActions.updateCity 将由 onUpdateCity 处理。 调用操作处理程序 updateCityonUpdateCity 之间没有区别,这只是审美偏好的问题。

Managing Store Data Dependencies

waitFor 主要是 Flux 的 Dispatcher waitFor 的别名。 以下是有关 waitFor 设计用途的 flux 文档的摘录:

随着应用程序的增长,不同商店之间的依赖关系几乎是确定的。 Store A必然需要Store B先更新自己,这样Store A才能知道如何更新自己。 我们需要调度程序能够调用商店 B 的回调,并在继续处理商店 A 之前完成该回调。要以声明方式断言此依赖关系,商店需要能够对调度程序说:“我需要等待以便商店 B 完成此操作的处理。” 调度程序通过其 waitFor() 方法提供此功能。

您可以像这样使用 waitFor:

const dependingStore = alt.createStore(class DependingStore {
  constructor() {
    this.bindActions(someActions);

    this.state = { answer: 42 };
  }

  onRandom(answer) {
    this.setState({ answer });
  }
})

const locationStore = alt.createStore(class LocationStore {
  constructor() {
    this.bindActions(someOtherActions)

    this.state = {
      meaningOfLife: null
    };
  }

  onThings() {
    this.waitFor(dependingStore.dispatchToken);

    this.setState({ meaningOfLife: dependingStore.getState().answer });
  }
})

您还可以通过传入数组来 waitFor 多个存储:this.waitFor([store1.dispatchToken, store2.dispatchToken])

Views

您选择的视图对 alt 并不重要。 重要的是了解视图如何使用商店的数据,即通过事件侦听器。

在此示例中,我将使用 React,但您可以自由使用您选择的库。

class LocationView extends React.Component {
  // these are methods that work with `connectToStores` which connects
  // one or many stores to your component passing the state in as props.
  // you're free to choose how the state from the store is passed into the
  // component as props.

  // this automatically does the listening/unlistening for you as well as
  // handles the state changes
  static getStores() {
    return [locationStore];
  }

  static getPropsFromStores() {
    return locationStore.getState();
  }

  render() {
    return (
      <div>
        <p>
          City {this.props.city}
        </p>
        <p>
          Country {this.props.country}
        </p>
      </div>
    )
  }
}

// just make sure to wrap your component with connectToStores()
export default connectToStores(LocationView);

Full Circle

通过让您的视图启动新操作来重新启动循环。

Alt Features

Snapshots

takeSnapshot :: ?...string -> string

快照是 alt 的核心组件。 这个想法是,在任何给定的时间点,您都可以takeSnapshot 并获得整个应用程序的状态 为持久化、传输、日志记录或调试而序列化。

拍摄快照就像调用 alt.takeSnapshot() 一样简单。 它还可以将可选数量的参数作为字符串,这些参数对应于您希望包含在快照中的商店名称。 这使您可以拍摄应用程序数据子集的快照。

Bootstrapping

bootstrap :: string -> undefined

引导可以进行任意多次,但通常在初始化应用程序时使用。 alt.bootstrap() 函数获取快照(JSON 字符串) 您已经使用该快照保存并重新加载了所有状态,在此过程中不会向您的组件发出任何事件,因此最好还是这样做 在视图甚至呈现之前初始化。 如果您需要发出更改事件,您可以在 bootstrap 生命周期方法中使用 this.emitChange

如果您正在运行一个同构应用程序,或者如果您将状态持久化到本地存储然后稍后在 init 上检索它,那么 Bootstrap 非常有用。 您可以在服务器端保存快照,将其发送下来,然后将其引导回客户端。

如果你正在引导,那么建议你传入一个唯一的标识符,类的名称就足够了,到 createStore 以便以后可以引用它来进行引导。

alt.createStore(LocationStore, 'LocationStore')

Rollback

rollback :: undefined

如果你搞砸了状态,或者你只是想回滚,你可以调用 alt.rollback()。 回滚在某种意义上是相当愚蠢的 如果出现错误,它不是自动的,它只会回滚到上次保存的快照,这意味着您必须先保存快照才能回滚。

Flushing

flush :: string

Flush 获取当前状态的快照,然后将所有存储重置回其原始初始状态。 如果您将 alt 存储用作单例并由于并发而进行服务器端渲染,这将很有用。 在此特定场景中,您将通过 bootstrap 加载数据,然后使用 flush 拍摄快照、呈现数据并重置您的存储,以便它们为下一次做好准备要求。

Recycling

<代码>回收 :: ?...字符串 -> undefined

如果您希望将特定或所有商店的状态重置回其原始初始状态,您可以调用 recycle。 Recycle 将可选数量的参数作为字符串,这些字符串对应于您想要重置的商店名称。 如果未提供参数,则所有存储都将被重置。

Lifecycle Methods

在启动、快照或回收时,您可以将特殊方法分配给您的商店,以确保需要完成任何记账工作。 您可以将它们放在商店的构造函数中。

bootstrap 在商店启动后调用。 您可以在此处添加一些逻辑来获取自举数据并对其进行操作。

class Store {
  constructor() {
    this.on('bootstrap', () => {
      // do something here
    })
  }
}

init 在商店被初始化时以及商店被回收时被调用。

class Store {
  constructor() {
    this.on('init', () => {
      // do something here
    })
  }
}

rollback 在所有存储回滚时调用。

class Store {
  constructor() {
    this.on('rollback', () => {
      // do something here
    })
  }
}

error 在发货期间您的商店发生错误时调用。 您可以使用此侦听器来捕获错误并执行任何清理任务。

class Store {
  constructor() {
    this.on('error', (err, actionName, payloadData, currentState) => {
      if (actionName === MyActions.fire) {
        logError(err, payloadData);
      }
    });

    this.bindListeners({
      handleFire: MyActions.fire
    });
  }

  handleFire() {
    throw new Error('Something is broken');
  }
}

查看所有生命周期方法

Single Dispatcher

单个调度程序实例可用于侦听所有经过的事件。 您可以通过 dispatcher 属性访问它:alt.dispatcher 并且监听所有事件就像

alt.dispatcher.register(console.log.bind(console))

每个商店都有对调度程序的引用

alt.createStore(class MyStore {
  constructor() {
    this.dispatcher.register(console.log.bind(console))
  }
})

Flexibility

一样简单您可以选择以多种方式使用 alt,就像您使用 flux 一样。 这意味着您的异步数据获取可以存在于操作中,或者它们可以存在于商店中。 商店也可以是不断变化的传统单例,或者您可以创建一个实例并拥有多个商店副本。 这导致我们进入服务器端渲染。

Server Side Rendering

Alt 是在考虑同构的情况下构建的。 这意味着您可以在服务器端运行完整的流量并在客户端恢复。

在服务器上使用 flux 有两种选择:

  • Keep stores as singletons, keep data loading synchronous, bootstrap, and flush.
  • Create multiple instances of flux and inject the context into your app.

Stores as Singletons

使用这种方法,您的商店是单身人士。 任何加载数据的操作都必须是同步的,这意味着您可以在操作和存储之外获取数据,一旦完成,您就会触发一个加载的同步操作 商店。 或者,您可以收集所有数据,一旦完成,您就调用 bootstrap() 为所有商店播种一些初始数据。

一旦您完成加载商店的数据,您将调用 flush(),它会拍摄快照发送给客户端,然后将所有商店的状态重置回其初始状态。 这允许商店为下一个服务器请求做好准备。

Flux Instances

在构建同构应用程序时,创建单独的 flux 实例而不是依赖单例会有所帮助。

单例的问题在于您需要通过清除所有状态并在每次请求时用新状态重新加载它们来管理它们,因为请求是同时发生的。 如果您已经拥有数据并且只需要将其加载到 flux 中,或者如果您不想与客户端共享数据获取逻辑,这不是问题——在这种情况下,您可以将所有数据加载到一旦在服务器上并渲染一次就完成了。

如果您希望与客户端和服务器共享数据获取,单例只会成为一个问题,不想使用类似 Render 的东西来定义您的数据获取组件级别,或者如果您有一个非常复杂的数据获取方案,其中一些获取取决于其他获取的结果。 在这些情况下,创建单独的实例(或副本)将通量沙盒化到每个请求,因此其他异步请求不会改变存储中的状态。

采用这种方法意味着您要权衡将 flux 实例注入您的应用程序以检索商店和使用操作。 这种方法类似于 fluxible 解决同构应用程序的方式。

创建一个新的 alt 实例相当简单。

class Flux extends Alt {
  constructor() {
    super();

    this.addActions('myActions', ActionCreators);
    this.addStore('storeName', Store);
  }
}

const flux = new Flux();
// client.js

React.render(
  <App flux={flux} />,
  document.body
);
// server.js
React.renderToString(<App flux={flux} />);
// retrieving stores
flux.getStore('storeName').getState();

// actions
flux.getActions('myActions');

Picking back up on the client

为了帮助促进同构,alt 建议您使用 iso,这是一个辅助函数,它将服务器上的数据序列化为标记,然后解析该数据回到客户端上可用的 JavaScript。 对于全栈通量方法,Iso 是对 alt 的一个很好的补充。

Converting a flux application to alt

  1. Importing the chat project.
  2. Adding alt and removing boilerplate.
  3. Converting some actions and the last action.
  4. Converting the stores MessageStore, ThreadStore, and UnreadThreadStore.
  5. Finishing touches.

Differences Example

Flux 有常量,调度器也很愚蠢,因为它只接受你在动作中传递的内容 并将其通过管道传输到商店。 这完全没问题,但不是你应该写的东西。 常量的好处是您可以在您的应用程序中轻松地 grep 查找它们并查看位置 所有的动作都被调用,使用 alt 你可以获得相同的好处而无需管理它们。

Before: Flux

var keyMirror = require('keymirror');

var actionConstants = keyMirror({
  HANDLE_ACTION: null
});

var action = {
  foo() {
    AppDispatcher.handleAction({ type: actionConstants.HANDLE_ACTION, data: 'foo' })
  }
};

var AppDispatcher = Object.assign(new Dispatcher(), {
  handleAction(payload) {
    this.dispatch(payload);
  }
});

After: Alt

class Action {
  handleAction() {
    return 'foo';
  }
}

const action = alt.createActions(Action);

TL;DR

  • Isomorphic
  • Pure Flux
  • No constants
  • No static string checking
  • No giant switch statement
  • Save state snapshots
  • Rollbacks
  • Bootstrap components on app load
  • Light-weight and terse
  • ES6 Syntax, code your actions and stores with classes
  • Flexible
  • No direct setters on stores
  • Single dispatcher
  • Global listening for debugging
  • Small library

License

麻省理工学院

alt

Check out the API Reference for full in-depth docs. For a high-level walk-through on flux, take a look at the Getting Started guide. What follows below applies only to the master branch of alt and not the latest distribution. Any questions? ask in the gitter room.

Gitter

NPM version Build Status Coverage Status Dependency Status Download Count JS.ORG

Why you should be using Alt

  • It is pure flux. Stores have no setters, the flow is unidirectional.
  • Isomorphic and works with react-native.
  • Actively maintained and being used in production.
  • Extremely flexible and unopinionated in how you use flux. Create traditional singletons or use dependency injection.
  • It is terse. No boilerplate.

What does it look like?

Alt

import Alt from 'alt';
export default new Alt();

Actions

import alt from './alt';

class TodoActions {
  updateTodo(id, text) {
    return { id, text }
  }
}

export default alt.createActions(TodoActions);

Store

import alt from './alt';
import TodoActions from './TodoActions'

class TodoStore {
  constructor() {
    this.bindListeners({
      updateTodo: TodoActions.updateTodo
    });

    this.state = {
      todos: []
    };
  }

  updateTodo(todo) {
    this.setState({ todos: this.state.todos.concat(todo) });
  }
}

export default alt.createStore(TodoStore, 'TodoStore');

View

Using the connectToStores util from alt-utils package (npm install alt-utils)

// ES2015 (ES6)
import connectToStores from 'alt-utils/lib/connectToStores';
import { Component } from 'react';
import TodoStore from './TodoStore';

class TodoView extends Component {
  static getStores() {
    return [TodoStore];
  }

  static getPropsFromStores() {
    return TodoStore.getState();
  }

  render() {
    return (
      <ul>
        {this.props.todos.map((todo) => {
          return (
            <li key={todo.id}>{todo.text}</li>
          );
        })}
      </ul>
    );
  }
}
export default connectToStores(TodoView);

or

//ES2016 (ES7) using @connectToStores Decorator
import connectToStores from 'alt-utils/lib/connectToStores';
import { Component } from 'react';
import TodoStore from './TodoStore';

@connectToStores
class TodoView extends Component {
  static getStores() {
    return [TodoStore];
  }

  static getPropsFromStores() {
    return TodoStore.getState();
  }

  ...
}

In the Wild

Examples

Boilerplates

Pure Flux + More

  • Unidirectional data flow
  • Stores have no setters
  • Inversion of control
  • Single central dispatcher
  • All Stores receive the dispatch

Read about the Principles of Flux.

One really cool aspect of alt is that you can save snapshots of the entire application's state at any given point in time. This has many different use cases like:

  • Time traveling through the state of your application. For fun and profit.
  • Being able to debug from a broken state. Have your team send you the exact state the app was in when it crashed.
  • Isomorphism. You save a snapshot that you send from the server to the client and then bootstrap back on the client.
  • Rolling back to previous stable states.

There are also many utils available which interface well with alt:

  • ActionListener lets you listen to individual actions without having to create a store.
  • AltContainer a higher-order container component that is your swiss army knife for React.
  • AltIso addon that uses iso to render your application on both server and client.
  • atomic enables your stores for atomic transactions.
  • connectToStores a higher-order function that wraps your React components for store listening.
  • decorators a collection of useful ES7 decorators for working with alt.
  • DispatchRecorder lets you record all your dispatches and replay them back at a later time.
  • FinalStore is a Store that you can listen to that only emits when all your other stores have received all their data.
  • ImmutableUtil makes working with immutable-js easy.
  • TimeTravel enhances your stores so they are able to travel through different states in time.

Topical Guide

First we install alt through npm. Although alt is also available through bower.

npm install alt

The following topical guide covers on using alt as a singleton in a traditional flux way.

We'll be referring back to this code a lot by using the alt reference declared.

const Alt = require('alt');
const alt = new Alt();

ES6

Alt is written in, and encourages ES6. It is completely optional but it is pleasant to write.

You can use the es6 transpiler that comes with react courtesy of jstransform or you can use one of the other popular ES6 transpilers: babel or traceur.

You won't need an es6-shim but you can use one for further goodies in your javascripts.

Alt does depend on ES5 features, the good news is so does React. You can use es5-shim to support those pesky old browsers.

Typescript Definitions and Support

The typescript definitions for alt are located in the typings directory. This should be included in your project under typings/alt or whatever folder you use to manage your definitions files. You can import the dependencies react and es6-promises, easily with TSD. From here you can reference your typings as per usual with a reference tag <reference path="<path>.d.ts" />. Check the alt-typescript-tutorial for more information and project examples.

Using Typescript 1.5 you can import with the legacy syntax:

import Alt = require("alt");
import chromeDebug = require("alt/utils/chromeDebug");
import AltContainer = require("alt/AltContainer");

Creating Actions

Actions are the way you update state. They're kind of a big deal.

alt.createActions :: Class -> Actions

class LocationActions {
  updateLocation(city) {
    return city;
  }
}

const locationActions = alt.createActions(LocationActions);

You return the data from your action that you wish to dispatch. If you want to run async in your actions then you simply return a function where the first argument is the dispatch:

class LocationActions {
  updateLocationThatDoesItAsync(city) {
    return (dispatch) => {
      setTimeout(() => dispatch(city));
    };
  }
}

alt.createActions then returns an Object containing all the methods defined. You can then call your actions directly.

locationActions.updateLocation('Paris');

Writing out actions that pass data through directly can get quite tedious so there's a shorthand for writing these what are essentially identity functions

class LocationActions {
  constructor() {
    // for single action
    this.generateActions('updateLocation');

    // as well as for many actions
    this.generateActions('updateCity', 'updateCountry');
  }
}

const locationActions = alt.createActions(LocationActions);
locationActions.updateLocation('Las Vegas')

locationActions.updateCity('Las Vegas')
locationActions.updateCountry('US')

Remember, dispatch only takes one argument. Therefore, if you need to pass multiple arguments into a store you can use an Object.

class LocationActions {
  updateLocation(x, y) {
    return { x, y };
  }
}

const locationActions = alt.createActions(LocationActions);

locationActions.updateLocation('Miami', 'Florida');

A shorthand function created in the constructor will pass through the multiple parameters as an Array

class LocationActions {
  constructor() {
    this.generateActions('updateLocation'); // ['South Lake Tahoe, 'California']
  }
}

const locationActions = alt.createActions(LocationActions);

locationActions.updateLocation('South Lake Tahoe', 'California');

There's even a shorthand for the shorthand if all you're doing is generating a list of actions

const locationActions = alt.generateActions('updateLocation', 'updateCity', 'updateCountry');

Stores

Stores are where you keep a part of your application's state.

You can either define your stores as a class/constructor-prototype or as an Object.

alt.createStore :: Class, string -> Store

class LocationStore {
  constructor() {
    this.bindAction(locationActions.updateLocation, this.onUpdateLocation);

    this.state = {
      city: 'Denver',
      country: 'US'
    };
  }

  onUpdateLocation(obj) {
    const { city, country } = obj
    this.setState({ city, country });
  }
}

const locationStore = alt.createStore(LocationStore);

You can also use a regular old JavaScript Object to create your stores. This is more about aesthetic preference.

const locationStore = alt.createStore({
  displayName: 'LocationStore',

  bindListeners: {
    onUpdateLocation: locationActions.updateLocation
  },

  state: {
    city: 'Denver',
    country: 'US'
  },

  onUpdateLocation(obj) {
    const { city, country } = obj
    this.setState({ city, country });
  }
});

If you're creating a store using a class/constructor then you also have the option of assigning your state values to your instance directly and then you're able to update them in place.

function LocationStore() {
  this.city = 'San Francisco';
  this.country = 'US';
}

LocationStore.prototype.onUpdateLocation = function (obj) {
  this.city = obj.city;
  this.country = obj.country;
};

Store instances returned by alt.createStore can be listened to for updates by calling listen.

listen is meant to be used by your View components in order to await changes made to each store. It returns a function you can use to un-listen to your store.

locationStore.listen((data) => {
  console.log(data)
});

Alternatively, you can use the unlisten method. It takes in the same function you used for listen and unregisters it.

Another important method is getState, which returns a copy of the current store's state.

locationStore.getState().city === 'Denver'

Important Note

All defined methods in your Store class will not be available on the store instance. They are accessible within the class but not on the returned Object via alt.createStore. This ensures that stores have no direct setters and the state remains mutable only through actions keeping the flow unidirectional. If you want to attach public/static functions to your store the recommended method is to call the exportPublicMethods method from the constructor:

class LocationStore {
  constructor() {
    this.exportPublicMethods({
      myPublicMethod: this.myPublicMethod
    });
  }

  myPublicMethod() {
    const internalInstanceState = this.getState();
    return internalInstanceState;
  }
}

const locationStore = alt.createStore(LocationStore);

locationStore.myPublicMethod();

An alternative is to declare the method as static, which will cause alt to expose the method on the store:

// does the same thing as above except in a more magical way
class LocationStore {
  static myPublicMethod() {
    const internalInstanceState = this.getState();
    return internalInstanceState;
  }
}

Canceling An Event

If you don't want the store to inform the view of an action you can call this.preventDefault() (or you can return false) from inside an action handler method.

class LocationStore {
  constructor() {
    this.bindAction(locationActions.updateCity, this.onUpdateCity);

    this.state = {
      city: 'Portland',
      country: 'US'
    };
  }

  onUpdateCity(city) {
    this.setState({ city });

    // ensure the view never finds out
    this.preventDefault();
  }
}

const locationStore = alt.createStore(LocationStore);

Constants

I thought you said there were no constants? Well, yeah, sort of. The thing is, they're automagically created for you. Feel free to use them to bind your actions or use the method itself, whatever reads better in your opinion.

class LocationStore {
  constructor() {
    this.bindAction(locationActions.UPDATE_CITY, this.onUpdateCity);

    this.state = {
      city: '',
      country: ''
    };
  }
}

const locationStore = alt.createStore(LocationStore);

Listening To Multiple Actions

class LocationActions {
  constructor() {
    this.generateActions('updateCity', 'updateCountry');
  }
}

const locationActions = alt.createActions(LocationActions);

Using the function bindListeners you're able to specify which action handlers belong to which actions this way you have ultimate control over what gets called and handled.

The function bindListeners is the inverse of bindAction. bindListeners takes an object of action handlers as keys and actions as a value.

class LocationStore {
  constructor() {
    this.bindListeners({
      handleCity: locationActions.updateCity,
      handleCountry: [locationActions.updateCountry, locationActions.updateLatLng]
    });
  }

  handleCity(data) {
    // will only be called by locationActions.updateCity()
  }

  handleCountry(data) {
    // will be called by locationActions.updateCountry() and locationActions.updateLatLng()
  }
}

Alternatively, you can bind all the actions inside locationActions using the shortcut bindActions

class LocationStore {
  constructor() {
    this.bindActions(locationActions);

    this.state = {
      city: 'Austin',
      country: 'US'
    };
  }

  onUpdateCity(city) {
    this.setState({ city });
  }

  onUpdateCountry(country) {
    this.setState({ country });
  }
}

const locationStore = alt.createStore(LocationStore);

Actions who have a onCamelCasedAction method or an actionName method available in the store will be bound. In this example locationActions.updateCity will be handled by onUpdateCity. There is no difference between calling the action handler updateCity or onUpdateCity it's just a matter of aesthetic preference.

Managing Store Data Dependencies

waitFor is mostly an alias to Flux's Dispatcher waitFor. Here's an excerpt from the flux docs on what waitFor is designed for:

As an application grows, dependencies across different stores are a near certainty. Store A will inevitably need Store B to update itself first, so that Store A can know how to update itself. We need the dispatcher to be able to invoke the callback for Store B, and finish that callback, before moving forward with Store A. To declaratively assert this dependency, a store needs to be able to say to the dispatcher, "I need to wait for Store B to finish processing this action." The dispatcher provides this functionality through its waitFor() method.

You can use waitFor like so:

const dependingStore = alt.createStore(class DependingStore {
  constructor() {
    this.bindActions(someActions);

    this.state = { answer: 42 };
  }

  onRandom(answer) {
    this.setState({ answer });
  }
})

const locationStore = alt.createStore(class LocationStore {
  constructor() {
    this.bindActions(someOtherActions)

    this.state = {
      meaningOfLife: null
    };
  }

  onThings() {
    this.waitFor(dependingStore.dispatchToken);

    this.setState({ meaningOfLife: dependingStore.getState().answer });
  }
})

You can also waitFor multiple stores by passing in an Array: this.waitFor([store1.dispatchToken, store2.dispatchToken])

Views

Your choice of view isn't important to alt. What's important is to know how the view consumes the store's data, and that is via event listeners.

In this example I'll be using React, but you're free to use your library of choice.

class LocationView extends React.Component {
  // these are methods that work with `connectToStores` which connects
  // one or many stores to your component passing the state in as props.
  // you're free to choose how the state from the store is passed into the
  // component as props.

  // this automatically does the listening/unlistening for you as well as
  // handles the state changes
  static getStores() {
    return [locationStore];
  }

  static getPropsFromStores() {
    return locationStore.getState();
  }

  render() {
    return (
      <div>
        <p>
          City {this.props.city}
        </p>
        <p>
          Country {this.props.country}
        </p>
      </div>
    )
  }
}

// just make sure to wrap your component with connectToStores()
export default connectToStores(LocationView);

Full Circle

Restart the loop by making your views kick off new actions.

Alt Features

Snapshots

takeSnapshot :: ?...string -> string

Snapshots are a core component of alt. The idea is that at any given point in time you can takeSnapshot and have your entire application's state serialized for persistence, transferring, logging, or debugging.

Taking a snapshot is as easy as calling alt.takeSnapshot(). It can also take an optional number of arguments as strings which correspond to the store names you would like to include in the snapshot. This allows you to take a snapshot of a subset of your app's data.

Bootstrapping

bootstrap :: string -> undefined

Bootstrapping can be done as many times as you wish, but it is common to use when initializing your application. The alt.bootstrap() function takes in a snapshot (JSON string) you've saved and reloads all the state with that snapshot, no events will be emitted to your components during this process, so again, it's best to do this on init before the view has even rendered. If you need to emit a change event, you can use this.emitChange inside of your bootstrap life cycle method.

Bootstrap is great if you're running an isomorphic app, or if you're persisting state to localstorage and then retrieving it on init later on. You can save a snapshot on the server side, send it down, and then bootstrap it back on the client.

If you're bootstrapping then it is recommended you pass in a unique Identifier, name of the class is good enough, to createStore so that it can be referenced later for bootstrapping.

alt.createStore(LocationStore, 'LocationStore')

Rollback

rollback :: undefined

If you've screwed up the state, or you just feel like rolling back you can call alt.rollback(). Rollback is pretty dumb in the sense that it's not automatic in case of errors, and it only rolls back to the last saved snapshot, meaning you have to save a snapshot first in order to roll back.

Flushing

flush :: string

Flush takes a snapshot of the current state and then resets all the stores back to their original initial state. This is useful if you're using alt stores as singletons and doing server side rendering because of concurrency. In this particular scenario you would load the data in via bootstrap and then use flush to take a snapshot, render the data, and reset your stores so they are ready for the next request.

Recycling

recycle :: ?...string -> undefined

If you wish to reset a particular, or all, store's state back to their original initial state you would call recycle. Recycle takes an optional number of arguments as strings which correspond to the store's names you would like reset. If no argument is provided then all stores are reset.

Lifecycle Methods

When bootstrapping, snapshotting, or recycling there are special methods you can assign to your store to ensure any bookeeping that needs to be done. You would place these in your store's constructor.

bootstrap is called after the store has been bootstrapped. Here you can add some logic to take your bootstrapped data and manipulate it.

class Store {
  constructor() {
    this.on('bootstrap', () => {
      // do something here
    })
  }
}

init is called when the store is initialized as well as whenever a store is recycled.

class Store {
  constructor() {
    this.on('init', () => {
      // do something here
    })
  }
}

rollback is called whenever all the stores are rolled back.

class Store {
  constructor() {
    this.on('rollback', () => {
      // do something here
    })
  }
}

error is called whenever an error occurs in your store during a dispatch. You can use this listener to catch errors and perform any cleanup tasks.

class Store {
  constructor() {
    this.on('error', (err, actionName, payloadData, currentState) => {
      if (actionName === MyActions.fire) {
        logError(err, payloadData);
      }
    });

    this.bindListeners({
      handleFire: MyActions.fire
    });
  }

  handleFire() {
    throw new Error('Something is broken');
  }
}

See all the lifecycle methods

Single Dispatcher

A single dispatcher instance is made available for listening to all events passing through. You can access this via the dispatcher property: alt.dispatcher and listening to all events is as easy as

alt.dispatcher.register(console.log.bind(console))

Each store has a reference to the dispatcher as well

alt.createStore(class MyStore {
  constructor() {
    this.dispatcher.register(console.log.bind(console))
  }
})

Flexibility

You can choose to use alt in many ways just like you'd use flux. This means your asynchronous data fetching can live in the actions, or they can live in the stores. Stores may also be traditional singletons as in flux, or you can create an instance and have multiple store copies. This leads us into server side rendering.

Server Side Rendering

Alt was built with isomorphism in mind. This means that you can run full flux server-side and pick back up on the client-side.

There are two options for using flux on the server:

  • Keep stores as singletons, keep data loading synchronous, bootstrap, and flush.
  • Create multiple instances of flux and inject the context into your app.

Stores as Singletons

With this approach your stores are singletons. Any actions that load data must be synchronous, meaning you can fetch your data outside of actions and stores, and once done you fire off a synchronous action which loads the store. Alternatively, you can gather all of your data, and once complete, you call bootstrap() which seeds all the stores with some initial data.

Once you've completed loading the stores with data you call flush() which takes a snapshot to send to the client and then resets all the stores' state back to their initial state. This allows the stores to be ready for the next server request.

Flux Instances

Creating separate instances of flux rather than relying on singletons can help when building isomorphic applications.

The problem with singletons is that you need to manage them by clearing out all their state and reloading them with new state on every request because requests happen concurrently. This isn't a problem if you already have your data and just need to load it into flux, or if you don't want to share your data fetching logic with the client -- in which case you can just load all your data at once on the server and render once that is all complete.

Singletons only become a problem if you wish to share data fetching with client and server, don't want to use something like Render to define your data fetching at the component level, or if you have a really complex data fetching scheme where some fetches depend on the result of other ones. In these cases creating separate instances (or copies) keeps flux sandboxed to each request so other async requests won't mutate the state in the stores.

Taking this approach means you're making the trade-off of injecting the flux instance into your application in order to retrieve the stores and use the actions. This approach is similar to how fluxible solves isomorphic applications.

Creating a new alt instances is fairly simple.

class Flux extends Alt {
  constructor() {
    super();

    this.addActions('myActions', ActionCreators);
    this.addStore('storeName', Store);
  }
}

const flux = new Flux();
// client.js

React.render(
  <App flux={flux} />,
  document.body
);
// server.js
React.renderToString(<App flux={flux} />);
// retrieving stores
flux.getStore('storeName').getState();

// actions
flux.getActions('myActions');

Picking back up on the client

To help facilitate with isomorphism alt recommends you use iso, a helper function which serializes the data on the server into markup and then parses that data back into usable JavaScript on the client. Iso is a great complement to alt for a full-stack flux approach.

Converting a flux application to alt

  1. Importing the chat project.
  2. Adding alt and removing boilerplate.
  3. Converting some actions and the last action.
  4. Converting the stores MessageStore, ThreadStore, and UnreadThreadStore.
  5. Finishing touches.

Differences Example

Flux has constants, the dispatcher is also pretty dumb as in it just takes what you passed in the action and pipes it through to the store. This is completely fine but not something you should be expected to write. The nice thing about constants is that you can easily grep for them in your application and see where all the actions are being called, with alt you get the same benefit without having to manage them.

Before: Flux

var keyMirror = require('keymirror');

var actionConstants = keyMirror({
  HANDLE_ACTION: null
});

var action = {
  foo() {
    AppDispatcher.handleAction({ type: actionConstants.HANDLE_ACTION, data: 'foo' })
  }
};

var AppDispatcher = Object.assign(new Dispatcher(), {
  handleAction(payload) {
    this.dispatch(payload);
  }
});

After: Alt

class Action {
  handleAction() {
    return 'foo';
  }
}

const action = alt.createActions(Action);

TL;DR

  • Isomorphic
  • Pure Flux
  • No constants
  • No static string checking
  • No giant switch statement
  • Save state snapshots
  • Rollbacks
  • Bootstrap components on app load
  • Light-weight and terse
  • ES6 Syntax, code your actions and stores with classes
  • Flexible
  • No direct setters on stores
  • Single dispatcher
  • Global listening for debugging
  • Small library

License

MIT

更多

友情链接

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文