簡體   English   中英

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

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

我正在閱讀關於Flux但是示例Todo應用程序太簡單了,我無法理解一些關鍵點。

想象一下像Facebook這樣的單頁應用程序,它具有用戶個人資料頁面 在每個用戶個人資料頁面上,我們希望顯示一些用戶信息及其最后的帖子,並帶有無限滾動。 我們可以從一個用戶檔案導航到另一個用戶檔案。

在Flux架構中,這將如何與商店和調度員相對應?

我們會為每個用戶使用一個PostStore ,還是會有某種全局商店? 那么調度員呢,我們會為每個“用戶頁面”創建一個新的Dispatcher,還是我們會使用單例? 最后,該架構的哪個部分負責管理“特定於頁面”的商店的生命周期以響應路由變化?

此外,單個偽頁面可以具有多個相同類型的數據列表。 例如,個人資料頁上,我想同時顯示關注者跟隨 單例UserStore如何在這種情況下工作? UserPageStore管理UserPageStore followedBy: UserStorefollows: 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)。

這里有幾點我特別感興趣:

我如何分類商店

我試圖避免在其他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_FEEDREQUEST_FEED_SUCCESSREQUEST_FEED_ERROR )。

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

索引列表存儲類似於列表存儲,但它們定義了一對多關系。 例如,“用戶的訂閱者”,“存儲庫的觀星者”,“用戶的存儲庫”。 他們也處理分頁。

它們通常也只響應幾個動作(例如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, ...],
  ...
}

注意:這些不是實際的類或其他東西; 這就是我喜歡考慮商店的方式。 我做了幾個助手。

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

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

組件

我假設您有整個頁面視圖,用戶個人資料頁面和帖子列表的組件。 需要連接以下內容:

  • 打開用戶配置文件的按鈕需要在其單擊事件期間使用正確的id調用Action.openUserProfile
  • 頁面組件應該監聽currentPageStore以便它知道要切換到哪個頁面。
  • 用戶配置文件頁面組件需要偵聽currentUserProfileStore以便它知道要顯示的用戶配置文件數據
  • 帖子列表需要監聽currentPostsStore以接收加載的帖子
  • 無限滾動事件需要調用Action.loadMorePosts

這應該是相當的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM