(참고 : JSX Harmony 옵션을 사용하여 ES6 구문을 사용했습니다.)
연습 으로 브라우징 및 리 포지션 을 허용 하는 샘플 Flux 앱 을 작성했습니다 Github users
.
그것은 fisherwebdev의 답변을 기반으로 하지만 API 응답을 정규화하는 데 사용하는 접근법을 반영합니다.
Flux를 배우는 동안 시도한 몇 가지 접근법을 문서화했습니다.
나는 그것을 현실 세계에 가깝게 유지하려고 노력했다 (매김, 가짜 localStorage API 없음).
여기에 특히 관심이있는 몇 가지 비트가 있습니다.
상점을 분류하는 방법
다른 Flux 예제, 특히 Stores에서 본 중복을 피하려고했습니다. 논리적으로 상점을 세 가지 범주로 나누는 것이 유용하다는 것을 알았습니다.
컨텐츠 저장소 는 모든 앱 엔티티를 보유합니다. ID가있는 모든 것은 자체 컨텐츠 저장소가 필요합니다. 개별 항목을 렌더링하는 구성 요소는 Content Store에 최신 데이터를 요청합니다.
컨텐츠 저장소는 모든 서버 조치 에서 오브젝트를 수집 합니다. 예를 들어, UserStore
에 보이는action.response.entities.users
존재하는 경우 에 관계없이 작업이 발사있는. 필요가 없습니다 switch
. Normalizr를 사용하면 API 응답을이 형식으로 쉽게 병합 할 수 있습니다.
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
목록 저장소 는 일부 전체 목록에 나타나는 엔티티의 ID (예 : "피드", "알림")를 추적합니다. 이 프로젝트에는 그런 상점이 없지만 어쨌든 언급 할 것이라고 생각했습니다. 그들은 페이지 매김을 처리합니다.
그들은 일반적으로 (예를 들어, 몇 가지의 행동에 반응 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, ...],
...
}
참고 : 이들은 실제 클래스 나 다른 것이 아닙니다. 상점에 대해 생각하는 방식입니다. 그래도 헬퍼 몇 명을 만들었습니다.
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]));
}
}
}
페이지 매김 상태를 저장하고 특정 어설 션을 적용합니다 (가져 오는 동안 페이지를 가져올 수 없음 등).
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
상용구 분석법 및 작업 처리 기능을 제공하여 인덱싱 된 목록 저장소를 최대한 간단하게 만듭니다.
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
};
예를 들어, 구성 요소를 관심있는 상점에 맞출 수있는 믹스 인 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
모든 관련 사용자를 포함하는 것입니다. 그리고 각 사용자에게는 현재 사용자 프로필과의 관계를 설명하는 두 개의 부울 플래그가 있습니다. 같은 뭔가{ follower: true, followed: false }
예를 들어,. 메서드getFolloweds()
와getFollowers()
UI에 필요한 다른 사용자 집합을 검색합니다.