(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 users
và 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ù.
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]));
}
}
}
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++;
}
}
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
};
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;
}
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ứcgetFolloweds()
vàgetFollowers()
sẽ truy xuất các nhóm người dùng khác nhau mà bạn cần cho UI.