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

我正在阅读关于 变化的文章,但是 例子 Todo 应用程序对我来说太简单了,以至于我无法理解一些关键点。

想象一下,像 Facebook 这样的单页应用程序拥有 用户配置文件页。在每个用户配置文件页面,我们希望显示一些用户信息和他们的最后一篇文章,无限滚动。我们可以从一个用户档案导航到另一个。

在 Flux 体系结构中,这将如何对应于 Stores 和 Dispatchers?

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

而且,一个伪页面可能有几个相同类型的数据列表。例如,在配置文件页面上,我想同时显示 追随者接下来。在这种情况下,单例 UserStore如何工作?UserPageStore会管理 followedBy: UserStorefollows: UserStore吗?

36551 次浏览

在 Flux 应用程序中,应该只有一个 Dispatcher。所有数据都流经这个中心枢纽。使用单一的 Dispatcher 允许它管理所有的 Stores。当您需要 Store # 1更新本身,然后让 Store # 2根据 Action 和 Store # 1的状态更新本身时,这一点就变得非常重要。Flux 假定这种情况在大型应用程序中是不可避免的。理想情况下,这种情况不需要发生,如果可能的话,开发人员应该努力避免这种复杂性。但是单例调度器已经准备好在时机成熟时处理这个问题。

商店也是单身的。它们应该尽可能保持独立性和解耦性——一个可以从 Controller-View 查询的自包含的宇宙。进入商店的唯一途径是通过它在 Dispatcher 中注册的回调。唯一的出路是通过 getter 函数。存储还在其状态更改时发布事件,这样 Controller-View 就可以知道何时使用 getter 查询新的状态。

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

PostStore将有一个 initialize()方法。此方法将始终清除旧状态,即使这是第一次初始化,然后根据它通过 Action (通过 Dispatcher)接收的数据创建状态。从一个伪页面移动到另一个伪页面可能涉及 PAGE_UPDATE操作,这将触发 initialize()的调用。在从本地缓存检索数据、从服务器检索数据、乐观呈现和 XHR 错误状态等方面还有一些细节需要解决,但这是大致思路。

如果一个特定的伪页面不需要应用程序中的所有存储,那么除了内存限制之外,我不能完全确定是否有任何理由销毁未使用的存储。但是商店通常不会消耗大量的内存。您只需要确保删除您正在销毁的 Controller-View 中的事件侦听器。这是在 React 的 componentWillUnmount()方法中完成的。

因此,在 反流中,Dispatcher 的概念被移除了,您只需要考虑通过操作和存储的数据流。也就是说。

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

这里的每个箭头都模拟如何监听数据流,这反过来又意味着数据流向相反的方向。数据流的实际数字如下:

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

在您的用例中,如果我理解正确的话,我们需要一个 openUserProfile操作来启动用户配置文件加载和切换页面,还需要一些帖子加载操作来在用户配置文件页面打开和无限滚动事件期间加载帖子。因此,我假设我们在应用程序中有以下数据存储:

  • 处理切换页的页数据存储区
  • 用户配置文件数据存储区,在打开页时加载用户配置文件
  • 加载和处理可见文章的文章列表数据存储区

在返流中,你可以这样设置:

行动

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

网页商店

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

用户配置文件存储

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

邮局商店

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

组件

我假设您有一个用于整个页面视图、用户配置文件页面和帖子列表的组件。以下内容需要连接起来:

  • 打开用户配置文件的按钮需要在其单击事件期间使用正确的 id 调用 Action.openUserProfile
  • 页面组件应该监听 currentPageStore,这样它就知道要切换到哪个页面。
  • 用户配置文件页面组件需要监听 currentUserProfileStore,以便知道要显示哪些用户配置文件数据
  • 岗位列表需要监听 currentPostsStore来接收加载的岗位
  • 无限滚动事件需要调用 Action.loadMorePosts

差不多就是这样了。

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

作为一个练习,我写了一个 一个 href = “ https://github.com/gaearon/Flux-response-router-example”rel = “ nofollow noReferrer”> sample Flux app ,允许浏览 Github users和回购协议。
它基于 Fisher webdev 的回答,但也反映了我用于规范化 API 响应的一种方法。

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

这里有一些我特别感兴趣的地方:

我如何分类商店

我试图避免我在其他 Flux 示例中看到的一些重复,特别是在 Stores 中。 我发现从逻辑上把商店分成三类很有用:

Content Stores 保存所有应用程序实体。所有具有 ID 的内容都需要自己的内容存储。呈现单个项的组件请求 ContentStores 提供新数据。

内容存储从 所有服务器操作中获取对象。例如,如果 UserStore 查看 action.response.entities.users存在 无论如何,则触发其中的操作。不需要 switch正常化可以很容易地将任何 API 响应压平到这种格式。

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

List Stores 跟踪出现在某个全局列表中的实体的 ID (例如“ feed”、“ your  通知”)。在这个项目中,我没有这样的商店,但我认为我会提到他们无论如何。它们处理分页。

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

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

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

它们通常也只对几个动作作出反应(例如 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, ...],
...
}

注意: 这些不是实际的类或其他东西; 这只是我对 Stores 的看法。 不过我找了几个帮手。

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

我使用它来创建所有商店。

isInBagmergeIntoBag

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

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

createListStorecreateIndexedListStorecreateListActionHandler

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

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

允许组件调优到它们感兴趣的 Stores 的 mix in,例如 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;
}