You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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`);
    }
  }

我的疑问:

  1. 当前用空Subject触发列表更新的方式是否最优?不想用NgRx(觉得简单应用太冗余),该怎么优化?
  2. 不想在增删改后重新加载列表并显示加载器,Angular有没有类似React Query的方案?
  3. 即将学习课程中的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);
      }
    });
  }
}

优化点说明:

  • 移除空recipesUpdate Subject,通过更新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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 02:54:56