Trong kiến ​​trúc Flux, làm thế nào để bạn quản lý vòng đời Store?


132

Tôi đang đọc về Flux nhưng ứng dụng Todo ví dụ quá đơn giản để tôi hiểu một số điểm chính.

Hãy tưởng tượng một ứng dụng một trang như Facebook có trang hồ sơ người dùng . Trên mỗi trang hồ sơ người dùng, chúng tôi muốn hiển thị một số thông tin người dùng và bài đăng cuối cùng của họ, với cuộn vô hạn. Chúng tôi có thể điều hướng từ hồ sơ người dùng này sang hồ sơ khác.

Trong kiến ​​trúc Flux, làm thế nào điều này sẽ tương ứng với Cửa hàng và Công văn?

Chúng tôi sẽ sử dụng một PostStorecho mỗi người dùng, hoặc chúng tôi sẽ có một loại cửa hàng toàn cầu? Thế còn những người điều phối, chúng ta sẽ tạo ra một Người điều phối mới cho mỗi trang người dùng của Cameron, hay chúng ta sẽ sử dụng một đơn? Cuối cùng, phần nào của kiến ​​trúc chịu trách nhiệm quản lý vòng đời của các Cửa hàng trên mạng cụ thể trên trang của Google để đáp ứng với thay đổi tuyến đường?

Hơn nữa, một trang giả đơn có thể có một vài danh sách dữ liệu cùng loại. Ví dụ: trên trang hồ sơ, tôi muốn hiển thị cả Người theo dõiNgười theo dõi . Làm thế nào một singleton có thể UserStorelàm việc trong trường hợp này? Sẽ UserPageStorequản lý followedBy: UserStorefollows: UserStore?

Câu trả lời:


124

Trong một ứng dụng Flux chỉ nên có một Dispatcher. Tất cả dữ liệu chảy qua trung tâm này. Có một Depatcher đơn lẻ cho phép nó quản lý tất cả các Cửa hàng. Điều này trở nên quan trọng khi bạn cần tự cập nhật Cửa hàng số 1 và sau đó tự cập nhật Cửa hàng số 2 dựa trên cả Hành động và trạng thái của Cửa hàng số 1. Flux giả định tình huống này là một sự kiện trong một ứng dụng lớn. Lý tưởng nhất là tình huống này sẽ không cần phải xảy ra và các nhà phát triển nên cố gắng tránh sự phức tạp này, nếu có thể. Nhưng người điều phối đơn lẻ sẵn sàng xử lý nó khi đến lúc.

Cửa hàng là singletons là tốt. Chúng nên tồn tại độc lập và tách rời nhất có thể - một vũ trụ khép kín mà người ta có thể truy vấn từ Chế độ xem Điều khiển. Con đường duy nhất vào Cửa hàng là thông qua cuộc gọi lại mà nó đăng ký với Bộ điều phối. Con đường duy nhất là thông qua các chức năng getter. Các cửa hàng cũng xuất bản một sự kiện khi trạng thái của họ thay đổi, do đó, Bộ điều khiển-Lượt xem có thể biết khi nào cần truy vấn trạng thái mới, bằng cách sử dụng các biểu đồ.

Trong ứng dụng ví dụ của bạn, sẽ có một PostStore. Cửa hàng này có thể quản lý các bài đăng trên một "trang" (trang giả) giống như Newsfeed của FB, nơi các bài đăng xuất hiện từ những người dùng khác nhau. Tên miền logic của nó là danh sách các bài đăng và nó có thể xử lý bất kỳ danh sách bài viết nào. Khi chúng tôi chuyển từ trang giả sang trang giả, chúng tôi muốn xác định lại trạng thái của cửa hàng để phản ánh trạng thái mới. Chúng tôi cũng có thể muốn lưu trữ trạng thái trước đó trong localStorage như một tối ưu hóa để di chuyển qua lại giữa các trang giả, nhưng thiên hướng của tôi sẽ là thiết lập PageStorechờ tất cả các cửa hàng khác, quản lý mối quan hệ với localStorage cho tất cả các cửa hàng trên trang giả, và sau đó cập nhật trạng thái của chính nó. Lưu ý rằng điều này PageStoresẽ không lưu trữ gì về bài viết - đó là tên miền củaPostStore. Nó chỉ đơn giản là biết liệu một trang giả cụ thể đã được lưu trữ hay chưa, bởi vì các trang giả là miền của nó.

Các PostStoresẽ có một initialize()phương pháp. Phương pháp này sẽ luôn xóa trạng thái cũ, ngay cả khi đây là lần khởi tạo đầu tiên, sau đó tạo trạng thái dựa trên dữ liệu mà nó nhận được thông qua Hành động, thông qua Bộ điều phối. Chuyển từ trang giả này sang trang khác có thể sẽ liên quan đến một PAGE_UPDATEhành động, điều này sẽ kích hoạt việc gọi initialize(). Có các chi tiết để tìm ra cách truy xuất dữ liệu từ bộ đệm cục bộ, truy xuất dữ liệu từ máy chủ, hiển thị lạc quan và trạng thái lỗi XHR, nhưng đây là ý tưởng chung.

Nếu một trang giả cụ thể không cần tất cả các Cửa hàng trong ứng dụng, tôi không hoàn toàn chắc chắn có bất kỳ lý do nào để phá hủy những trang không sử dụng, ngoại trừ các hạn chế về bộ nhớ. Nhưng các cửa hàng thường không tiêu thụ nhiều bộ nhớ. Bạn chỉ cần đảm bảo xóa trình lắng nghe sự kiện trong Trình điều khiển-Lượt xem mà bạn đang hủy. Điều này được thực hiện trong componentWillUnmount()phương pháp của React .


5
Chắc chắn có một vài cách tiếp cận khác nhau cho những gì bạn muốn làm và tôi nghĩ nó phụ thuộc vào những gì bạn đang cố gắng xây dựng. Một cách tiếp cận sẽ là UserListStore, với tất cả những người dùng có liên quan. Và mỗi người dùng sẽ có một vài cờ boolean mô tả mối quan hệ với hồ sơ người dùng hiện tại. Một cái gì đó như { follower: true, followed: false }, ví dụ. Các phương thức getFolloweds()getFollowers()sẽ truy xuất các nhóm người dùng khác nhau mà bạn cần cho UI.
fishingwebdev

4
Ngoài ra, bạn có thể có FollowedUserListStore và FollowerUserListStore, cả hai đều kế thừa từ một UserListStore trừu tượng.
fishingwebdev

Tôi có một câu hỏi nhỏ - tại sao không sử dụng pub sub để phát trực tiếp dữ liệu từ các cửa hàng thay vì yêu cầu người đăng ký lấy dữ liệu?
sunwukung

2
@sunwukung Điều này sẽ yêu cầu các cửa hàng theo dõi xem lượt xem của bộ điều khiển cần dữ liệu gì. Sẽ tốt hơn nếu các cửa hàng công bố thực tế rằng họ đã thay đổi theo một cách nào đó, và sau đó để cho các chế độ xem bộ điều khiển quan tâm lấy lại phần nào của dữ liệu họ cần.
fishingwebdev

Điều gì sẽ xảy ra nếu tôi có một trang hồ sơ nơi tôi hiển thị thông tin về người dùng nhưng cũng có một danh sách bạn bè của anh ấy. Cả người dùng và bạn bè sẽ là cùng một loại. Nếu họ ở trong cùng một cửa hàng nếu vậy?
Nick Dima

79

(Lưu ý: Tôi đã sử dụng cú pháp ES6 bằng tùy chọn JSX Harmony.)

Như một bài tập, tôi đã viết một ứng dụng Flux mẫu cho phép duyệt Github usersvà repos.
Nó dựa trên câu trả lời của fishingwebdev nhưng cũng phản ánh cách tiếp cận tôi sử dụng để bình thường hóa các phản hồi API.

Tôi đã làm nó để ghi lại một vài cách tiếp cận mà tôi đã thử khi học Flux.
Tôi đã cố gắng giữ nó gần với thế giới thực (phân trang, không có API localStorage giả).

Có một vài bit ở đây tôi đặc biệt quan tâm:

Cách tôi phân loại cửa hàng

Tôi đã cố gắng tránh một số trùng lặp mà tôi đã thấy trong ví dụ Flux khác, đặc biệt là trong Cửa hàng. Tôi thấy hữu ích khi phân chia hợp lý các Cửa hàng thành ba loại:

Cửa hàng nội dung chứa tất cả các thực thể ứng dụng. Mọi thứ có ID đều cần Content Store riêng. Các thành phần kết xuất các mục riêng lẻ yêu cầu Cửa hàng Nội dung cho dữ liệu mới.

Cửa hàng nội dung thu hoạch các đối tượng của họ từ tất cả các hành động của máy chủ. Ví dụ, UserStore xem xétaction.response.entities.users nếu nó tồn tại bất kể hành động nào được bắn. Không cần a switch. Normalizr giúp dễ dàng làm phẳng bất kỳ API nào trả lại định dạng này.

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

Danh sách Cửa hàng theo dõi ID của các thực thể xuất hiện trong một số danh sách toàn cầu (ví dụ: nguồn cấp dữ liệu ăn chay, thông báo của bạn. Trong dự án này, tôi không có Cửa hàng như vậy, nhưng tôi nghĩ dù sao tôi cũng sẽ đề cập đến chúng. Họ xử lý phân trang.

Họ thường phản ứng chỉ với một vài hành động (ví dụ REQUEST_FEED, REQUEST_FEED_SUCCESS, REQUEST_FEED_ERROR).

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

Cửa hàng danh sách được lập chỉ mục giống như Cửa hàng danh sách nhưng chúng xác định mối quan hệ một-nhiều. Ví dụ: các thuê bao của người dùng trên mạng, các nhà cung cấp của kho lưu trữ của Google, các nhà cung cấp của kho lưu trữ của Google. Họ cũng xử lý phân trang.

Họ cũng thường đáp ứng với chỉ một vài hành động (ví dụ REQUEST_USER_REPOS, REQUEST_USER_REPOS_SUCCESS, REQUEST_USER_REPOS_ERROR).

Trong hầu hết các ứng dụng xã hội, bạn sẽ có rất nhiều ứng dụng này và bạn muốn có thể nhanh chóng tạo thêm một trong số chúng.

// Indexed Paginated Stores keep their data like this
{
  2: [7, 10, 5, ...],
  6: [7, 1, 2, ...],
  ...
}

Lưu ý: đây không phải là các lớp học thực tế hoặc một cái gì đó; đó chỉ là cách tôi muốn nghĩ về Cửa hàng. Tôi đã làm một vài người giúp đỡ mặc dù.

StoreUtils

createStore

Phương pháp này cung cấp cho bạn Cửa hàng cơ bản nhất:

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

Tôi sử dụng nó để tạo tất cả các Cửa hàng.

isInBag, mergeIntoBag

Người trợ giúp nhỏ hữu ích cho Cửa hàng nội dung.

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

Lưu trữ trạng thái phân trang và thực thi các xác nhận nhất định (không thể tìm nạp trang trong khi tìm nạp, v.v.).

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

Làm cho việc tạo các Cửa hàng Danh sách được Lập chỉ mục đơn giản nhất có thể bằng cách cung cấp các phương thức soạn sẵn và xử lý hành động:

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

Một mixin cho phép các thành phần để điều chỉnh trong Cửa hàng họ đang quan tâm, ví dụ 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;
}

1
Với thực tế là bạn đã viết Stampsy, nếu bạn viết lại toàn bộ ứng dụng phía máy khách, bạn có sử dụng FLUX và chính sự chấp thuận mà bạn đã sử dụng để xây dựng ứng dụng ví dụ này không?
eAbi

2
eAbi: Đây là cách tiếp cận chúng tôi hiện đang sử dụng vì chúng tôi đang viết lại Stampsy trong Flux (hy vọng sẽ phát hành vào tháng tới). Nó không lý tưởng nhưng nó hoạt động tốt cho chúng tôi. Khi / nếu chúng tôi tìm ra những cách tốt hơn để làm những thứ đó, chúng tôi sẽ chia sẻ chúng.
Dan Abramov

1
eAbi: Tuy nhiên, chúng tôi không sử dụng normalizr nữa vì một người trong nhóm của chúng tôi đã viết lại tất cả các API của chúng tôi để trả về các phản hồi được chuẩn hóa. Nó rất hữu ích trước khi nó được thực hiện.
Dan Abramov

Cảm ơn thông tin của bạn. Tôi đã kiểm tra repo github của bạn và tôi đang cố gắng bắt đầu một dự án (được xây dựng trong YUI3) với cách tiếp cận của bạn, nhưng tôi gặp một số rắc rối khi biên dịch mã (nếu bạn có thể nói như vậy). Tôi không chạy máy chủ dưới nút nên tôi muốn sao chép nguồn vào thư mục tĩnh của mình nhưng tôi vẫn phải thực hiện một số công việc ... Nó hơi cồng kềnh và tôi cũng tìm thấy một số tệp có cú pháp JS khác nhau. Đặc biệt là trong các tập tin jsx.
eAbi

2
@Sean: Tôi không thấy nó là một vấn đề. Các luồng dữ liệu là về cách viết dữ liệu, không đọc nó. Chắc chắn sẽ tốt nhất nếu các hành động không tin tưởng vào các cửa hàng, nhưng để tối ưu hóa các yêu cầu tôi nghĩ rằng việc đọc từ các cửa hàng là hoàn toàn tốt. Rốt cuộc, các thành phần đọc từ các cửa hàng và bắn những hành động đó. Bạn có thể lặp lại logic này trong mọi thành phần, nhưng đó là những gì người tạo hành động dành cho ..
Dan Abramov

27

Vì vậy, trong Reflux , khái niệm về Bộ điều phối bị loại bỏ và bạn chỉ cần suy nghĩ về luồng dữ liệu thông qua các hành động và lưu trữ. I E

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

Mỗi mũi tên ở đây mô hình cách nghe luồng dữ liệu, điều này có nghĩa là dữ liệu chảy theo hướng ngược lại. Con số thực tế cho luồng dữ liệu là:

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

Trong trường hợp sử dụng của bạn, nếu tôi hiểu chính xác, chúng tôi cần một openUserProfilehành động khởi tạo tải hồ sơ người dùng và chuyển trang và một số bài đăng tải hành động sẽ tải bài đăng khi trang hồ sơ người dùng được mở và trong sự kiện cuộn vô hạn. Vì vậy, tôi tưởng tượng chúng ta có các kho dữ liệu sau trong ứng dụng:

  • Kho lưu trữ dữ liệu trang xử lý các trang chuyển đổi
  • Lưu trữ dữ liệu hồ sơ người dùng tải hồ sơ người dùng khi trang được mở
  • Một bài viết liệt kê lưu trữ dữ liệu tải và xử lý các bài đăng hiển thị

Trong Reflux bạn sẽ thiết lập nó như thế này:

Các hành động

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

Cửa hàng trang

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

Cửa hàng hồ sơ người dùng

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

Cửa hàng bài viết

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

Các thành phần

Tôi giả sử bạn có một thành phần cho toàn bộ lượt xem trang, trang hồ sơ người dùng và danh sách bài viết. Những điều sau đây cần được nối dây:

  • Các nút mở hồ sơ người dùng cần phải gọi Action.openUserProfile với id chính xác trong sự kiện nhấp chuột.
  • Thành phần trang nên lắng nghe currentPageStore nó biết trang nào sẽ chuyển sang.
  • Thành phần trang hồ sơ người dùng cần lắng nghe currentUserProfileStore nó biết dữ liệu hồ sơ người dùng nào sẽ hiển thị
  • Danh sách bài viết cần lắng nghe currentPostsStore để nhận các bài đăng được tải
  • Sự kiện cuộn vô hạn cần phải gọi Action.loadMorePosts.

Và đó nên là khá nhiều đó.


Cảm ơn đã viết lên!
Dan Abramov

2
Có thể hơi muộn để dự tiệc, nhưng đây là một bài viết hay giải thích lý do tại sao để tránh gọi API trực tiếp từ các cửa hàng . Tôi vẫn đang tìm ra những thực tiễn tốt nhất là gì, nhưng tôi nghĩ nó có thể giúp những vấp ngã khác trong việc này. Có rất nhiều cách tiếp cận khác nhau trôi nổi liên quan đến các cửa hàng.
Thijs Koerselman
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.