Angular无NgRx简易CRUD应用优化及相关技术疑问
问题描述
我正在学习Angular,跟着Maximilian Schwarzmüller的Udemy课程开发一个简易食谱管理CRUD应用,数据存储在数据库中,需要通过HTTP请求实现食谱的增删改并同步更新食谱列表。
目前我用RxJS BehaviorSubject和Service实现了删除功能,代码如下:
RecipeDetailComponent
export class RecipeDetailComponent implements OnInit { selectedRecipe: Recipe; constructor( private recipeService: RecipeService, private activatedRoute: ActivatedRoute, private router: Router ) { } ngOnInit() { this.activatedRoute.params.subscribe((params) => { const id = params.id; this.recipeService.getRecipeById(id).subscribe((recipe) => { this.selectedRecipe = recipe; }); }); } onDeleteRecipe() { this.recipeService.deleteRecipe(this.selectedRecipe.id).subscribe({ next: () => { this.router.navigate(['/recipes']); this.recipeService.recipesUpdate.next(); }, error: (error) => { console.error('error deleting recipe : ', error); } }); } }
RecipeListComponent
export class RecipeListComponent implements OnInit { recipes$: Observable<Recipe[]>; isLoading = false; errorMessage: string; constructor(private recipeService: RecipeService) {} ngOnInit() { this.initRecipes(); this.recipeService.recipesUpdate.subscribe(() => this.initRecipes()); } initRecipes() { this.isLoading = true; this.recipes$ = this.recipeService.getRecipes().pipe( catchError((error) => { console.error('error retrieving recipes : ', error); this.errorMessage = `Error retrieving recipes : ${error.error.error}`; return of([]); }), tap({ complete: () => (this.isLoading = false) }) ); } }
RecipeService
export class RecipeService { API_URL = 'XXX'; private recipes: Recipe[] = []; recipesUpdate: Subject<void> = new Subject<void>(); recipes$ = new BehaviorSubject<Recipe[]>(this.recipes); constructor(private http: HttpClient) { } getRecipes() { return this.http.get<Recipe[]>(`${this.API_URL}/recipes.json`) } getRecipeById(id: string) { return this.http.get<Recipe>(`${this.API_URL}/recipes/${id}.json`) } addRecipe(recipe: Recipe) { return this.http .post(`${this.API_URL}/recipes.json`, recipe) .subscribe((response) => { this.recipesUpdate.next(); }); } updateRecipe(recipe: Recipe) { return this.http .put(`${this.API_URL}/recipes/${recipe.id}.json`, recipe) .subscribe((response) => { this.recipesUpdate.next(); }); } deleteRecipe(id: string) { return this.http.delete(`${this.API_URL}/recipes/${id}.json`); } }
我的疑问:
- 当前用空Subject触发列表更新的方式是否最优?不想用NgRx(觉得简单应用太冗余),该怎么优化?
- 不想在增删改后重新加载列表并显示加载器,Angular有没有类似React Query的方案?
- 即将学习课程中的NgRx部分,值得学吗?NgRx的学习价值是什么?
问题解答
一、当前实现的优化方案(无需NgRx)
当前实现可用,但可以通过直接维护Service内的状态流避免重复请求和空Subject的触发,核心思路是让recipes$始终保持最新数据源,增删改操作后直接更新该流,而非重新发起请求:
优化后的RecipeService
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, catchError, map, of, switchMap, tap } from 'rxjs'; import { Recipe } from './recipe.model'; @Injectable({ providedIn: 'root' }) export class RecipeService { private API_URL = 'XXX'; // 用BehaviorSubject维护最新食谱列表状态 private _recipes$ = new BehaviorSubject<Recipe[]>([]); // 对外暴露只读Observable,防止外部直接修改状态 recipes$ = this._recipes$.asObservable(); constructor(private http: HttpClient) { // 初始化时加载一次数据 this.loadRecipes(); } // 加载食谱列表并更新内部状态 private loadRecipes() { this.http.get<Recipe[]>(`${this.API_URL}/recipes.json`).pipe( catchError(error => { console.error('加载食谱失败:', error); return of([]); }) ).subscribe(recipes => { this._recipes$.next(recipes); }); } getRecipeById(id: string) { // 优先从本地状态获取,不存在再请求API(可根据需求调整) const existingRecipe = this._recipes$.value.find(r => r.id === id); if (existingRecipe) { return of(existingRecipe); } return this.http.get<Recipe>(`${this.API_URL}/recipes/${id}.json`); } addRecipe(recipe: Recipe) { return this.http.post(`${this.API_URL}/recipes.json`, recipe).pipe( switchMap((response: any) => { // 假设返回结果包含新食谱ID,更新本地状态 const newRecipe = { ...recipe, id: response.name }; const updatedRecipes = [...this._recipes$.value, newRecipe]; this._recipes$.next(updatedRecipes); return of(newRecipe); }), catchError(error => { console.error('添加食谱失败:', error); return of(null); }) ); } updateRecipe(recipe: Recipe) { return this.http.put(`${this.API_URL}/recipes/${recipe.id}.json`, recipe).pipe( tap(() => { // 替换本地状态中的旧食谱 const updatedRecipes = this._recipes$.value.map(r => r.id === recipe.id ? recipe : r ); this._recipes$.next(updatedRecipes); }), catchError(error => { console.error('更新食谱失败:', error); return of(null); }) ); } deleteRecipe(id: string) { return this.http.delete(`${this.API_URL}/recipes/${id}.json`).pipe( tap(() => { // 从本地状态移除对应食谱 const updatedRecipes = this._recipes$.value.filter(r => r.id !== id); this._recipes$.next(updatedRecipes); }), catchError(error => { console.error('删除食谱失败:', error); return of(null); }) ); } }
优化后的RecipeListComponent
无需再订阅空Subject,直接订阅Service的recipes$即可,加载状态通过tap处理:
import { Component, OnInit } from '@angular/core'; import { Observable, tap } from 'rxjs'; import { Recipe } from '../recipe.model'; import { RecipeService } from '../recipe.service'; @Component({ selector: 'app-recipe-list', templateUrl: './recipe-list.component.html', styleUrls: ['./recipe-list.component.css'] }) export class RecipeListComponent implements OnInit { recipes$: Observable<Recipe[]>; isLoading = false; errorMessage: string; constructor(private recipeService: RecipeService) {} ngOnInit() { this.isLoading = true; this.recipes$ = this.recipeService.recipes$.pipe( tap({ next: () => { this.isLoading = false; this.errorMessage = ''; }, error: (err) => { this.isLoading = false; this.errorMessage = '加载食谱失败'; } }) ); } }
优化后的RecipeDetailComponent
删除后无需手动触发更新,Service已自动维护状态:
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Recipe } from '../recipe.model'; import { RecipeService } from '../recipe.service'; @Component({ selector: 'app-recipe-detail', templateUrl: './recipe-detail.component.html', styleUrls: ['./recipe-detail.component.css'] }) export class RecipeDetailComponent implements OnInit { selectedRecipe: Recipe; constructor( private recipeService: RecipeService, private activatedRoute: ActivatedRoute, private router: Router ) { } ngOnInit() { this.activatedRoute.params.subscribe((params) => { const id = params.id; this.recipeService.getRecipeById(id).subscribe((recipe) => { this.selectedRecipe = recipe; }); }); } onDeleteRecipe() { this.recipeService.deleteRecipe(this.selectedRecipe.id).subscribe({ next: () => { this.router.navigate(['/recipes']); }, error: (error) => { console.error('删除食谱失败:', error); } }); } }
优化点说明:
- 移除空
recipesUpdateSubject,通过更新Service内的_recipes$状态流同步所有组件 - 增删改操作后直接修改本地状态,无需重新请求整个列表,避免加载器重复显示
- 组件只需订阅
recipes$即可获取最新数据,无需手动调用initRecipes
二、Angular中类似React Query的方案
Angular生态中有多个成熟库可替代React Query,解决数据缓存、自动同步、避免重复请求的问题:
- Angular Query(@tanstack/angular-query):React Query的Angular版本,API几乎一致,支持缓存、背景刷新、自动重试等功能,完美匹配需求
- NgRx Data:NgRx衍生库,针对CRUD场景封装大量逻辑,比原生NgRx简洁,适合需要状态管理但不想写大量样板代码的场景
- Akita:轻量级状态管理库,内置CRUD和缓存支持,学习曲线比NgRx平缓
其中Angular Query是最接近React Query的选择,使用示例:
// 在AppModule中配置Angular Query import { provideAngularQuery } from '@tanstack/angular-query'; @NgModule({ providers: [provideAngularQuery()] }) export class AppModule {} // 在RecipeService中使用Angular Query import { injectQueryClient, useQuery, useMutation } from '@tanstack/angular-query'; export class RecipeService { private API_URL = 'XXX'; private queryClient = injectQueryClient(); // 获取食谱列表,自动缓存 getRecipes() { return useQuery({ queryKey: ['recipes'], queryFn: () => this.http.get<Recipe[]>(`${this.API_URL}/recipes.json`), }); } // 删除食谱的Mutation,自动更新缓存 deleteRecipe() { return useMutation({ mutationFn: (id: string) => this.http.delete(`${this.API_URL}/recipes/${id}.json`), onSuccess: () => { // 使食谱列表缓存失效,自动重新获取(或直接更新缓存) this.queryClient.invalidateQueries({ queryKey: ['recipes'] }); }, }); } }
使用Angular Query后,无需手动维护状态流,它会自动处理缓存、更新、加载状态等,无需手动调用刷新列表。
三、NgRx的学习价值
值得学习,尤其是想深入Angular生态或从事中大型Angular项目开发时:
- 标准化状态管理:NgRx提供统一的状态管理模式,适合多人协作的大型项目,避免状态管理混乱
- 生态集成:Angular官方推荐的状态管理方案,与Angular Router、Forms等模块集成度高
- 调试工具:NgRx DevTools可直观追踪状态变化,定位问题更高效
- 技能竞争力:多数企业级Angular项目会使用NgRx,掌握它能提升求职竞争力
当然,小型应用中NgRx确实存在样板代码过多的问题,但学习它能帮助理解单向数据流、状态不可变性等核心概念,这些概念在所有前端框架中通用。如果项目未来可能扩展,提前了解NgRx的设计思路也会有帮助。
内容的提问来源于stack exchange,提问作者Clem_Fr_3
相关产品推荐
相关产品推荐

