NGRX 流程和路由 - 组件被访问次数过多
NGRX flow and routing - Component being acessed too many times
在我的应用程序中实现 NGRX 商店后,我发现我的 HomeComponent 被加载了太多次。
流程如下,从头开始:
1 - 调用页面时,尝试加载仪表板,但 AuthGuard 告诉我用户未登录并加载 LoginComponent。
应用-routing.module.ts
const routes: Routes = [
{
path: 'login',
loadChildren: './landing/landing.module#LandingModule'
},
{
path: '',
canActivate: [AuthGuard],
loadChildren: './dashboard/dashboard.module#DashboardModule'
}
];
2 - 然后,用户选择通过 Facebook 登录。
login.component.ts
signInWithFacebook() {
this.store.dispatch(new FacebookLogIn());
}
3 - reducer 被调用,调用我的 LoginService,如果身份验证正常,发送到 LogInSuccess 效果。继续,我不会post这部分。
4 - 如果登录成功,我必须加载有关用户的其他信息,因此我调用其他商店,然后导航到我的 DashboardComponent。
@Effect({ dispatch: false })
LogInSuccess: Observable<any> = this.actions.pipe(
ofType(LoginActionTypes.LOGIN_SUCCESS),
tap(action => {
this.zone.run(() => {
this.store.dispatch(new GetData(action.payload.user.email));
this.store.dispatch(new GetData2(action.payload.user.uid));
localStorage.setItem('user', JSON.stringify(action.payload.user));
this.router.navigate(['/dashboard'])
});
})
);
5 - 仪表板一起加载 HomeComponent。
仪表板-routing.module.ts
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [AuthGuard],
children: [
{
path: '',
component: HomeComponent,
},
...
...
]
}
6 - 存储调用结果如下:
7 - 这就是问题所在。如果我在 HomeComponent 中执行 console.log,我会看到每个调用的商店都被调用了 1 次,如下所示。
问题是:
为什么?
我应该怎么做才能避免所有这些不必要的负载?
如果我删除上面的调度之一,它只会进入 HomeComponent 3 次,而不是图片中的 5 次,因为它删除了 2 次效果。
--更新--
HomeComponent.ts
isTermSigned = false;
homeInfo = {
isBeta: false,
isTermSigned: false,
displayName: '',
email: ''
};
homeSubscription: Subscription;
constructor(
private afs: AngularFirestore,
private router: Router,
private store: Store<AppState>
) { }
ngOnInit() {
this.homeSubscription = combineLatest(
this.store.pipe(select(selectData)),
this.store.pipe(select(selectStatusLogin))
).subscribe(([data, login]) => {
console.log(login);
if (login.user) {
this.homeInfo = {
isBeta: data.isBeta,
isTermSigned: data.isBeta,
displayName: login.user.displayName,
email: login.user.email
};
}
});
}
--更新2--
这是数据存储的重要部分
data.action.ts
export class GetData implements Action {
readonly type = PlayerActionTypes.GET_BETA_USER;
constructor(public payload: any) {}
}
export class GetDataSuccess implements Action {
readonly type = PlayerActionTypes.GET_DATA_SUCCESS;
constructor(public payload: any) {}
}
data.effect.ts
@Effect()
GetData: Observable<any> = this.actions.pipe(
ofType(PlayerActionTypes.GET_DATA),
mergeMap(email =>
this.dataService
.getData(email)
.then(data=> {
return new GetDataSuccess({
isBeta: data.email ? true : false,
isTermSigned: data.acceptTerms ? true : false
});
})
.catch(error => {
return new GetDataError({
isBetaUser: false,
isTermSigned: false
});
})
)
);
@Effect({ dispatch: false })
GetDataSuccess: Observable<any> = this.actions.pipe(
ofType(PlayerActionTypes.GET_DATA_SUCCESS),
tap(action => {
localStorage.setItem('data', JSON.stringify(action.payload));
})
);
data.reducer.ts
export interface State {
isBeta: boolean;
isTermSigned: boolean;
}
export const initialState: State = {
isBeta: false,
isTermSigned: false
};
export function reducer(state = initialState, action: All): State {
switch (action.type) {
case DataActionTypes.GET_DATA_SUCCESS: {
return {
...state,
isBeta: action.payload.isBeta,
isTermSigned: action.payload.isTermSigned
};
}
case DataActionTypes.GET_DATA_ERROR: {
return {
...state,
isBeta: action.payload.isBeta,
isTermSigned: action.payload.isTermSigned
};
}
...
default: {
const data = JSON.parse(localStorage.getItem('data'));
if (data) {
return {
...state,
isBeta: betaUser.isBeta,
isTermSigned: betaUser.isTermSigned
};
} else {
return state;
}
}
}
}
data.selector.ts
import { AppState } from '../reducers';
export const selectData = (state: AppState) => state.data;
--更新3--
另一件事可能会有所帮助并且让我心碎,当我注销时,调用了一个,而且只有一个,效果但是我的 HomeComponent,它根本没有重定向到它,被调用了两次:
{isAuthenticated: true, user: {…}, errorMessage: null}
{isAuthenticated: false, user: null, errorMessage: null}
我不确定是否完全理解您的上下文和需求,但我认为您的 HomeComponent
没有多次加载。但是,使用 combineLatest
创建的可观察对象收到多次相同的值。
我可以建议您 2 项可能的改进:
1) 使用选择器组成商店的多个切片。
例如,您可以创建一个getHomeInfo
选择器来接收您需要的所有信息,而避免在HomeComponent
中调用combineLatest
。它更清晰,文档更完整,也更适合下一点。
2) 使用记忆选择器 createSelector
检查托德座右铭中的 good post。
Memoized 选择器将避免无用的计算以及在可观察对象中发出无用的值。
您只会在值更新的情况下收到通知。
例子
为了说明这两点,我在 stackblitz 上创建了一个项目:
https://stackblitz.com/edit/angular-ajhyz4
没有createSelector
:
export const getValueWithoutCreateSelector = (state) => {
return state.app.value;
};
与createSelector
:
export const getValue = createSelector(getAppState, (state) => {
return state.value;
});
组合选择器:
export const getCustomMessage = createSelector(getValue, getText,
(value, text) => {
return `Your value is ${value} and text is '${text}'`;
})
在我的应用程序中实现 NGRX 商店后,我发现我的 HomeComponent 被加载了太多次。
流程如下,从头开始:
1 - 调用页面时,尝试加载仪表板,但 AuthGuard 告诉我用户未登录并加载 LoginComponent。
应用-routing.module.ts
const routes: Routes = [
{
path: 'login',
loadChildren: './landing/landing.module#LandingModule'
},
{
path: '',
canActivate: [AuthGuard],
loadChildren: './dashboard/dashboard.module#DashboardModule'
}
];
2 - 然后,用户选择通过 Facebook 登录。
login.component.ts
signInWithFacebook() {
this.store.dispatch(new FacebookLogIn());
}
3 - reducer 被调用,调用我的 LoginService,如果身份验证正常,发送到 LogInSuccess 效果。继续,我不会post这部分。
4 - 如果登录成功,我必须加载有关用户的其他信息,因此我调用其他商店,然后导航到我的 DashboardComponent。
@Effect({ dispatch: false })
LogInSuccess: Observable<any> = this.actions.pipe(
ofType(LoginActionTypes.LOGIN_SUCCESS),
tap(action => {
this.zone.run(() => {
this.store.dispatch(new GetData(action.payload.user.email));
this.store.dispatch(new GetData2(action.payload.user.uid));
localStorage.setItem('user', JSON.stringify(action.payload.user));
this.router.navigate(['/dashboard'])
});
})
);
5 - 仪表板一起加载 HomeComponent。
仪表板-routing.module.ts
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [AuthGuard],
children: [
{
path: '',
component: HomeComponent,
},
...
...
]
}
6 - 存储调用结果如下:
7 - 这就是问题所在。如果我在 HomeComponent 中执行 console.log,我会看到每个调用的商店都被调用了 1 次,如下所示。
问题是:
为什么?
我应该怎么做才能避免所有这些不必要的负载?
如果我删除上面的调度之一,它只会进入 HomeComponent 3 次,而不是图片中的 5 次,因为它删除了 2 次效果。
--更新--
HomeComponent.ts
isTermSigned = false;
homeInfo = {
isBeta: false,
isTermSigned: false,
displayName: '',
email: ''
};
homeSubscription: Subscription;
constructor(
private afs: AngularFirestore,
private router: Router,
private store: Store<AppState>
) { }
ngOnInit() {
this.homeSubscription = combineLatest(
this.store.pipe(select(selectData)),
this.store.pipe(select(selectStatusLogin))
).subscribe(([data, login]) => {
console.log(login);
if (login.user) {
this.homeInfo = {
isBeta: data.isBeta,
isTermSigned: data.isBeta,
displayName: login.user.displayName,
email: login.user.email
};
}
});
}
--更新2-- 这是数据存储的重要部分
data.action.ts
export class GetData implements Action {
readonly type = PlayerActionTypes.GET_BETA_USER;
constructor(public payload: any) {}
}
export class GetDataSuccess implements Action {
readonly type = PlayerActionTypes.GET_DATA_SUCCESS;
constructor(public payload: any) {}
}
data.effect.ts
@Effect()
GetData: Observable<any> = this.actions.pipe(
ofType(PlayerActionTypes.GET_DATA),
mergeMap(email =>
this.dataService
.getData(email)
.then(data=> {
return new GetDataSuccess({
isBeta: data.email ? true : false,
isTermSigned: data.acceptTerms ? true : false
});
})
.catch(error => {
return new GetDataError({
isBetaUser: false,
isTermSigned: false
});
})
)
);
@Effect({ dispatch: false })
GetDataSuccess: Observable<any> = this.actions.pipe(
ofType(PlayerActionTypes.GET_DATA_SUCCESS),
tap(action => {
localStorage.setItem('data', JSON.stringify(action.payload));
})
);
data.reducer.ts
export interface State {
isBeta: boolean;
isTermSigned: boolean;
}
export const initialState: State = {
isBeta: false,
isTermSigned: false
};
export function reducer(state = initialState, action: All): State {
switch (action.type) {
case DataActionTypes.GET_DATA_SUCCESS: {
return {
...state,
isBeta: action.payload.isBeta,
isTermSigned: action.payload.isTermSigned
};
}
case DataActionTypes.GET_DATA_ERROR: {
return {
...state,
isBeta: action.payload.isBeta,
isTermSigned: action.payload.isTermSigned
};
}
...
default: {
const data = JSON.parse(localStorage.getItem('data'));
if (data) {
return {
...state,
isBeta: betaUser.isBeta,
isTermSigned: betaUser.isTermSigned
};
} else {
return state;
}
}
}
}
data.selector.ts
import { AppState } from '../reducers';
export const selectData = (state: AppState) => state.data;
--更新3--
另一件事可能会有所帮助并且让我心碎,当我注销时,调用了一个,而且只有一个,效果但是我的 HomeComponent,它根本没有重定向到它,被调用了两次:
{isAuthenticated: true, user: {…}, errorMessage: null}
{isAuthenticated: false, user: null, errorMessage: null}
我不确定是否完全理解您的上下文和需求,但我认为您的 HomeComponent
没有多次加载。但是,使用 combineLatest
创建的可观察对象收到多次相同的值。
我可以建议您 2 项可能的改进:
1) 使用选择器组成商店的多个切片。
例如,您可以创建一个getHomeInfo
选择器来接收您需要的所有信息,而避免在HomeComponent
中调用combineLatest
。它更清晰,文档更完整,也更适合下一点。
2) 使用记忆选择器 createSelector
检查托德座右铭中的 good post。
Memoized 选择器将避免无用的计算以及在可观察对象中发出无用的值。 您只会在值更新的情况下收到通知。
例子
为了说明这两点,我在 stackblitz 上创建了一个项目: https://stackblitz.com/edit/angular-ajhyz4
没有createSelector
:
export const getValueWithoutCreateSelector = (state) => {
return state.app.value;
};
与createSelector
:
export const getValue = createSelector(getAppState, (state) => {
return state.value;
});
组合选择器:
export const getCustomMessage = createSelector(getValue, getText,
(value, text) => {
return `Your value is ${value} and text is '${text}'`;
})