[英]In Flux architecture, how do you manage Store lifecycle?
我正在阅读关于Flux但是示例Todo应用程序太简单了,我无法理解一些关键点。
想象一下像Facebook这样的单页应用程序,它具有用户个人资料页面 。 在每个用户个人资料页面上,我们希望显示一些用户信息及其最后的帖子,并带有无限滚动。 我们可以从一个用户档案导航到另一个用户档案。
在Flux架构中,这将如何与商店和调度员相对应?
我们会为每个用户使用一个PostStore
,还是会有某种全局商店? 那么调度员呢,我们会为每个“用户页面”创建一个新的Dispatcher,还是我们会使用单例? 最后,该架构的哪个部分负责管理“特定于页面”的商店的生命周期以响应路由变化?
此外,单个伪页面可以具有多个相同类型的数据列表。 例如,个人资料页上,我想同时显示关注者和跟随 。 单例UserStore
如何在这种情况下工作? UserPageStore
管理UserPageStore
followedBy: UserStore
并follows: UserStore
?
在Flux应用程序中,应该只有一个Dispatcher。 所有数据都流经该中心枢纽。 拥有单例Dispatcher允许它管理所有商店。 当您需要Store#1更新时,这变得很重要,然后Store#2会根据Action#1和Store#1的状态自行更新。 Flux假设这种情况在大型应用程序中是可能发生的。 理想情况下,这种情况不需要发生,开发人员应尽可能避免这种复杂性。 但单身人士Dispatcher已准备好在时机成熟时处理它。
商店也是单身人士。 它们应该保持独立并尽可能分离 - 一个可以从Controller-View查询的自包含Universe。 进入商店的唯一途径是通过它向Dispatcher注册的回调。 唯一的出路是通过吸气功能。 存储还会在状态发生变化时发布事件,因此Controller-Views可以使用getter知道何时查询新状态。
在您的示例应用程序中,将有一个PostStore
。 同一个商店可以管理“页面”(伪页面)上的帖子,这更像是FB的新闻源,其中帖子来自不同的用户。 它的逻辑域是帖子列表,它可以处理任何帖子列表。 当我们从伪页面移动到伪页面时,我们想要重新初始化商店的状态以反映新的状态。 我们可能还想将localStorage中的先前状态缓存为在伪页面之间来回移动的优化,但我倾向于设置等待所有其他商店的PageStore
,管理与所有商店的localStorage的关系在伪页面上,然后更新自己的状态。 请注意,此PageStore
不会存储有关帖子的内容 - 这是PostStore
的域名。 它只是知道特定的伪页面是否已被缓存,因为伪页面是它的域。
PostStore
将有一个initialize()
方法。 此方法将始终清除旧状态,即使这是第一次初始化,然后通过Dispatcher基于通过Action接收的数据创建状态。 从一个伪页面移动到另一个伪页面可能涉及PAGE_UPDATE
操作,这将触发initialize()
的调用。 有一些细节可以解决从本地缓存中检索数据,从服务器检索数据,乐观渲染和XHR错误状态,但这是一般的想法。
如果特定的伪页面不需要应用程序中的所有存储,我不完全确定有任何理由来销毁未使用的存储器限制。 但商店通常不会消耗大量内存。 您只需要确保删除正在销毁的Controller-Views中的事件侦听器。 这是在React的componentWillUnmount()
方法中完成的。
(注意:我使用了JSX Harmony选项使用了ES6语法。)
作为练习,我编写了一个示例Flux应用程序 ,允许浏览Github users
和repos。
它基于fisherwebdev的答案,但也反映了我用于规范API响应的方法。
我记录了一些我在学习Flux时尝试过的方法。
我试图让它接近现实世界(分页,没有虚假的localStorage API)。
这里有几点我特别感兴趣:
switch
; 我试图避免在其他Flux示例中看到的一些重复,特别是在商店中。 我发现从逻辑上将商店划分为三类很有用:
内容商店包含所有应用实体。 拥有ID的所有内容都需要自己的内容存储库。 呈现单个项目的组件向内容存储区请求新数据。
内容存储从所有服务器操作中收集其对象。 例如, UserStore
查看action.response.entities.users
如果它存在, 无论触发了哪个操作。 不需要switch
。 Normalizr可以轻松地将任何API响应压缩为此格式。
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
列表商店会跟踪某些全局列表中显示的实体的ID(例如“Feed”,“您的通知”)。 在这个项目中,我没有这样的商店,但我想我还是会提到它们。 他们处理分页。
它们通常只响应几个动作(例如REQUEST_FEED
, REQUEST_FEED_SUCCESS
, REQUEST_FEED_ERROR
)。
// Paginated Stores keep their data like this
[7, 10, 5, ...]
索引列表存储类似于列表存储,但它们定义了一对多关系。 例如,“用户的订阅者”,“存储库的观星者”,“用户的存储库”。 他们也处理分页。
它们通常也只响应几个动作(例如REQUEST_USER_REPOS
, REQUEST_USER_REPOS_SUCCESS
, REQUEST_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
, createIndexedListStore
, createListActionHandler
通过提供样板方法和操作处理,使索引列表存储的创建尽可能简单:
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)]
他们感兴趣的商店,例如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;
}
因此,在Reflux中,Dispatcher的概念被删除,您只需要考虑通过操作和存储的数据流。 即
Actions <-- Store { <-- Another Store } <-- Components
此处的每个箭头都模拟了如何监听数据流,这反过来意味着数据以相反的方向流动。 数据流的实际数字是这样的:
Actions --> Stores --> Components
^ | |
+----------+------------+
在您的用例中,如果我理解正确,我们需要一个openUserProfile
操作来启动用户配置文件加载和切换页面,还有一些帖子加载操作,这些操作将在打开用户配置文件页面和无限滚动事件期间加载帖子。 所以我想我们在应用程序中有以下数据存储:
在Reflux中你可以这样设置:
// 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');
}
});
我假设您有整个页面视图,用户个人资料页面和帖子列表的组件。 需要连接以下内容:
Action.openUserProfile
。 currentPageStore
以便它知道要切换到哪个页面。 currentUserProfileStore
以便它知道要显示的用户配置文件数据 currentPostsStore
以接收加载的帖子 Action.loadMorePosts
。 这应该是相当的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.