我怎样才能捕捉到两个(或更多)@ngrx/store 动作并在组件中保持订阅直到发生这种情况?
How can I catch two (or more) @ngrx/store actions and hold subscription in components till that happens?
在我的主要组件中,我开始初始化我的数据:
_store.dispatch(_statusActions.initialize());
即触发所有初始化动作:
@Effect()
loading$ = this.actions$
.ofType(StatusActions.INITIALIZING)
.mergeMap(() => Observable.from([
this._characterActions.initCharacters(),
this._vehicleActions.initVehicles()
]))
在商店中加载数据后,将触发所有商店实体的成功操作。
对于车辆:
@Effect()
loadVehicles$ = this.actions$
.ofType(VehicleActions.INIT_VEHICLES)
.switchMap(() => this._vehicleService.getVehicles()
.map((vehicles: Vehicle[]) => this._vehicleActions.initVehiclesSuccess(vehicles))
.catch(err => Observable.of(this._statusActions.dataLoadingError('vehicles')))
);
对于字符:
@Effect()
loadVehicles$ = this.actions$
.ofType(CharacterActions.INIT_CHARACTERS)
.switchMap(() => this._characterService.getCharacters())
.map((characters: Character[]) =>
this._characterActions.initCharactersSucess(characters))
最后,在触发所有 *_DATA_SUCCESS 操作之后,我希望触发我的 INITIALIZED 操作以将 READY 标志放入我的存储中。
export const initReducer = (state: boolean = false, action: Action): boolean => {
switch (action.type){
case StatusActions.INITIALIZING:
return false;
case StatusActions.INITIALIZED:
console.log('App initialized...');
return true;
default:
return state;
}
我的问题 - 如何执行?如何知道何时触发了所有成功操作?
UPD
Snks mtx,我跟着你的第一个更快的adwise.
抱歉提出额外的问题,但我真的一直在寻找好的方法来做
下一步。如何保持这个订阅(在组件内部)直到我的 INITIALIZED 操作被触发(需要用 if(vehicles.length>0) 移除这个可怕的拐杖):
constructor(
...
) {
this.vehicles$ = _store.select(s => s.vehicles);
this.initialized$ = _store.select(s => s.initilized);
}
ngOnInit() {
let id = this._route.snapshot.params.id ? this._route.snapshot.params.id :
null;
this.sub = this.vehicles$.subscribe(vehicles => {
if (vehicles.length > 0){
if(id){
this.vehicle = vehicles.find(item => item.id === Number(id))
} else {
this.vehicle = new Vehicle(vehicles[vehicles.length-1].id+1, '');
}
this.viewReady = true;
}
})
}
ngOnDestroy(){
this.sub && this.sub.unsubscribe();
}
我试图在 subscribe() 之前插入 skipUntil(),但现在我遇到了问题
从另一个组件打开此组件(当所有数据都已加载时)。在这种情况下,订阅回调不能再触发了!我不明白为什么...
...
private initDone$ = new Subject<boolean>();
...
this.initialized$.subscribe((init: Init) => {
if(init.app)
this.initDone$.next(true);
})
this.sub = this.vehicles$.skipUntil(this.initDone$).subscribe(vehicles => {
if(id)
this.vehicle = vehicles.find(item => item.id === Number(id))
else
this.vehicle = new Vehicle(vehicles[vehicles.length-1].id+1, '');
this.viewReady = true;
});
}
要重现我的问题,只需按列表中的其中一辆车。订阅回调未触发。然后按 F5 -> 现在载具加载,因为按设计触发了回调。
完整的源代码在这里:GitHub,
最新版本 运行 on GitHub Pages
我可以想到两种方法(我相信还有其他方法):
1。在减速器
通过减少 initReducer
状态中的两个 *_DATA_SUCCESS 操作,让 initReducer
状态为每个必须成功的请求设置标志,以便将就绪标志设置为 true
=16=]:
init.reducer.ts
export interface InitState = {
characterSuccess: boolean,
vehicleSuccess: boolean,
ready: boolean
}
const initialState = {
characterSuccess = false,
vehicleSuccess = false,
ready = false
};
export const initReducer (state: InitState = initialState, action: Action): InitState {
switch (action.type) {
/* ...
* other cases, like INITIALIZING or INITIALIZING_ERROR...
*/
case CharacterActions.CHARACTER_DATA_SUCCESS: {
/* set characterSuccess to true.
*
* if vehicleSuccess is already true
* this means that both requests completed successfully
* otherwise ready will stay false until the second request completes
*/
return Object.assign({}, state, {
characterSuccess: true
ready: state.vehicleSuccess
});
}
case VehicleActions.VEHICLE_DATA_SUCCESS: {
/* set vehicleSuccess to true.
*
* if characterSuccess is already true
* this means that both requests completed successfully
* otherwise ready will stay false until the second request completes
*/
return Object.assign({}, state, {
vehicleSuccess: true,
ready: state.characterSuccess
});
}
default:
return state;
}
}
2。使用选择器
如果您创建 initReducer
只是为了跟踪您当前是否正在初始化,则可以省略整个减速器并使用 选择器 来计算派生状态。
我喜欢使用 reselect 库,因为它可以让您创建仅在发生更改时才重新计算的高效选择器(= 记忆选择器)。
首先,向 Vehicles- 和 Characters-reducer 的状态形状添加 loading
- 和 ready
- 标志。
然后,在减速器级别添加选择器功能。
VehiclesReducer 示例:
vehicle.reducer.ts
(对 Characters-reducer 重复相同的操作)
export interface VehicleState {
// vehicle ids and entities etc...
loading: boolean;
ready: boolean;
}
const initialState: VehicleState = {
// other init values
loading: false,
ready: false
}
export function reducer(state = initialState, action: Action): VehicleState {
switch (action.type) {
// other cases...
case VehicleActions.INIT_VEHICLES: {
return Object.assign({}, state, {
loading: true,
ready: false
});
}
case VehicleActions.VEHICLE_DATA_SUCCESS: {
return Object.assign({}, state, {
/* other reducer logic like
* entities: action.payload
*/
loading: false,
ready: true
});
}
default:
return state;
}
}
// Selector functions
export const getLoading = (state: VehicleState) => state.loading;
export const getReady = (state: VehicleState) => state.ready;
接下来,在您的 root-reducer 或您放置选择器的附加文件中,编写提供所需派生状态的选择器:
selectors.ts
import { MyGlobalAppState } from './root.reducer';
import * as fromVehicle from './vehicle.reducer';
import * as fromCharacter from './character.reducer';
import { createSelector } from 'reselect';
// selector for vehicle-state
export const getVehicleState = (state: MyGlobalAppState) => state.vehicle;
// selector for character-state
export const getCharacterState = (state: MyGlobalAppState) => state.character;
// selectors from vehicle
export const getVehicleLoading = createSelector(getVehicleState, fromVehicle.getLoading);
export const getVehicleReady = createSelector(getVehicleState, fromVehicle.getReady);
// selectors from character
export const getCharacterLoading = createSelector(getCharacterState, fromCharacter.getLoading);
export const getCharacterReady = createSelector(getCharacterState, fromCharacter.getReady);
// combined selectors that will calculate a derived state from both vehicle-state and character-state
export const getLoading = createSelector(getVehicleLoading, getCharacterLoading, (vehicle, character) => {
return (vehicle || character);
});
export const getReady = createSelector(getVehicleReady, getCharacterReady, (vehicle, character) => {
return (vehicle && character);
});
现在您可以在您的组件中使用这些选择器:
import * as selectors from './selectors';
let loading$ = this.store.select(selectors.getLoading);
let ready$ = this.store.select(selectors.getReady);
loading$.subscribe(loading => console.log(loading)); // will emit true when requests are still running
ready$.subscribe(ready => console.log(ready)); // will emit true when both requests where successful
虽然这种方法可能更冗长,但它更简洁并且遵循了 redux 的既定做法。你可以省略整个 initReducer
.
如果您以前没有使用过选择器,它会在 ngrx-example-app 中展示。
关于更新:
由于您使用的是路由器,因此您可以使用 router-guard 等来阻止路由激活,直到初始化完成。实现CanActivate接口:
@Injectable()
export class InitializedGuard implements CanActivate {
constructor(private store: Store<MyGlobalAppState>) { }
canActivate(): Observable<boolean> {
return this.store.select(fromRoot.getInitState) // select initialized from store
.filter(initialized => initialized === true)
.take(1)
}
}
然后,将警卫添加到您的路线中:
{
path: 'vehicle/:id',
component: VehicleComponent,
canActivate: [ InitializedGuard ]
}
路由激活守卫也在 ngrx-example-app 中展示,看here
在我的主要组件中,我开始初始化我的数据:
_store.dispatch(_statusActions.initialize());
即触发所有初始化动作:
@Effect()
loading$ = this.actions$
.ofType(StatusActions.INITIALIZING)
.mergeMap(() => Observable.from([
this._characterActions.initCharacters(),
this._vehicleActions.initVehicles()
]))
在商店中加载数据后,将触发所有商店实体的成功操作。
对于车辆:
@Effect()
loadVehicles$ = this.actions$
.ofType(VehicleActions.INIT_VEHICLES)
.switchMap(() => this._vehicleService.getVehicles()
.map((vehicles: Vehicle[]) => this._vehicleActions.initVehiclesSuccess(vehicles))
.catch(err => Observable.of(this._statusActions.dataLoadingError('vehicles')))
);
对于字符:
@Effect()
loadVehicles$ = this.actions$
.ofType(CharacterActions.INIT_CHARACTERS)
.switchMap(() => this._characterService.getCharacters())
.map((characters: Character[]) =>
this._characterActions.initCharactersSucess(characters))
最后,在触发所有 *_DATA_SUCCESS 操作之后,我希望触发我的 INITIALIZED 操作以将 READY 标志放入我的存储中。
export const initReducer = (state: boolean = false, action: Action): boolean => {
switch (action.type){
case StatusActions.INITIALIZING:
return false;
case StatusActions.INITIALIZED:
console.log('App initialized...');
return true;
default:
return state;
}
我的问题 - 如何执行?如何知道何时触发了所有成功操作?
UPD
Snks mtx,我跟着你的第一个更快的adwise.
抱歉提出额外的问题,但我真的一直在寻找好的方法来做 下一步。如何保持这个订阅(在组件内部)直到我的 INITIALIZED 操作被触发(需要用 if(vehicles.length>0) 移除这个可怕的拐杖):
constructor(
...
) {
this.vehicles$ = _store.select(s => s.vehicles);
this.initialized$ = _store.select(s => s.initilized);
}
ngOnInit() {
let id = this._route.snapshot.params.id ? this._route.snapshot.params.id :
null;
this.sub = this.vehicles$.subscribe(vehicles => {
if (vehicles.length > 0){
if(id){
this.vehicle = vehicles.find(item => item.id === Number(id))
} else {
this.vehicle = new Vehicle(vehicles[vehicles.length-1].id+1, '');
}
this.viewReady = true;
}
})
}
ngOnDestroy(){
this.sub && this.sub.unsubscribe();
}
我试图在 subscribe() 之前插入 skipUntil(),但现在我遇到了问题 从另一个组件打开此组件(当所有数据都已加载时)。在这种情况下,订阅回调不能再触发了!我不明白为什么...
...
private initDone$ = new Subject<boolean>();
...
this.initialized$.subscribe((init: Init) => {
if(init.app)
this.initDone$.next(true);
})
this.sub = this.vehicles$.skipUntil(this.initDone$).subscribe(vehicles => {
if(id)
this.vehicle = vehicles.find(item => item.id === Number(id))
else
this.vehicle = new Vehicle(vehicles[vehicles.length-1].id+1, '');
this.viewReady = true;
});
}
要重现我的问题,只需按列表中的其中一辆车。订阅回调未触发。然后按 F5 -> 现在载具加载,因为按设计触发了回调。
完整的源代码在这里:GitHub, 最新版本 运行 on GitHub Pages
我可以想到两种方法(我相信还有其他方法):
1。在减速器
通过减少 initReducer
状态中的两个 *_DATA_SUCCESS 操作,让 initReducer
状态为每个必须成功的请求设置标志,以便将就绪标志设置为 true
=16=]:
init.reducer.ts
export interface InitState = {
characterSuccess: boolean,
vehicleSuccess: boolean,
ready: boolean
}
const initialState = {
characterSuccess = false,
vehicleSuccess = false,
ready = false
};
export const initReducer (state: InitState = initialState, action: Action): InitState {
switch (action.type) {
/* ...
* other cases, like INITIALIZING or INITIALIZING_ERROR...
*/
case CharacterActions.CHARACTER_DATA_SUCCESS: {
/* set characterSuccess to true.
*
* if vehicleSuccess is already true
* this means that both requests completed successfully
* otherwise ready will stay false until the second request completes
*/
return Object.assign({}, state, {
characterSuccess: true
ready: state.vehicleSuccess
});
}
case VehicleActions.VEHICLE_DATA_SUCCESS: {
/* set vehicleSuccess to true.
*
* if characterSuccess is already true
* this means that both requests completed successfully
* otherwise ready will stay false until the second request completes
*/
return Object.assign({}, state, {
vehicleSuccess: true,
ready: state.characterSuccess
});
}
default:
return state;
}
}
2。使用选择器
如果您创建 initReducer
只是为了跟踪您当前是否正在初始化,则可以省略整个减速器并使用 选择器 来计算派生状态。
我喜欢使用 reselect 库,因为它可以让您创建仅在发生更改时才重新计算的高效选择器(= 记忆选择器)。
首先,向 Vehicles- 和 Characters-reducer 的状态形状添加 loading
- 和 ready
- 标志。
然后,在减速器级别添加选择器功能。
VehiclesReducer 示例:
vehicle.reducer.ts (对 Characters-reducer 重复相同的操作)
export interface VehicleState {
// vehicle ids and entities etc...
loading: boolean;
ready: boolean;
}
const initialState: VehicleState = {
// other init values
loading: false,
ready: false
}
export function reducer(state = initialState, action: Action): VehicleState {
switch (action.type) {
// other cases...
case VehicleActions.INIT_VEHICLES: {
return Object.assign({}, state, {
loading: true,
ready: false
});
}
case VehicleActions.VEHICLE_DATA_SUCCESS: {
return Object.assign({}, state, {
/* other reducer logic like
* entities: action.payload
*/
loading: false,
ready: true
});
}
default:
return state;
}
}
// Selector functions
export const getLoading = (state: VehicleState) => state.loading;
export const getReady = (state: VehicleState) => state.ready;
接下来,在您的 root-reducer 或您放置选择器的附加文件中,编写提供所需派生状态的选择器:
selectors.ts
import { MyGlobalAppState } from './root.reducer';
import * as fromVehicle from './vehicle.reducer';
import * as fromCharacter from './character.reducer';
import { createSelector } from 'reselect';
// selector for vehicle-state
export const getVehicleState = (state: MyGlobalAppState) => state.vehicle;
// selector for character-state
export const getCharacterState = (state: MyGlobalAppState) => state.character;
// selectors from vehicle
export const getVehicleLoading = createSelector(getVehicleState, fromVehicle.getLoading);
export const getVehicleReady = createSelector(getVehicleState, fromVehicle.getReady);
// selectors from character
export const getCharacterLoading = createSelector(getCharacterState, fromCharacter.getLoading);
export const getCharacterReady = createSelector(getCharacterState, fromCharacter.getReady);
// combined selectors that will calculate a derived state from both vehicle-state and character-state
export const getLoading = createSelector(getVehicleLoading, getCharacterLoading, (vehicle, character) => {
return (vehicle || character);
});
export const getReady = createSelector(getVehicleReady, getCharacterReady, (vehicle, character) => {
return (vehicle && character);
});
现在您可以在您的组件中使用这些选择器:
import * as selectors from './selectors';
let loading$ = this.store.select(selectors.getLoading);
let ready$ = this.store.select(selectors.getReady);
loading$.subscribe(loading => console.log(loading)); // will emit true when requests are still running
ready$.subscribe(ready => console.log(ready)); // will emit true when both requests where successful
虽然这种方法可能更冗长,但它更简洁并且遵循了 redux 的既定做法。你可以省略整个 initReducer
.
如果您以前没有使用过选择器,它会在 ngrx-example-app 中展示。
关于更新:
由于您使用的是路由器,因此您可以使用 router-guard 等来阻止路由激活,直到初始化完成。实现CanActivate接口:
@Injectable()
export class InitializedGuard implements CanActivate {
constructor(private store: Store<MyGlobalAppState>) { }
canActivate(): Observable<boolean> {
return this.store.select(fromRoot.getInitState) // select initialized from store
.filter(initialized => initialized === true)
.take(1)
}
}
然后,将警卫添加到您的路线中:
{
path: 'vehicle/:id',
component: VehicleComponent,
canActivate: [ InitializedGuard ]
}
路由激活守卫也在 ngrx-example-app 中展示,看here