[英]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.