[翻译] Angular 最佳实践: RxJS 错误处理
RxJS
是 Angular
的一个重要部分. 如果不了解如何正确地使用 RxJS 处理错误,那么当错误发生时,你肯定会遇到一些奇怪的问题.相反,如果你事先知道自己在做什么,你就可以消除这些奇怪的问题,并为自己节省调试的痛苦
本文将研究
- 最需要关注的
RxJS Observables
类型RxJS Infinite Observables
参阅 这篇文章 关于finite Observables
和infinite Observables
的不同, 尽管你可能猜得到
- 如何错误地处理
RxJS
中的错误- 当错误处理不正确时会发生什么
- 如果不处理
RxJS
中的错误,会发生什么 - 如何正确处理
RxJS
中的错误
本文的代码可以在 github
上找到
Infinite Observables
本文将讨论 infinite observables
- 那些你期望能从中一直获取值. 如果你错误的处理错误(do error handling wrong), 它们将不再是infinite observables
, 并且结束 - 这将是非常糟糕的, 因为你的应用程序期望它是infinite
以下这些将会被研究
DOM Event
- 对一个在页面上键入并使用 API 查询的keyup
的DOM Event
进行去抖- NgRx Effect - 期望始终监听已分派的操作的 NgRx Effect
DOM Event
案例研究
第一个案例研究会聚焦于处理 DOM Event
并基于它们进行搜索. 你将在两个输入框中输入《星球大战》的角色的名字. 当你停止输入 300 毫秒之后, 并且输入的内容和上一次的不相同, 将会使用 星球大战 API 搜索这些名字 并且展示. 第一个输入框会在出现错误之后继续工作,第二个输入框会在出现错误后停止工作.
这是界面
我稍微修改一下,如果你输入错误,它会搜索一个错误的 URL,从而产生一个错误
这是相关的 HTML
<input class="form-control" (keyup)="searchTerm$.next($event.target.value)" />
<input class="form-control" (keyup)="searchTermError$.next($event.target.value)" />
keyup
事件 只是简单的使用 Subject
的 next
方法发送数据
这是component
代码
searchTerm$ = new Subject<string>();
searchTermError$ = new Subject<string>();
this.rxjsService
.searchBadCatch(this.searchTermError$)
.pipe(finalize(() => console.log('searchTermError$ (bad catch) finalize called!')))
.subscribe((results) => {
console.log('Got results from search (bad catch)');
this.resultsError = results.results;
});
this.rxjsService
.search(this.searchTerm$)
.pipe(finalize(() => console.log('searchTerm$ finalize called!')))
.subscribe((results) => {
console.log('Got results from search (good catch)');
this.results = results.results;
});
这段代码基本上将向页面发送结果, 并输出日志是否被调用. 注意, 我们调用了两个不同的服务方法, 并传入了两个不同的 Subject
本案例研究的错误处理代码位于 rxjsService
中:
searchBadCatch(terms: Observable<string>) {
return terms.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.searchStarWarsNames(term)),
catchError(error => {
console.log("Caught search error the wrong way!");
return of({ results: null });
})
);
}
search(terms: Observable<string>) {
return terms.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term =>
this.searchStarWarsNames(term).pipe(
catchError(error => {
console.log("Caught search error the right way!");
return of({ results: null });
})
)
)
);
}
private searchStarWarsNames(term) {
let url = `https://swapi.co/api/people/?search=${term}`;
if (term === "error") {
url = `https://swapi.co/apix/people/?search=${term}`;
}
return this.http.get<any>(url);
}
糟糕的错误处理
searchBadCatch
方法实现了糟糕的错误处理. 它看起来没有问题, 对吧?
它在 300 毫秒内去抖动,并且使用distinctUntilChanged
确保我们不会连续两次搜索相同的东西. 在 switchMap
中, 我们使用了searchStarWarsNames
方法,并且使用catchError
方法捕获错误. 这有什么问题吗?
如果你在 Observables 的
pipe
方法的第一层使用catchError
捕捉错误(在本例中是return terms.pipe()
),它将允许你处理错误,并且返回一个或多个结果, 但是它会紧接着终止这个observable stream
(可观察者流). 这意味着它不会再监听keyup
事件. 因此, 无论如何, 绝不允许错误渗透到这一层.
注意, 如果在Observable
的pipe
方法的第一层触达catchError
, finalize
方法将会被调用. 你可以在component
代码中看到这一点.
这里有个可视化代码(visual
), 我希望有所帮助
永远不要让错误渗透到红线的水平.
良好的错误处理
search
方法中实现了 RxJS
错误处理的最佳实践代码
始终将
catchError
操作符 放在 类似switchMap
的方法中, 以便于它只结束 API 调用流,然后将流返回switchMap
中,继续执行Observable
. 如果你没有调用 API,确保添加了try/catch
代码来处理错误,并且不允许它渗透到第一层的pipe
, 不要假设你的代码不会失败, 一定要使用try/catch
.
所以, 你可以在代码中看到我们在 searchStarWarsNames
方法调用中 添加了 pipe
方法,这样我们就可以捕获错误,从而不允许错误渗透到第一层 pipe
这是最佳处理的可视化代码(visual
)
始终在 switchMap
/ mergeMap
/ concatMap
等内部的捕获错误
输出(Output)
现在是时候看看它是如何在网页上工作的.我们假设它一开始是工作的.当 API 调用出错时,就有乐子了.
首先,我将在两个输入框中键入错误,如下所示
我将把它作为练习, 你自己看控制台输出. 现在正式测试,我可以在处理错误后继续输入内容并获得结果吗?
这里我们看到第一个可行,第二个不再可行
第二个输入框出现了正如我开头介绍中所说的奇怪的问题.你将很难弄清楚为什么你的搜索停止工作
NgRx Effect
案例研究
我开始写这篇文章的原因是我在一个应用程序使用 NgRx Effects
出现的奇怪的问题. 有关信息请看这里. 可能是因为我没有在effect
中正确处理 RxJS 错误? 正如你在本研究中所看到的, 答案是肯定的
这是界面
这里没有什么特别的
Success
- 用person/1
(Luke Skywalker
)调用星球大战 API,并在屏幕上输出名称Error – Stops Listening
- 使用错误 URL 调用 API,因此它会生成一个错误 -catch
是错误的,所以它停止监听effect
Error – Don’t catch error
- 使用错误的 URL 调用 API,以便生成错误 - 不捕获错误Error – Keeps Listening
- 使用错误的 URL 调用 API,以便生成错误 - 正确捕获错误,所以可以多次单击它
我会跳过 HTML,因为它只是调用组件方法的按钮. 这是组件代码
ngrxSuccess() {
this.store.dispatch(new CallWithoutError());
}
ngrxError() {
this.store.dispatch(new CallWithError());
}
ngrxErrorKeepListening() {
this.store.dispatch(new CallWithErrorKeepListening());
}
ngrxErrorDontCatch() {
this.store.dispatch(new CallWithErrorNotCaught());
}
好(good),坏(bad)和丑陋(ugly)的错误处理都在 effect
代码中
CallWithoutError Effect
这是我们的成功案例
@Effect()
callWithoutError$ = this.actions$.pipe(
ofType(AppActionTypes.CallWithoutError),
switchMap(() => {
console.log("Calling api without error");
return this.http.get<any>(`https://swapi.co/api/people/1`).pipe(
map(results => results.name),
switchMap(name => of(new SetName(name))),
catchError(error => of(new SetName("Error!")))
);
}),
finalize(() => console.log("CallWithoutError finalize called!"))
);
这个每次都会工作.即使它失败了,它会继续工作,因为 catchError
在 http.get
的 pipe
中. 在这个成功案例,SetName reducer
将向 store
添加name
, 用户界面选择并显示它.
CallWithError Effect
此effect
将使用错误的 URL 调用 API,因此会生成错误. 错误处理操作不正确,因此一旦调用,这将永远不会再次运行,直到刷新应用程序.
@Effect()
callWithError$ = this.actions$.pipe(
ofType(AppActionTypes.CallWithError),
switchMap(() => {
console.log("Calling api with error - stop listening");
return this.http.get<any>(`https://swapi.co/apix/people/1`).pipe(
map(results => results.name),
switchMap(name => of(new SetName(name)))
);
}),
catchError(error => of(new SetName("Error - You're doomed!"))),
finalize(() => console.log("CallWithError finalize called!"))
);
在这种情况下, catchError
会在 this.actions$.pipe
的第一层中别调用, 从而结束 effect
,因为它的 Observable 流将结束. 这就像上面使用 RxJS Observables 的案例研究一样. 点击后我们应该在页面上看到Error – You’re doomed!
. 如果我们再次尝试单击该按钮,则不会触发该effect
.
以下是此输出:
CallWithErrorKeepListening Effect
此effect
将使用错误的 URL 调用 API,因此会生成错误. 但是,它会正确处理错误,以便可以再次调用它.
@Effect()
callWithErrorKeepListening$ = this.actions$.pipe(
ofType(AppActionTypes.CallWithErrorKeepListening),
switchMap(() => {
console.log("Calling api with error - keep listening");
return this.http.get<any>(`https://swapi.co/apix/people/1`).pipe(
map(results => results.name),
switchMap(name => of(new SetName(name))),
catchError(error => of(new SetName("Error but still listening!")))
);
}),
finalize(() => console.log("CallWithErrorKeepListening finalize called!"))
);
处理 RxJS 错误的正确方法是将 catchError
放在 http.get
的pipe
中. 它将结束 http.get
的 observable
,但这并不重要,因为它无论如何都是finite observable
,只发出一个值. 当它返回SetName action
时,switchMap
将emit
并继续 Observable 流. 请注意,此处的finalize
将永远不会被调用.
以下是此输出:
CallWithErrorNotCaught Effect
这是我们的最后一个effect
, 并回答了我们的问题"如果我们没有 catch 错误会发生什么?" 这个问题的答案是它的行为与我们不正确地处理错误的行为相同(因为这就是 RxJS 如何运转). 只是你没有处理(hooking into)那个错误流.
@Effect()
callWithErrorDontCatch$ = this.actions$.pipe(
ofType(AppActionTypes.CallWithErrorNotCaught),
switchMap(() => {
console.log("Calling api with error - don't catch");
return this.http.get<any>(`https://swapi.co/apix/people/1`).pipe(
map(results => results.name),
switchMap(name => of(new SetName(name)))
);
}),
finalize(() => console.log("CallWithErrorNotCaught finalize called!"))
);
此外,由于你没有 在 catchError
中调用 SetName
, 因此不会在 UI 上设置 name. 因此,如果点击第一个按钮,将看不到任何输出,或者将看到上一次设置的 name. 另一个很难调试的“怪异问题”.
最后
正如你在本文中所知,知道如何在 Angular 应用程序中正确处理的 RxJS 错误将帮助你阻止 infinite Observable
意外结束的奇怪的问题. 利用这些知识,你应该能够确保你的infinite Observables
永远不会结束,直到你决定结束它们为止.
原文
文章若有纰漏请大家补充指正,谢谢~~
http://blog.xinshangshangxin.com SHANG 殇
如果你在 Observables 的 pipe 方法的第一层使用 catchError 捕捉错误(在本例中是 return terms.pipe()),它将允许你处理错误,并且返回一个或多个结果, 但是它会紧接着终止这个 observable stream(可观察者流).
既然捕获了错误并且没有继续抛异常,怎么还会中止这个流呢?
或者说如果在这儿 捕捉错误,它将允许你处理错误,并且返回一个或多个结果
会导致中止流,那么在外层进行同样的操作就不会中止么?
如果在Observable的pipe方法的第一层触达catchError, finalize 方法将会被调用
也就是说, 一旦 catchError 触发, terms就会触发 finalize
, 而 finalize
是 complete
或者 error
时触发,也就是 中止这个流
terms.pipe(
catchError(AXXX)
finalize(BXXX)
)