Dan*_*mov 132 javascript architecture facebook reactjs reactjs-flux
我正在阅读关于Flux但是示例Todo应用程序太简单了,我无法理解一些关键点.
想象一下像Facebook这样的单页应用程序,它具有用户个人资料页面.在每个用户个人资料页面上,我们希望显示一些用户信息及其最后的帖子,并带有无限滚动.我们可以从一个用户档案导航到另一个用户档案.
在Flux架构中,这将如何与商店和调度员相对应?
我们会为PostStore每个用户使用一个,还是会有某种全球商店?那么调度员呢,我们会为每个"用户页面"创建一个新的Dispatcher,还是我们会使用单例?最后,该架构的哪个部分负责管理"特定于页面"的商店的生命周期以响应路由变化?
此外,单个伪页面可以具有多个相同类型的数据列表.例如,个人资料页上,我想同时显示关注和跟随.UserStore在这种情况下,单身人士如何工作?将UserPageStore管理followedBy: UserStore和follows: UserStore?
fis*_*dev 124
在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()方法中完成的.
Dan*_*mov 79
(注意:我使用了JSX Harmony选项使用了ES6语法.)
作为练习,我写了一个样本Flux应用程序,允许浏览Github users和回购.
它基于fisherwebdev的答案,但也反映了我用于规范API响应的方法.

我记录了一些我在学习Flux时尝试过的方法.
我试图让它接近现实世界(分页,没有虚假的localStorage API).
这里有几点我特别感兴趣:
switch有行动的巨人 ;我试图避免在其他Flux示例中看到的一些重复,特别是在商店中.我发现从逻辑上将商店划分为三类很有用:
内容商店包含所有应用实体.拥有ID的所有内容都需要自己的内容存储库.呈现单个项目的组件向内容存储区请求新数据.
内容存储从所有服务器操作中收集其对象.例如,无论触发哪个动作,都会UserStore 查看action.response.entities.users它是否存在.没有必要.Normalizr可以轻松地将任何API响应压缩为此格式.switch
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
Run Code Online (Sandbox Code Playgroud)
列表商店会跟踪某些全局列表中显示的实体的ID(例如"Feed","您的通知").在这个项目中,我没有这样的商店,但我想我还是会提到它们.他们处理分页.
他们通常应对短短的动作(例如REQUEST_FEED,REQUEST_FEED_SUCCESS,REQUEST_FEED_ERROR).
// Paginated Stores keep their data like this
[7, 10, 5, ...]
Run Code Online (Sandbox Code Playgroud)
索引列表存储类似于列表存储,但它们定义了一对多关系.例如,"用户的订阅者","存储库的观星者","用户的存储库".他们也处理分页.
他们也通常只有几个动作做出反应(例如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, ...],
...
}
Run Code Online (Sandbox Code Playgroud)
注意:这些不是实际的类或其他东西; 这就是我喜欢考虑商店的方式.我做了几个助手.
StoreUtilscreateStore此方法为您提供最基本的商店:
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;
}
Run Code Online (Sandbox Code Playgroud)
我用它来创建所有商店.
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]));
}
}
}
Run Code Online (Sandbox Code Playgroud)
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++;
}
}
Run Code Online (Sandbox Code Playgroud)
PaginatedStoreUtilscreateListStore,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
};
Run Code Online (Sandbox Code Playgroud)
createStoreMixin一种mixin,允许组件收听他们感兴趣的商店,例如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;
}
Run Code Online (Sandbox Code Playgroud)
Spo*_*ike 27
因此,在Reflux中,Dispatcher的概念被删除,您只需要考虑通过操作和存储的数据流.即
Actions <-- Store { <-- Another Store } <-- Components
Run Code Online (Sandbox Code Playgroud)
此处的每个箭头都模拟了如何监听数据流,这反过来意味着数据以相反的方向流动.数据流的实际数字是这样的:
Actions --> Stores --> Components
^ | |
+----------+------------+
Run Code Online (Sandbox Code Playgroud)
在您的用例中,如果我理解正确,我们需要一个openUserProfile启动用户配置文件加载和切换页面的操作,还有一些帖子加载将在打开用户配置文件页面和无限滚动事件期间加载帖子的操作.所以我想我们在应用程序中有以下数据存储:
在Reflux中你可以这样设置:
// Set up the two actions we need for this use case.
var Actions = Reflux.createActions(['openUserProfile', 'loadUserProfile', 'loadInitialPosts', 'loadMorePosts']);
Run Code Online (Sandbox Code Playgroud)
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);
}
});
Run Code Online (Sandbox Code Playgroud)
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);
}
});
Run Code Online (Sandbox Code Playgroud)
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');
}
});
Run Code Online (Sandbox Code Playgroud)
我假设您有整个页面视图,用户个人资料页面和帖子列表的组件.需要连接以下内容:
Action.openUserProfile在其单击事件期间使用正确的id 调用.currentPageStore因此它知道要切换到哪个页面.currentUserProfileStore以便知道要显示的用户配置文件数据currentPostsStore收到的帖子Action.loadMorePosts.这应该是相当的.
| 归档时间: |
|
| 查看次数: |
35196 次 |
| 最近记录: |