在 Flux 架构中,您如何管理商店生命周期?

我正在阅读有关Flux的信息,但示例Todo应用程序过于简单,我无法理解一些关键点。

想象一下,像Facebook这样的单页应用程序具有用户个人资料页面。在每个用户个人资料页面上,我们希望以无限滚动的方式显示一些用户信息及其最后的帖子。我们可以从一个用户配置文件导航到另一个用户配置文件。

在 Flux 架构中,这与存储和调度程序有何对应关系?

我们会为每个用户使用一个,还是会有某种全球商店?对于调度程序,我们是为每个“用户页面”创建一个新的调度程序,还是使用单例?最后,体系结构的哪个部分负责管理“特定于页面”的存储的生命周期以响应路由更改?PostStore

此外,单个伪页面可能具有多个相同类型的数据列表。例如,在个人资料页面上,我想同时显示关注者和关注者。在这种情况下,单例如何工作?会管理和?UserStoreUserPageStorefollowedBy: UserStorefollows: UserStore


答案 1

在 Flux 应用程序中,应该只有一个调度程序。所有数据都流经此中央集线器。拥有单例调度程序允许它管理所有应用商店。当您需要商店 #1 更新本身,然后让商店 #2 根据操作和商店 #1 的状态自行更新时,这一点变得很重要。Flux 假设这种情况在大型应用中是一种可能性。理想情况下,这种情况不需要发生,如果可能的话,开发人员应该努力避免这种复杂性。但是单例调度程序已准备好在时机成熟时处理它。

商店也是单例的。它们应该尽可能保持独立和分离 - 一个可以从控制器视图查询的独立宇宙。进入应用商店的唯一途径是通过它在调度程序中注册的回调。唯一的出路是通过 getter 函数。当存储的状态发生更改时,存储也会发布事件,因此控制器视图可以使用 getters 知道何时查询新状态。

在你的示例应用中,将有一个 .同一个商店可以管理“页面”(伪页面)上的帖子,该页面更像是FB的Newsfeed,其中帖子来自不同的用户。它的逻辑域是帖子列表,它可以处理任何帖子列表。当我们从伪页面移动到伪页面时,我们希望重新初始化存储的状态以反映新状态。我们可能还希望在 localStorage 中缓存以前的状态,作为在伪页面之间来回移动的优化,但我的倾向是设置一个等待所有其他存储,管理伪页面上所有存储的与 localStorage 的关系,然后更新其自己的状态。请注意,这不会存储有关帖子的任何内容 - 这是 .它只需知道特定的伪页面是否已被缓存,因为伪页面是其域。PostStorePageStorePageStorePostStore

将有一个方法。此方法将始终清除旧状态(即使这是第一次初始化),然后根据它通过调度程序通过操作接收的数据创建状态。从一个伪页面移动到另一个伪页面可能会涉及一个操作,这将触发 对 的调用。从本地缓存中检索数据,从服务器检索数据,乐观渲染和XHR错误状态有一些细节需要解决,但这是一般的想法。PostStoreinitialize()PAGE_UPDATEinitialize()

如果特定的伪页面不需要应用程序中的所有存储区,我不完全确定除了内存约束之外,是否有任何理由销毁未使用的伪页面。但商店通常不会消耗大量内存。您只需要确保删除要销毁的控制器视图中的事件侦听器即可。这是在 React 的方法中完成的。componentWillUnmount()


答案 2

(注意:我使用过使用 JSX Harmony 选项的 ES6 语法。

作为练习,我编写了一个示例 Flux 应用,允许浏览和存储库。
它基于fisherwebdev的答案,但也反映了我用于规范化API响应的方法。Github users

我记录了我在学习Flux时尝试过的一些方法。
我试图让它接近现实世界(分页,没有假的本地存储API)。

这里有一些我特别感兴趣的内容:

如何对商店进行分类

我试图避免在其他 Flux 示例中看到的一些重复,特别是在商店中。我发现将品牌旗舰店逻辑地分为三类很有用:

内容存储包含所有应用程序实体。具有 ID 的所有内容都需要自己的内容存储。呈现单个项目的组件会向内容存储请求最新数据。

内容存储从所有服务器操作中收集其对象。例如,查看 action.response.entities.users(如果存在),而不考虑触发了哪个操作。不需要 .Normalizr可以很容易地将任何API响应扁平化为这种格式。UserStoreswitch

// Content Stores keep their data like this
{
  7: {
    id: 7,
    name: 'Dan'
  },
  ...
}

列表存储跟踪出现在某些全局列表中的实体的 ID(例如“源”、“您的通知”)。在这个项目中,我没有这样的商店,但我想无论如何我都会提到它们。它们处理分页。

它们通常只对几个动作做出反应(例如,、、)。REQUEST_FEEDREQUEST_FEED_SUCCESSREQUEST_FEED_ERROR

// Paginated Stores keep their data like this
[7, 10, 5, ...]

索引列表存储类似于列表存储,但它们定义了一对多关系。例如,“用户的订阅者”,“存储库的观星者”,“用户的存储库”。它们还处理分页。

它们通常也只对几个动作做出反应(例如,,,)。REQUEST_USER_REPOSREQUEST_USER_REPOS_SUCCESSREQUEST_USER_REPOS_ERROR

在大多数社交应用中,你会有很多这样的应用,并且你希望能够快速创建另一个。

// Indexed Paginated Stores keep their data like this
{
  2: [7, 10, 5, ...],
  6: [7, 1, 2, ...],
  ...
}

注意:这些不是实际的类或其他东西;这只是我喜欢如何看待商店。不过我做了一些帮手。

StoreUtils

createStore

此方法为您提供最基本的应用商店:

createStore(spec) {
  var store = merge(EventEmitter.prototype, merge(spec, {
    emitChange() {
      this.emit(CHANGE_EVENT);
    },

    addChangeListener(callback) {
      this.on(CHANGE_EVENT, callback);
    },

    removeChangeListener(callback) {
      this.removeListener(CHANGE_EVENT, callback);
    }
  }));

  _.each(store, function (val, key) {
    if (_.isFunction(val)) {
      store[key] = store[key].bind(store);
    }
  });

  store.setMaxListeners(0);
  return store;
}

我用它来创建所有商店。

isInBag,mergeIntoBag

对内容存储有用的小帮手。

isInBag(bag, id, fields) {
  var item = bag[id];
  if (!bag[id]) {
    return false;
  }

  if (fields) {
    return fields.every(field => item.hasOwnProperty(field));
  } else {
    return true;
  }
},

mergeIntoBag(bag, entities, transform) {
  if (!transform) {
    transform = (x) => x;
  }

  for (var key in entities) {
    if (!entities.hasOwnProperty(key)) {
      continue;
    }

    if (!bag.hasOwnProperty(key)) {
      bag[key] = transform(entities[key]);
    } else if (!shallowEqual(bag[key], entities[key])) {
      bag[key] = transform(merge(bag[key], entities[key]));
    }
  }
}

PaginatedList

存储分页状态并强制执行某些断言(在提取时无法获取页面等)。

class PaginatedList {
  constructor(ids) {
    this._ids = ids || [];
    this._pageCount = 0;
    this._nextPageUrl = null;
    this._isExpectingPage = false;
  }

  getIds() {
    return this._ids;
  }

  getPageCount() {
    return this._pageCount;
  }

  isExpectingPage() {
    return this._isExpectingPage;
  }

  getNextPageUrl() {
    return this._nextPageUrl;
  }

  isLastPage() {
    return this.getNextPageUrl() === null && this.getPageCount() > 0;
  }

  prepend(id) {
    this._ids = _.union([id], this._ids);
  }

  remove(id) {
    this._ids = _.without(this._ids, id);
  }

  expectPage() {
    invariant(!this._isExpectingPage, 'Cannot call expectPage twice without prior cancelPage or receivePage call.');
    this._isExpectingPage = true;
  }

  cancelPage() {
    invariant(this._isExpectingPage, 'Cannot call cancelPage without prior expectPage call.');
    this._isExpectingPage = false;
  }

  receivePage(newIds, nextPageUrl) {
    invariant(this._isExpectingPage, 'Cannot call receivePage without prior expectPage call.');

    if (newIds.length) {
      this._ids = _.union(this._ids, newIds);
    }

    this._isExpectingPage = false;
    this._nextPageUrl = nextPageUrl || null;
    this._pageCount++;
  }
}

PaginatedStoreUtils

createListStore, ,createIndexedListStorecreateListActionHandler

通过提供样板方法和操作处理,使索引列表存储的创建尽可能简单:

var PROXIED_PAGINATED_LIST_METHODS = [
  'getIds', 'getPageCount', 'getNextPageUrl',
  'isExpectingPage', 'isLastPage'
];

function createListStoreSpec({ getList, callListMethod }) {
  var spec = {
    getList: getList
  };

  PROXIED_PAGINATED_LIST_METHODS.forEach(method => {
    spec[method] = function (...args) {
      return callListMethod(method, args);
    };
  });

  return spec;
}

/**
 * Creates a simple paginated store that represents a global list (e.g. feed).
 */
function createListStore(spec) {
  var list = new PaginatedList();

  function getList() {
    return list;
  }

  function callListMethod(method, args) {
    return list[method].call(list, args);
  }

  return createStore(
    merge(spec, createListStoreSpec({
      getList: getList,
      callListMethod: callListMethod
    }))
  );
}

/**
 * Creates an indexed paginated store that represents a one-many relationship
 * (e.g. user's posts). Expects foreign key ID to be passed as first parameter
 * to store methods.
 */
function createIndexedListStore(spec) {
  var lists = {};

  function getList(id) {
    if (!lists[id]) {
      lists[id] = new PaginatedList();
    }

    return lists[id];
  }

  function callListMethod(method, args) {
    var id = args.shift();
    if (typeof id ===  'undefined') {
      throw new Error('Indexed pagination store methods expect ID as first parameter.');
    }

    var list = getList(id);
    return list[method].call(list, args);
  }

  return createStore(
    merge(spec, createListStoreSpec({
      getList: getList,
      callListMethod: callListMethod
    }))
  );
}

/**
 * Creates a handler that responds to list store pagination actions.
 */
function createListActionHandler(actions) {
  var {
    request: requestAction,
    error: errorAction,
    success: successAction,
    preload: preloadAction
  } = actions;

  invariant(requestAction, 'Pass a valid request action.');
  invariant(errorAction, 'Pass a valid error action.');
  invariant(successAction, 'Pass a valid success action.');

  return function (action, list, emitChange) {
    switch (action.type) {
    case requestAction:
      list.expectPage();
      emitChange();
      break;

    case errorAction:
      list.cancelPage();
      emitChange();
      break;

    case successAction:
      list.receivePage(
        action.response.result,
        action.response.nextPageUrl
      );
      emitChange();
      break;
    }
  };
}

var PaginatedStoreUtils = {
  createListStore: createListStore,
  createIndexedListStore: createIndexedListStore,
  createListActionHandler: createListActionHandler
};

createStoreMixin

一种 mixin,允许组件调谐到他们感兴趣的商店,例如 .mixins: [createStoreMixin(UserStore)]

function createStoreMixin(...stores) {
  var StoreMixin = {
    getInitialState() {
      return this.getStateFromStores(this.props);
    },

    componentDidMount() {
      stores.forEach(store =>
        store.addChangeListener(this.handleStoresChanged)
      );

      this.setState(this.getStateFromStores(this.props));
    },

    componentWillUnmount() {
      stores.forEach(store =>
        store.removeChangeListener(this.handleStoresChanged)
      );
    },

    handleStoresChanged() {
      if (this.isMounted()) {
        this.setState(this.getStateFromStores(this.props));
      }
    }
  };

  return StoreMixin;
}