简体   繁体   English

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

[英]In Flux architecture, how do you manage Store lifecycle?

I'm reading about Flux but the example Todo app is too simplistic for me to understand some key points. 我正在阅读关于Flux但是示例Todo应用程序太简单了,我无法理解一些关键点。

Imagine a single-page app like Facebook that has user profile pages . 想象一下像Facebook这样的单页应用程序,它具有用户个人资料页面 On each user profile page, we want to show some user info and their last posts, with infinite scroll. 在每个用户个人资料页面上,我们希望显示一些用户信息及其最后的帖子,并带有无限滚动。 We can navigate from one user profile to another one. 我们可以从一个用户档案导航到另一个用户档案。

In Flux architecture, how would this correspond to Stores and Dispatchers? 在Flux架构中,这将如何与商店和调度员相对应?

Would we use one PostStore per user, or would we have some kind of a global store? 我们会为每个用户使用一个PostStore ,还是会有某种全局商店? What about dispatchers, would we create a new Dispatcher for each “user page”, or would we use a singleton? 那么调度员呢,我们会为每个“用户页面”创建一个新的Dispatcher,还是我们会使用单例? Finally, what part of the architecture is responsible for managing the lifecycle of “page-specific” Stores in response to route change? 最后,该架构的哪个部分负责管理“特定于页面”的商店的生命周期以响应路由变化?

Moreover, a single pseudo-page may have several lists of data of the same type. 此外,单个伪页面可以具有多个相同类型的数据列表。 For example, on a profile page, I want to show both Followers and Follows . 例如,个人资料页上,我想同时显示关注者跟随 How can a singleton UserStore work in this case? 单例UserStore如何在这种情况下工作? Would UserPageStore manage followedBy: UserStore and follows: UserStore ? UserPageStore管理UserPageStore followedBy: UserStorefollows: UserStore

In a Flux app there should only be one Dispatcher. 在Flux应用程序中,应该只有一个Dispatcher。 All data flows through this central hub. 所有数据都流经该中心枢纽。 Having a singleton Dispatcher allows it to manage all Stores. 拥有单例Dispatcher允许它管理所有商店。 This becomes important when you need Store #1 update itself, and then have Store #2 update itself based on both the Action and on the state of Store #1. 当您需要Store#1更新时,这变得很重要,然后Store#2会根据Action#1和Store#1的状态自行更新。 Flux assumes this situation is an eventuality in a large application. Flux假设这种情况在大型应用程序中是可能发生的。 Ideally this situation would not need to happen, and developers should strive to avoid this complexity, if possible. 理想情况下,这种情况不需要发生,开发人员应尽可能避免这种复杂性。 But the singleton Dispatcher is ready to handle it when the time comes. 但单身人士Dispatcher已准备好在时机成熟时处理它。

Stores are singletons as well. 商店也是单身人士。 They should remain as independent and decoupled as possible -- a self-contained universe that one can query from a Controller-View. 它们应该保持独立并尽可能分离 - 一个可以从Controller-View查询的自包含Universe。 The only road into the Store is through the callback it registers with the Dispatcher. 进入商店的唯一途径是通过它向Dispatcher注册的回调。 The only road out is through getter functions. 唯一的出路是通过吸气功能。 Stores also publish an event when their state has changed, so Controller-Views can know when to query for the new state, using the getters. 存储还会在状态发生变化时发布事件,因此Controller-Views可以使用getter知道何时查询新状态。

In your example app, there would be a single PostStore . 在您的示例应用程序中,将有一个PostStore This same store could manage the posts on a "page" (pseudo-page) that is more like FB's Newsfeed, where posts appear from different users. 同一个商店可以管理“页面”(伪页面)上的帖子,这更像是FB的新闻源,其中帖子来自不同的用户。 Its logical domain is the list of posts, and it can handle any list of posts. 它的逻辑域是帖子列表,它可以处理任何帖子列表。 When we move from pseudo-page to pseudo-page, we want to reinitialize the state of the store to reflect the new state. 当我们从伪页面移动到伪页面时,我们想要重新初始化商店的状态以反映新的状态。 We might also want to cache the previous state in localStorage as an optimization for moving back and forth between pseudo-pages, but my inclination would be to set up a PageStore that waits for all other stores, manages the relationship with localStorage for all the stores on the pseudo-page, and then updates its own state. 我们可能还想将localStorage中的先前状态缓存为在伪页面之间来回移动的优化,但我倾向于设置等待所有其他商店的PageStore ,管理与所有商店的localStorage的关系在伪页面上,然后更新自己的状态。 Note that this PageStore would store nothing about the posts -- that's the domain of the PostStore . 请注意,此PageStore不会存储有关帖子的内容 - 这是PostStore的域名。 It would simply know whether a particular pseudo-page has been cached or not, because pseudo-pages are its domain. 它只是知道特定的伪页面是否已被缓存,因为伪页面是它的域。

The PostStore would have an initialize() method. PostStore将有一个initialize()方法。 This method would always clear the old state, even if this is the first initialization, and then create the state based on the data it received through the Action, via the Dispatcher. 此方法将始终清除旧状态,即使这是第一次初始化,然后通过Dispatcher基于通过Action接收的数据创建状态。 Moving from one pseudo-page to another would probably involve a PAGE_UPDATE action, which would trigger the invocation of initialize() . 从一个伪页面移动到另一个伪页面可能涉及PAGE_UPDATE操作,这将触发initialize()的调用。 There are details to work out around retrieving data from the local cache, retrieving data from the server, optimistic rendering and XHR error states, but this is the general idea. 有一些细节可以解决从本地缓存中检索数据,从服务器检索数据,乐观渲染和XHR错误状态,但这是一般的想法。

If a particular pseudo-page does not need all the Stores in the application, I'm not entirely sure there is any reason to destroy the unused ones, other than memory constraints. 如果特定的伪页面不需要应用程序中的所有存储,我不完全确定有任何理由来销毁未使用的存储器限制。 But stores don't typically consume a great deal of memory. 但商店通常不会消耗大量内存。 You just need to make sure to remove the event listeners in the Controller-Views you are destroying. 您只需要确保删除正在销毁的Controller-Views中的事件侦听器。 This is done in React's componentWillUnmount() method. 这是在React的componentWillUnmount()方法中完成的。

(Note: I have used ES6 syntax using JSX Harmony option.) (注意:我使用了JSX Harmony选项使用了ES6语法。)

As an exercise, I wrote a sample Flux app that allows to browse Github users and repos. 作为练习,我编写了一个示例Flux应用程序 ,允许浏览Github users和repos。
It is based on fisherwebdev's answer but also reflects an approach I use for normalizing API responses. 它基于fisherwebdev的答案,但也反映了我用于规范API响应的方法。

I made it to document a few approaches I have tried while learning Flux. 我记录了一些我在学习Flux时尝试过的方法。
I tried to keep it close to real world (pagination, no fake localStorage APIs). 我试图让它接近现实世界(分页,没有虚假的localStorage API)。

There are a few bits here I was especially interested in: 这里有几点我特别感兴趣:

How I Classify Stores 我如何分类商店

I tried to avoid some of the duplication I've seen in other Flux example, specifically in Stores. 我试图避免在其他Flux示例中看到的一些重复,特别是在商店中。 I found it useful to logically divide Stores into three categories: 我发现从逻辑上将商店划分为三类很有用:

Content Stores hold all app entities. 内容商店包含所有应用实体。 Everything that has an ID needs its own Content Store. 拥有ID的所有内容都需要自己的内容存储库。 Components that render individual items ask Content Stores for the fresh data. 呈现单个项目的组件向内容存储区请求新数据。

Content Stores harvest their objects from all server actions. 内容存储从所有服务器操作中收集其对象。 For example, UserStore looks into action.response.entities.users if it exists regardless of which action fired. 例如, UserStore 查看action.response.entities.users如果它存在, 无论触发了哪个操作。 There is no need for a switch . 不需要switch Normalizr makes it easy to flatten any API reponses to this format. Normalizr可以轻松地将任何API响应压缩为此格式。

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

List Stores keep track of IDs of entities that appear in some global list (eg “feed”, “your notifications”). 列表商店会跟踪某些全局列表中显示的实体的ID(例如“Feed”,“您的通知”)。 In this project, I don't have such Stores, but I thought I'd mention them anyway. 在这个项目中,我没有这样的商店,但我想我还是会提到它们。 They handle pagination. 他们处理分页。

They normally respond to just a few actions (eg REQUEST_FEED , REQUEST_FEED_SUCCESS , REQUEST_FEED_ERROR ). 它们通常只响应几个动作(例如REQUEST_FEEDREQUEST_FEED_SUCCESSREQUEST_FEED_ERROR )。

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

Indexed List Stores are like List Stores but they define one-to-many relationship. 索引列表存储类似于列表存储,但它们定义了一对多关系。 For example, “user's subscribers”, “repository's stargazers”, “user's repositories”. 例如,“用户的订阅者”,“存储库的观星者”,“用户的存储库”。 They also handle pagination. 他们也处理分页。

They also normally respond to just a few actions (eg REQUEST_USER_REPOS , REQUEST_USER_REPOS_SUCCESS , REQUEST_USER_REPOS_ERROR ). 它们通常也只响应几个动作(例如REQUEST_USER_REPOSREQUEST_USER_REPOS_SUCCESSREQUEST_USER_REPOS_ERROR )。

In most social apps, you'll have lots of these and you want to be able to quickly create one more of them. 在大多数社交应用程序中,您将拥有大量这些应用程序,并希望能够快速创建其中一个。

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

Note: these are not actual classes or something; 注意:这些不是实际的类或其他东西; it's just how I like to think about Stores. 这就是我喜欢考虑商店的方式。 I made a few helpers though. 我做了几个助手。

StoreUtils

createStore

This method gives you the most basic Store: 此方法为您提供最基本的商店:

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;
}

I use it to create all Stores. 我用它来创建所有商店。

isInBag , mergeIntoBag isInBagmergeIntoBag

Small helpers useful for Content Stores. 对内容商店有用的小助手。

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

Stores pagination state and enforces certain assertions (can't fetch page while fetching, etc). 存储分页状态并强制执行某些断言(在获取时无法获取页面等)。

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 , createIndexedListStore , createListActionHandler createListStorecreateIndexedListStorecreateListActionHandler

Makes creation of Indexed List Stores as simple as possible by providing boilerplate methods and action handling: 通过提供样板方法和操作处理,使索引列表存储的创建尽可能简单:

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

A mixin that allows components to tune in to Stores they're interested in, eg mixins: [createStoreMixin(UserStore)] . mixin,允许组件mixins: [createStoreMixin(UserStore)]他们感兴趣的商店,例如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;
}

So in Reflux the concept of the Dispatcher is removed and you only need to think in terms of data flow through actions and stores. 因此,在Reflux中,Dispatcher的概念被删除,您只需要考虑通过操作和存储的数据流。 Ie

Actions <-- Store { <-- Another Store } <-- Components

Each arrow here models how the data flow is listened to, which in turn means that the data flows in the opposite direction. 此处的每个箭头都模拟了如何监听数据流,这反过来意味着数据以相反的方向流动。 The actual figure for data flow is this: 数据流的实际数字是这样的:

Actions --> Stores --> Components
   ^          |            |
   +----------+------------+

In your use case, if I understood correctly, we need a openUserProfile action that initiates the user profile loading and switching the page and also some posts loading actions that will load posts when the user profile page is opened and during the infinite scroll event. 在您的用例中,如果我理解正确,我们需要一个openUserProfile操作来启动用户配置文件加载和切换页面,还有一些帖子加载操作,这些操作将在打开用户配置文件页面和无限滚动事件期间加载帖子。 So I'd imagine we have the following data stores in the application: 所以我想我们在应用程序中有以下数据存储:

  • A page data store that handles switching pages 处理切换页面的页面数据存储
  • A user profile data store that loads the user profile when the page is opened 用户配置文件数据存储,用于在打开页面时加载用户配置文件
  • A posts list data store that loads and handles the visible posts 用于加载和处理可见帖子的帖子列表数据存储

In Reflux you'd set it up like this: 在Reflux中你可以这样设置:

The actions 行动

// Set up the two actions we need for this use case.
var Actions = Reflux.createActions(['openUserProfile', 'loadUserProfile', 'loadInitialPosts', 'loadMorePosts']);

The page store 页面存储

var currentPageStore = Reflux.createStore({
    init: function() {
        this.listenTo(openUserProfile, this.openUserProfileCallback);
    },
    // We are assuming that the action is invoked with a profileid
    openUserProfileCallback: function(userProfileId) {
        // Trigger to the page handling component to open the user profile
        this.trigger('user profile');

        // Invoke the following action with the loaded the user profile
        Actions.loadUserProfile(userProfileId);
    }
});

The user profile store 用户个人资料商店

var currentUserProfileStore = Reflux.createStore({
    init: function() {
        this.listenTo(Actions.loadUserProfile, this.switchToUser);
    },
    switchToUser: function(userProfileId) {
        // Do some ajaxy stuff then with the loaded user profile
        // trigger the stores internal change event with it
        this.trigger(userProfile);
    }
});

The posts store 帖子商店

var currentPostsStore = Reflux.createStore({
    init: function() {
        // for initial posts loading by listening to when the 
        // user profile store changes
        this.listenTo(currentUserProfileStore, this.loadInitialPostsFor);
        // for infinite posts loading
        this.listenTo(Actions.loadMorePosts, this.loadMorePosts);
    },
    loadInitialPostsFor: function(userProfile) {
        this.currentUserProfile = userProfile;

        // Do some ajax stuff here to fetch the initial posts then send
        // them through the change event
        this.trigger(postData, 'initial');
    },
    loadMorePosts: function() {
        // Do some ajaxy stuff to fetch more posts then send them through
        // the change event
        this.trigger(postData, 'more');
    }
});

The components 组件

I'm assuming you have a component for the whole page view, the user profile page and the posts list. 我假设您有整个页面视图,用户个人资料页面和帖子列表的组件。 The following needs to be wired up: 需要连接以下内容:

  • The buttons that opens up the user profile need to invoke the Action.openUserProfile with the correct id during it's click event. 打开用户配置文件的按钮需要在其单击事件期间使用正确的id调用Action.openUserProfile
  • The page component should be listening to the currentPageStore so it knows which page to switch to. 页面组件应该监听currentPageStore以便它知道要切换到哪个页面。
  • The user profile page component needs to listen to the currentUserProfileStore so it knows what user profile data to show 用户配置文件页面组件需要侦听currentUserProfileStore以便它知道要显示的用户配置文件数据
  • The posts list needs to listen to the currentPostsStore to receive the loaded posts 帖子列表需要监听currentPostsStore以接收加载的帖子
  • The infinite scroll event needs to call the Action.loadMorePosts . 无限滚动事件需要调用Action.loadMorePosts

And that should be pretty much it. 这应该是相当的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM