在 NgRx 中启动数据加载的位置
在 NgRx 中,从数据源(例如 REST API 或数据库)加载数据是通过 effect 来实现的。然而,首先需要有一些东西来 dispatch 触发 effect 的 action。我见过一些不同的建议/方法来实现这一点。
在我们的示例中,我们将从Order
服务加载实体集合。我们将介绍两个 action:LoadOrdersRequested
和LoadOrders
。第一个 action 将启动数据加载,然后 effect 将执行加载并 dispatch 一个LoadOrders
action,该 action 将加载的数据放入 store。
处理的效果LoadOrdersRequested
如下:
@Effect()
loadOrdersRequested$ = this.actions$.pipe(
ofType<LoadOrdersRequested>(ActionTypes.LoadOrdersRequested),
// Don't load if we've already loaded.
withLatestFrom(this.store.select(getAllOrdersLoaded)),
filter(([_, loaded]) => !loaded),
// Don't handle more than one load request at a time.
exhaustMap(() => this.ordersService.fetchAllOrders().pipe(
map(result => new LoadOrders(result))
))
);
现在,为了启动数据加载,我们需要LoadOrdersRequested
从某个地方调度该操作。主要有四个选项:
- 当应用程序启动时。
- 当容器组件被初始化时。
- 当应用程序导航到某条路线时。
- 当用户执行某个操作时。
第四种情况可能是用户点击按钮来显式加载或重新加载某些数据。本文我们将重点讨论前三种情况。
当应用程序启动时
优点:
- 保证数据加载。
缺点:
- 如果要加载大量数据,则会出现内存/性能问题。
在你的 AppComponent 中
LoadOrdersRequested
最简单的方法是从你的AppComponent
init 生命周期方法中调度动作:
export class AppComponent implements OnInit {
constructor(private store: Store<AppState>) {}
ngOnInit() {
this.store.dispatch(new LoadOrdersRequested());
}
}
https://stackblitz.com/edit/angular-ngrx-initiate-load-at-app-start-app-component
事实上
NgRx 提供了一个INIT
在应用启动时调度的 action。这似乎是启动数据加载的好时机,但有一个问题。该INIT
action 在 effect 订阅之前就被调度了,所以这样是行不通的:
@Effect()
init$ = this.actions$.pipe(
ofType(INIT),
map(() => new LoadOrdersRequested())
);
相反,NgRx 团队建议使用defer
RxJS 运算符:
@Effect()
init$ = defer(() => new LoadOrdersRequested());
然而,如果我们想让我们的效果潜在地触发其他效果,这种方法就行不通了。这是因为,虽然将动作defer
的创建延迟LoadOrdersRequested
到init$
可观察对象被订阅(在效果模块初始化期间),但该动作会在初始化完成之前被调度。因此,我们正在寻找的效果LoadOrdersRequested
可能尚未注册,具体取决于效果系统订阅不同效果的顺序。
我们也许可以通过重新排序效果来缓解这个问题,但更好的解决方案是使用asyncScheduler
延迟分派LoadOrdersRequested
动作:
import { asyncScheduler, of } from 'rxjs';
...
@Effect()
$init = of(new LoadOrdersRequested, asyncScheduler);
虽然不起作用,但我们也可以使用INIT
内置操作::ROOT_EFFECTS_INIT
@Effect()
$init = this.actions$.pipe(
ofType(ROOT_EFFECTS_INIT),
map(() => new LoadOrdersRequested())
);
https://stackblitz.com/edit/angular-ngrx-initiate-load-at-app-start-init
APP_INITIALIZER
Angular 提供了APP_INITIALIZER
一种在应用程序启动时运行代码的方法,您可以从那里分派操作:
@NgModule({
...
providers: [
{
provide: APP_INITIALIZER,
useFactory: (store: Store<AppState>) => {
return () => {
store.dispatch(new LoadOrdersRequested());
};
},
multi: true,
deps: [Store]
}
]
})
https://stackblitz.com/edit/angular-ngrx-initiate-load-at-app-start-app-initializer
当容器组件初始化时
优点:
- 您只需在需要时加载数据。
- 通过查看组件可以清楚地知道它所依赖的数据。
缺点:
- 您要么需要大量的操作,要么需要在多个地方调度相同的操作。
- 该组件不太纯粹,因为它有加载数据的副作用。
- 您可能会忘记从需要数据的组件中调度操作。如果您通常通过另一个启动数据加载的组件访问该组件,则此错误可能会被掩盖。例如,您通常会先打开列表页,然后再打开详情页。然后,有一天,您直接导航到详情页,结果页面就崩溃了。
@Component({ ... })
export class OrdersComponent implements OnInit {
order$: Observable<Order>;
constructor(private store: Store<AppState>) {
this.order$ = this.store.select(getOrder);
}
ngOnInit() {
this.store.dispatch(new LoadOrdersRequested());
}
}
https://stackblitz.com/edit/angular-ngrx-initiate-load-in-component
当应用导航到路线时
优点:
- 减少重复。位于路由层次结构根部的单个守卫可以加载所有子路由的数据,即使这些子路由是直接导航到的。
- 组件更加纯粹,因为它们仅从选定状态映射到它们的模板输出。
缺点:
- 相当直白:保护器将触发任何子路由的数据加载,即使其组件不需要它。
- 从组件本身来看,它需要哪些数据才能工作不太明显。如果它被移动到路由器层级结构中的其他位置,它就会崩溃。
- 如果需要某些特定数据的路由分布在整个路由器层次结构中,则用处不大,因为您需要在不同的位置包含保护。
路由器保护
@Injectable()
export class OrdersGuard implements CanActivate {
constructor(private store: Store<AppState>) {}
canActivate(): Observable<boolean> {
return this.store.pipe(
select(getAllOrdersLoaded),
tap(loaded => {
if (!loaded) {
this.store.dispatch(new LoadOrdersRequested());
}
}),
filter(loaded => loaded),
first()
);
}
}
const ROUTES: Route[] = [
{
path: 'orders',
component: OrdersList,
canActivate: [OrdersGuard],
children: [
...
]
}
]
https://stackblitz.com/edit/angular-ngrx-initiate-load-router-guard
基本的守卫可以直接调度LoadOrdersRequested
action,并依靠 effect 过滤掉不必要的加载请求。然而,通过检查 的条件allOrdersLoaded
,守卫可以延迟导航,直到加载完成。
路由器解析器
@Injectable()
export class OrdersResolver implements Resolve<boolean> {
constructor(private store: Store<DatasetsState>) { }
resolve(): Observable<boolean> {
return this.store.pipe(
select(allDatasetsLoaded),
tap(loaded => {
if (!loaded) {
this.store.dispatch(new AllDatasetsRequested());
}
}),
filter(loaded => loaded),
first()
);
}
}
https://stackblitz.com/edit/angular-ngrx-initiate-load-router-resolve
解析器的使用方式与守卫非常相似。主要区别在于,解析器的运行环境与守卫略有不同,它应该返回一个对象,并合并到激活路由的数据中。但是,我们不应该这样做,因为组件应该从存储中检索数据,而不是从激活路由中检索。因此,解析器应该只返回一个布尔值。
路由器动作效果
@Effect()
loadOrders$ = this.actions$.pipe(
ofType<RouterNavigationAction>(ROUTER_NAVIGATION),
withLatestFrom(this.store.select(allOrdersLoaded)),
filter(([action, loaded]) =>
action.payload.routerState.url.includes('/orders') && !loaded
),
map(() => new LoadOrdersRequested())
);
优点:
- 将事物保留在 NgRx 中,因此感觉更符合惯用习惯。
缺点:
- 需要你检查新路由是否匹配,而不是像守卫方法那样依赖路由器本身来执行此操作。如果有人在你的路由器配置中更改了路径,却忘记在你的效果中更改,这可能会导致加载错误。
通过选择器内的检查来发送操作
export function getAllOrders(store: Store<AppState>) {
return createSelector(
getOrders,
state => {
if (!state.allOrdersLoaded) {
store.dispatch(new LoadOrdersRequested());
}
return state.orders;
}
);
}
我实际上没有在野外见过这种做法,但这是我想到的一种方法。
优点:
- 保证当且仅当数据被查询使用时才加载数据。
缺点:
- 违反选择器应为纯函数的原则。
- 如果您对重用和组合选择器不够严格,最终可能会出现一些选择器触发加载,而一些选择器则不会触发加载,因为它们会遵循触发加载的选择器。
未来的可能性
听起来Angular Ivy可能会开启在组件上使用元编程的可能性,以更具声明性的方式配置诸如存储依赖项之类的东西。
结论
我不确定这些方法中是否有一种在所有情况下都能明显优于其他方法。您最好根据需要加载的数据源数量、数据量以及路由器树的复杂性和布局来选择方法。
例如,如果您有一个小型、简单的应用程序,数据量很少,那么在 INIT 时急切地加载所有内容可能是最好的主意。
但是,如果您有一个大型应用程序,分为不同的功能,每个功能都需要从单独的源加载数据,那么最好在每个功能的路由层次结构的根部使用一个保护器来根据其要求调度加载操作。
如果您有一个复杂的应用程序,其中各种容器的数据需求有部分重叠,那么最好让每个容器调度操作来加载它们所需的内容。
鏂囩珷鏉ユ簮锛�https://dev.to/jonrimmer/where-to-initiate-data-load-in-ngrx-358l