在Flux架构中,您如何pipe理商店生命周期?
我在阅读Flux,但Todo应用程序的例子太简单了,以至于我不能理解一些关键点。
想像一下像Facebook这样的单页面应用程序,它具有用户个人资料页面 。 在每个用户个人资料页面上,我们要显示一些用户信息和他们最后的post,无限滚动。 我们可以从一个用户configuration文件导航到另一个。
在Flux体系结构中,这与商店和调度程序如何对应?
我们会为每个用户使用一个PostStore
,还是我们会有某种全球性的商店? 调度员怎么样,我们会为每个“用户页面”创build一个新的Dispatcher,还是我们会使用一个单独的? 最后,体系结构的哪一部分负责pipe理“特定于页面”商店的生命周期以响应路由变化?
而且,一个伪页面可能有几个相同types的数据列表。 例如,在个人资料页面上,我想显示“ 追随者”和“ 追随者” 。 单身UserStore
如何在这种情况下工作? UserPageStore
pipe理UserPageStore
followedBy: UserStore
和follows: UserStore
?
在Flux应用程序中应该只有一个Dispatcher。 所有的数据stream经这个中心枢纽。 拥有一个单独的分派器允许它pipe理所有的商店。 当您需要Store#1更新本身,然后根据Store#1的Action和Store状态对Store#2进行更新时,这变得非常重要。 Flux认为这种情况在大型应用中是可能发生的。 理想情况下,这种情况不需要发生,如果可能的话,开发者应该努力避免这种复杂性。 但是单身调度员随时准备处理它。
商店也是单身人士。 他们应该保持独立和解耦 – 一个自足的宇宙,可以从一个控制器查看查询。 进入商店的唯一途径是通过它向分派器注册的callback。 唯一的出路是通过getter函数。 商店也会在状态发生变化时发布一个事件,所以Controller-Views可以知道何时使用getters来查询新状态。
在您的示例应用程序中,将会有一个PostStore
。 同一家商店可以在“页面”(伪页面)上pipe理post,这更像是FB的新闻源,其中post来自不同的用户。 它的逻辑域是post列表,它可以处理任何post列表。 当我们从伪页面转到伪页面时,我们希望重新初始化商店的状态以反映新的状态。 我们可能还想将localStorage中的前一个状态caching为在伪页之间来回移动的优化,但是我倾向于设置一个等待所有其他存储的PageStore
,pipe理与所有存储的localStorage的关系在伪页面上,然后更新自己的状态。 请注意,这个PageStore
将不会存储任何有关post的信息 – 这就是PostStore
的域名。 它只是知道一个特定的伪页面是否被caching,因为伪页面是它的域名。
PostStore
将有一个initialize()
方法。 即使这是第一次初始化,该方法总是会清除旧状态,然后通过Dispatcher根据通过Action接收到的数据创build状态。 从一个伪页面移动到另一个可能会涉及一个PAGE_UPDATE
操作,这会触发initialize()
的调用。 有关于从本地caching中检索数据,从服务器检索数据,乐观渲染和XHR错误状态的细节,但这是一般的想法。
如果一个特定的伪页面不需要应用程序中的所有存储,我不完全确定除了内存限制之外,还有任何理由去销毁这些未使用的页面。 但是商店通常不会消耗大量的内存。 你只需要确保删除你正在销毁的Controller-Views中的事件监听器。 这是在React的componentWillUnmount()
方法中完成的。
(注意:我使用JSX Harmony选项使用了ES6语法。)
作为一个练习,我写了一个示例Flux应用程序 ,允许浏览Github users
和回购。
这是基于fisherwebdev的答案,但也反映了我用来规范API响应的方法。
我在文档中logging了一些学习Flux的方法。
我试图保持接近现实世界(分页,没有假的本地存储API)。
这里有一些我特别感兴趣的地方:
- 它使用Flux架构和反应路由器 ;
- 它可以显示部分已知信息的用户页面,并在旅途中加载详细信息;
- 它支持用户和回购分页;
- 它用normalizrparsingGithub的嵌套JSON响应;
- 内容商店不需要包含具有动作的巨型
switch
; - “返回”是即时的(因为所有数据都在商店中)。
如何分类商店
我试图避免在其他Flux示例中看到的一些重复,特别是在商店中。 我发现从逻辑上将商店分成三类是有用的:
内容商店拥有所有的应用程序实体。 所有具有ID的东西都需要自己的内容存储。 呈现单个项目的组件向内容存储库请求新数据。
内容商店从所有服务器操作中收集对象。 例如, UserStore
查看action.response.entities.users
是否存在, 而不pipe哪个操作被触发。 不需要switch
。 Normalizr可以很容易地将任何API响应变成这种格式。
// Content Stores keep their data like this { 7: { id: 7, name: 'Dan' }, ... }
列表商店跟踪出现在某个全局列表中的实体的ID(例如“feed”,“your notifications”)。 在这个项目中,我没有这样的商店,但我想我会提及它们。 他们处理分页。
他们通常只响应几个动作(例如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
)。
在大多数社交应用程序中,您将拥有大量这些应用程序,并希望能够快速创build更多应用程序。
// 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; }
我用它来创build所有商店。
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
通过提供样板方法和动作处理,创build索引列表存储尽可能简单:
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 (eg 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 * (eg 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的概念被删除了,你只需要通过行为和存储来思考数据stream。 即
Actions <-- Store { <-- Another Store } <-- Components
这里的每个箭头模拟如何收听数据stream,这又意味着数据stream向相反的方向。 数据stream的实际数字是这样的:
Actions --> Stores --> Components ^ | | +----------+------------+
在你的用例中,如果我理解正确,我们需要一个openUserProfile
操作来启动用户configuration文件加载和切换页面,还有一些文章加载操作,当用户configuration文件页面被打开时和在无限滚动事件中加载文章。 所以我想像我们在应用程序中有以下数据存储:
- 处理切换页面的页面数据存储
- 打开页面时加载用户configuration文件的用户configuration文件数据存储
- 加载和处理可见post的post列表数据存储
在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); } });
用户configuration文件存储
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); } });
post存储
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'); } });
组件
我假设你有一个组件的整个页面视图,用户configuration文件页面和post列表。 以下需要连线:
- 打开用户configuration文件的button需要在点击事件期间使用正确的ID调用
Action.openUserProfile
。 - 页面组件应该监听
currentPageStore
以便知道要切换到哪个页面。 - 用户configuration文件页面组件需要监听
currentUserProfileStore
以便知道要显示的用户configuration文件数据 - post列表需要侦听
currentPostsStore
以接收已加载的post - 无限滚动事件需要调用
Action.loadMorePosts
。
这应该是非常多的。