如何在不调用 subscribe 方法的情况下重写 rxjs 代码?
How to rewrite rxjs code without making calls within the subcribe method?
您好,我正在使用 angular 5 项目和 rxjs 库。我在下面有代码片段。我想以更好的方式重写。
export class AccountDetailsComponent implements OnInit {
ngOnInit() {
this.route.paramMap
.pipe(
tap(paramMap => { this.id = paramMap.get('id') }),
switchMap(paramMap =>
forkJoin(
this.accountsService.get(paramMap.get('id')).pipe(
catchError(error => {
this.notFoundService.checkStatus(error);
return _throw(error);
})
),
this.accountsService.getAccountUsers(paramMap.get('id'))
)
)
)
.subscribe(
([account, users]) => {
this.isLoading = false;
this.account = account;
let hasTransactionalReportingEnabled : boolean = this.account.features.some( feature => feature.code === FeatureCode.TRANSACTIONAL_REPORTING);
if(hasTransactionalReportingEnabled) {
this.accountsService.getAccountEsmeGroup(this.id).subscribe(
(accountEsmeGroup) => {
this.accountEsmeGroup = accountEsmeGroup;
}
);
}
this.users = users;
},
() => {
this.isLoading = false;
}
);
}
}
在帐户中-details.component.ts class 有一个 ngOnInit() 方法。它的两个 http 服务调用使用 rxjs forkJoin 函数,即 this.accountsService.get(paramMap.get('id')) & this.accountsService.getAccountUsers(paramMap.get('id') ) 。然后在订阅中我再次调用 this.accountsService.getAccountEsmeGroup(this.id),仅当变量 hasTransactionalReportingEnabled 表达式为真时。有没有更好的方法在 rxjs 中重写上面的代码片段。我不确定在订阅方法中再次调用是否是个好主意。
如果您能提供帮助,我们将不胜感激
谢谢
我发现如果将代码分解成不同的部分,简化代码会容易得多。您基本上有 5 个不同的数据:id
、isLoading
、account
、users
、accountEsmeGroup
.
让我们从 ID 开始。它来自一个可观察的来源。看起来您正在复制到另一个 属性 (this.id
) 只是为了将它用于其他可观察的调用。
这不是“敲出”这个值所必需的(或任何值,大部分时间)。让我们将其定义为可观察对象:
private id$ = this.route.paramMap.pipe(
map(params => params.get('id'))
);
这不是很好吗?很明显 id$
,如果订阅,将发出 id。而且,如果路由参数值发生变化(用户导航到另一个帐户),将发出新的 ID。太棒了!
现在,让我们看看如何定义相互依赖的可观察对象。由于 users
依赖于 id
,我们将这样定义:
private users$ = this.id$.pipe(
switchMap(id => this.accountsService.getAccountUsers(id))
);
此处 users$
将始终代表 id$
上次发出的帐户的用户。 switchMap
处理对 getAccountUsers 的可观察调用的订阅,每当它收到新的 id
时,它都会进行新的调用并发出该结果。惊人的!这是反应性的:如果参数更改 -> id$
发出新的 id -> users$
发出新的用户数组。
account$
的定义也将以 id$
开头。看起来 esmeGroup
可能是 account
的可选 属性;要分配 esmeGroup
属性,我们可以有条件地 return undefined
或 getAccountEsmeGroup()
:
的调用结果
private account$ = this.id$.pipe(
switchMap(id => this.accountsService.get(id)),
switchMap(account => {
const hasTransactionalReportingEnabled = account.features.some(
feature => feature.code === FeatureCode.TRANSACTIONAL_REPORTING
);
const esmeGroup$ = hasTransactionalReportingEnabled
? this.accountsService.getAccountEsmeGroup(account.id)
: of(undefined)
return esmeGroup$.pipe(
map(esmeGroup => ({...account, esmeGroup}))
);
}),
catchError(...)
);
现在,让我们将这些 observable 组合成一个单一的视图模型 observable,供我们的模板使用。而不是使用 forkJoin
, we will use combineLatest
。它们很相似,但是 forkJoin 只会发出一次,而 combineLatest 可以发出多次,这很好,因为如果我们的 id
url 参数发生变化,如果我们的数据自动 (反应性 ) 更新。
public vm$ = combineLatest([this.users$, this.account$]).pipe(
map(([users, account]) => ({users, account}))
);
你有没有注意到我们还没有订阅任何东西?
您是否也注意到我们也不需要 ngOnInit
?
在我看来,这使得组件控制器更加简单。
现在,在模板中,您可以使用 async
管道。我不知道您的模板是什么样的,但这是一个简化的示例:
<ng-container *ngIf="isLoading; else content">
<p>loading...</p>
</ng-container>
<ng-template #content>
<div *ngIf="vm$ | async as vm">
<ul>
<li *ngFor="let user of vm.users">{{ user.name }}</li>
</ul>
<h3>{{ vm.account.name }}</h3>
<p *ngIf="vm.account.esmeGroup as group">{{ group.name }}</p>
</div>
</ng-template>
有几种方法可以处理 isLoading
值。一种简单的方法是使用 tap
在 id$
发出时设置 isLoading = true
并在 vm$
发出时设置 isLoading = false
。
因此,总的来说,一个完整的解决方案可能如下所示:
export class AccountDetailsComponent {
constructor(private route: ActivatedRoute) { }
private isLoading = true;
private id$ = this.route.paramMap.pipe(
map(params => params.get('id')),
tap(() => this.isLoading = true)
);
private users$ = this.id$.pipe(
switchMap(id => this.accountsService.getAccountUsers(id))
);
private account$ = this.id$.pipe(
switchMap(id => this.accountsService.get(id)),
switchMap(account => {
const hasTransactionalReportingEnabled = account.features.some(
feature => feature.code === FeatureCode.TRANSACTIONAL_REPORTING
);
const esmeGroup$ = hasTransactionalReportingEnabled
? this.accountsService.getAccountEsmeGroup(account.id)
: of(undefined);
return esmeGroup$.pipe(
map(esmeGroup => ({...account, esmeGroup}))
);
})
catchError(...)
);
public vm$ = combineLatest([this.users$, this.account$]).pipe(
map(([users, account]) => ({users, account})),
tap(() => this.isLoading = false)
);
}
在我看来,此解决方案可实现您的目标:
rewrite in a better way
有几个好处:
- 没有嵌套订阅
- 每条数据单独定义,方便以后理解和修改
- 代码模板是反应式的;如果我们的任何数据发生变化,视图将自动更新
- 我们将单个可观察对象暴露给我们的视图,然后我们可以使用单个
async
管道轻松使用它;减轻管理任何订阅的需要
您好,我正在使用 angular 5 项目和 rxjs 库。我在下面有代码片段。我想以更好的方式重写。
export class AccountDetailsComponent implements OnInit {
ngOnInit() {
this.route.paramMap
.pipe(
tap(paramMap => { this.id = paramMap.get('id') }),
switchMap(paramMap =>
forkJoin(
this.accountsService.get(paramMap.get('id')).pipe(
catchError(error => {
this.notFoundService.checkStatus(error);
return _throw(error);
})
),
this.accountsService.getAccountUsers(paramMap.get('id'))
)
)
)
.subscribe(
([account, users]) => {
this.isLoading = false;
this.account = account;
let hasTransactionalReportingEnabled : boolean = this.account.features.some( feature => feature.code === FeatureCode.TRANSACTIONAL_REPORTING);
if(hasTransactionalReportingEnabled) {
this.accountsService.getAccountEsmeGroup(this.id).subscribe(
(accountEsmeGroup) => {
this.accountEsmeGroup = accountEsmeGroup;
}
);
}
this.users = users;
},
() => {
this.isLoading = false;
}
);
}
}
在帐户中-details.component.ts class 有一个 ngOnInit() 方法。它的两个 http 服务调用使用 rxjs forkJoin 函数,即 this.accountsService.get(paramMap.get('id')) & this.accountsService.getAccountUsers(paramMap.get('id') ) 。然后在订阅中我再次调用 this.accountsService.getAccountEsmeGroup(this.id),仅当变量 hasTransactionalReportingEnabled 表达式为真时。有没有更好的方法在 rxjs 中重写上面的代码片段。我不确定在订阅方法中再次调用是否是个好主意。
如果您能提供帮助,我们将不胜感激
谢谢
我发现如果将代码分解成不同的部分,简化代码会容易得多。您基本上有 5 个不同的数据:id
、isLoading
、account
、users
、accountEsmeGroup
.
让我们从 ID 开始。它来自一个可观察的来源。看起来您正在复制到另一个 属性 (this.id
) 只是为了将它用于其他可观察的调用。
这不是“敲出”这个值所必需的(或任何值,大部分时间)。让我们将其定义为可观察对象:
private id$ = this.route.paramMap.pipe(
map(params => params.get('id'))
);
这不是很好吗?很明显 id$
,如果订阅,将发出 id。而且,如果路由参数值发生变化(用户导航到另一个帐户),将发出新的 ID。太棒了!
现在,让我们看看如何定义相互依赖的可观察对象。由于 users
依赖于 id
,我们将这样定义:
private users$ = this.id$.pipe(
switchMap(id => this.accountsService.getAccountUsers(id))
);
此处 users$
将始终代表 id$
上次发出的帐户的用户。 switchMap
处理对 getAccountUsers 的可观察调用的订阅,每当它收到新的 id
时,它都会进行新的调用并发出该结果。惊人的!这是反应性的:如果参数更改 -> id$
发出新的 id -> users$
发出新的用户数组。
account$
的定义也将以 id$
开头。看起来 esmeGroup
可能是 account
的可选 属性;要分配 esmeGroup
属性,我们可以有条件地 return undefined
或 getAccountEsmeGroup()
:
private account$ = this.id$.pipe(
switchMap(id => this.accountsService.get(id)),
switchMap(account => {
const hasTransactionalReportingEnabled = account.features.some(
feature => feature.code === FeatureCode.TRANSACTIONAL_REPORTING
);
const esmeGroup$ = hasTransactionalReportingEnabled
? this.accountsService.getAccountEsmeGroup(account.id)
: of(undefined)
return esmeGroup$.pipe(
map(esmeGroup => ({...account, esmeGroup}))
);
}),
catchError(...)
);
现在,让我们将这些 observable 组合成一个单一的视图模型 observable,供我们的模板使用。而不是使用 forkJoin
, we will use combineLatest
。它们很相似,但是 forkJoin 只会发出一次,而 combineLatest 可以发出多次,这很好,因为如果我们的 id
url 参数发生变化,如果我们的数据自动 (反应性 ) 更新。
public vm$ = combineLatest([this.users$, this.account$]).pipe(
map(([users, account]) => ({users, account}))
);
你有没有注意到我们还没有订阅任何东西?
您是否也注意到我们也不需要 ngOnInit
?
在我看来,这使得组件控制器更加简单。
现在,在模板中,您可以使用 async
管道。我不知道您的模板是什么样的,但这是一个简化的示例:
<ng-container *ngIf="isLoading; else content">
<p>loading...</p>
</ng-container>
<ng-template #content>
<div *ngIf="vm$ | async as vm">
<ul>
<li *ngFor="let user of vm.users">{{ user.name }}</li>
</ul>
<h3>{{ vm.account.name }}</h3>
<p *ngIf="vm.account.esmeGroup as group">{{ group.name }}</p>
</div>
</ng-template>
有几种方法可以处理 isLoading
值。一种简单的方法是使用 tap
在 id$
发出时设置 isLoading = true
并在 vm$
发出时设置 isLoading = false
。
因此,总的来说,一个完整的解决方案可能如下所示:
export class AccountDetailsComponent {
constructor(private route: ActivatedRoute) { }
private isLoading = true;
private id$ = this.route.paramMap.pipe(
map(params => params.get('id')),
tap(() => this.isLoading = true)
);
private users$ = this.id$.pipe(
switchMap(id => this.accountsService.getAccountUsers(id))
);
private account$ = this.id$.pipe(
switchMap(id => this.accountsService.get(id)),
switchMap(account => {
const hasTransactionalReportingEnabled = account.features.some(
feature => feature.code === FeatureCode.TRANSACTIONAL_REPORTING
);
const esmeGroup$ = hasTransactionalReportingEnabled
? this.accountsService.getAccountEsmeGroup(account.id)
: of(undefined);
return esmeGroup$.pipe(
map(esmeGroup => ({...account, esmeGroup}))
);
})
catchError(...)
);
public vm$ = combineLatest([this.users$, this.account$]).pipe(
map(([users, account]) => ({users, account})),
tap(() => this.isLoading = false)
);
}
在我看来,此解决方案可实现您的目标:
rewrite in a better way
有几个好处:
- 没有嵌套订阅
- 每条数据单独定义,方便以后理解和修改
- 代码模板是反应式的;如果我们的任何数据发生变化,视图将自动更新
- 我们将单个可观察对象暴露给我们的视图,然后我们可以使用单个
async
管道轻松使用它;减轻管理任何订阅的需要