(Catatan: Saya telah menggunakan sintaks ES6 menggunakan opsi JSX Harmony.)
Sebagai latihan, saya menulis contoh aplikasi Flux yang memungkinkan untuk menjelajah Github users
dan repo.
Ini didasarkan pada jawaban fisherwebdev tetapi juga mencerminkan pendekatan yang saya gunakan untuk menormalkan respons API.
Saya berhasil mendokumentasikan beberapa pendekatan yang telah saya coba saat mempelajari Flux.
Saya mencoba untuk tetap dekat dengan dunia nyata (pagination, tidak ada API localStorage palsu).
Ada beberapa bit di sini saya sangat tertarik pada:
Bagaimana Saya Mengklasifikasikan Toko
Saya mencoba menghindari beberapa duplikasi yang saya lihat di contoh Flux lain, khususnya di Toko. Saya merasa berguna untuk membagi Toko secara logis ke dalam tiga kategori:
Toko Konten menampung semua entitas aplikasi. Segala sesuatu yang memiliki ID membutuhkan Content Store-nya sendiri. Komponen yang membuat item individual meminta Content Store untuk data baru.
Konten Toko memanen objek mereka dari semua tindakan server. Misalnya, UserStore
perhatikanaction.response.entities.users
apakah ada tindakan apa pun yang diluncurkan. Tidak perlu untuk switch
. Normalizr memudahkan untuk meratakan setiap respons API ke format ini.
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
Daftar Toko melacak ID entitas yang muncul di beberapa daftar global (misalnya "umpan", "pemberitahuan Anda"). Dalam proyek ini, saya tidak memiliki Toko seperti itu, tetapi saya pikir saya akan tetap menyebutkannya. Mereka menangani pagination.
Mereka biasanya menanggapi hanya beberapa tindakan (misalnya REQUEST_FEED
, REQUEST_FEED_SUCCESS
, REQUEST_FEED_ERROR
).
// Paginated Stores keep their data like this
[7, 10, 5, ...]
Daftar Toko Terindeks seperti Daftar Toko tetapi mereka mendefinisikan hubungan satu-ke-banyak. Misalnya, "pelanggan pengguna", "stargazer repositori", "repositori pengguna". Mereka juga menangani pagination.
Mereka juga biasanya menanggapi hanya beberapa tindakan (misalnya REQUEST_USER_REPOS
, REQUEST_USER_REPOS_SUCCESS
, REQUEST_USER_REPOS_ERROR
).
Di sebagian besar aplikasi sosial, Anda akan memiliki banyak ini dan Anda ingin dapat membuatnya dengan cepat.
// Indexed Paginated Stores keep their data like this
{
2: [7, 10, 5, ...],
6: [7, 1, 2, ...],
...
}
Catatan: ini bukan kelas aktual atau sesuatu; itu hanya bagaimana saya suka berpikir tentang Toko. Saya membuat beberapa pembantu.
createStore
Metode ini memberi Anda Toko paling dasar:
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;
}
Saya menggunakannya untuk membuat semua Toko.
isInBag
, mergeIntoBag
Pembantu kecil berguna untuk Toko Konten.
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]));
}
}
}
Menyimpan status pagination dan memberlakukan pernyataan tertentu (tidak dapat mengambil halaman saat mengambil, dll).
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
Jadikan pembuatan Toko Daftar Terindeks sesederhana mungkin dengan menyediakan metode boilerplate dan penanganan tindakan:
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
};
Mixin yang memungkinkan komponen untuk mendengarkan Toko yang mereka minati, misalnya 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
, dengan semua pengguna yang relevan di dalamnya. Dan setiap pengguna akan memiliki beberapa bendera boolean yang menggambarkan hubungan dengan profil pengguna saat ini. Sesuatu seperti{ follower: true, followed: false }
, misalnya. MetodegetFolloweds()
dangetFollowers()
akan mengambil set pengguna yang berbeda yang Anda butuhkan untuk UI.