Angular懒加载Shop模块时BehaviorSubject值无法被组件识别的问题
Angular懒加载模块下购物车服务状态丢失问题
我用Angular实现购物车功能,商品可通过购物车页面或商品详情页的「ADD TO CART」按钮加入购物车(两者共用同一个ShoppingCartService)。预加载ShoppingCartModule和包含商品详情组件的ShopModule时功能正常,但懒加载ShopModule时,商品详情组件调用ShoppingCartService无法获取BehaviorSubject保存的现有购物车值,会重新初始化购物车而非更新现有购物车。
购物车服务(ShoppingCartService)
import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { BehaviorSubject } from "rxjs"; import { take } from "rxjs/operators"; import { ICartItem } from "../../../app/shared/Interfaces/ICartItem"; import { IOrderSummary } from "../../../app/shared/Interfaces/IOrderSummary"; import { IProduct } from "../../../app/shared/Interfaces/IProduct"; import { ShoppingCart } from "../../../app/shared/Models/ShoppingCart"; @Injectable() export class ShoppingCartService { baseUrl = "https://localhost:44330/api/"; constructor(private http:HttpClient) { } //shoppingCart Observable private _shoppingCartObservable = new BehaviorSubject<ShoppingCart | null>(null); public shoppingCartObservable$ = this._shoppingCartObservable.asObservable(); //orderSummaryObservable private _orderSummaryObservable = new BehaviorSubject<IOrderSummary | null>(null); public orderSummaryObservable$ = this._orderSummaryObservable .asObservable(); getShoppingCart(shoppingCartId:string){ this.http.get<ShoppingCart>(this.baseUrl+"GetShoppingCartById?id="+shoppingCartId).subscribe( {next : shoppingCart =>{this._shoppingCartObservable.next(shoppingCart); this.setOrderSummary(shoppingCart); } }) } setShoppingCart(shoppingCart:ShoppingCart){ this.http.post<ShoppingCart>(this.baseUrl+"UpdateShoppingCart", shoppingCart).subscribe( {next : shoppingCart => {this._shoppingCartObservable.next(shoppingCart) this.setOrderSummary(shoppingCart); } } )} getCurrentShoppingCart(){ return this._shoppingCartObservable.value; //.value returns the last/current value of the Behavioral Event } addItemToCart(item: IProduct | ICartItem , qty:number=1){ if ((item as ICartItem).brand ===undefined) item = this.mapProductToCartItem( (item as IProduct) ); var shoppingCart = this.getCurrentShoppingCart(); if(shoppingCart==null){ shoppingCart = this.createShoppingCart(); } shoppingCart.items = this.AddOrUpdateCartItem(shoppingCart.items, item as ICartItem, qty); this.setShoppingCart(shoppingCart); } removeFromCart(itemId:number, qty:number=1){ let shoppingCart = this.getCurrentShoppingCart(); if(shoppingCart==null) return; let itemToRemove = shoppingCart.items.find(i=>i.id===itemId); if(itemToRemove){ itemToRemove.qty -= qty; if(itemToRemove.qty <=0) shoppingCart.items = shoppingCart.items.filter(i=>i.id !== itemId); } if(shoppingCart.items.length <=0){ this.removeShoppingCart(shoppingCart) }else{ this.setShoppingCart(shoppingCart); } } //private helper methods private mapProductToCartItem(item:IProduct):ICartItem{ return { id : item.id, productName: item.productName, price: item.salesPrice, qty: 0, pictureUrl:item.pictureUrl, brand: item.brandName, type: item.categoryName }; }; private createShoppingCart(){ const shoppingCart = new ShoppingCart(); localStorage.setItem("shoppingCartId",shoppingCart.id); return shoppingCart; } private AddOrUpdateCartItem(shoppingCartItems:ICartItem[], cartItem:ICartItem, qty:number){ const cartItemToAddOrUpdate = shoppingCartItems.find(i=>i.id===cartItem.id); if(cartItemToAddOrUpdate){ cartItemToAddOrUpdate.qty += qty; } else{ cartItem.qty = qty; shoppingCartItems.push(cartItem); } return shoppingCartItems; } private setOrderSummary(shoppingCart:ShoppingCart){ if(!shoppingCart) return; let totalqty = 0; let totalPrice = 0; let shipping = 0; shoppingCart.items.forEach(item=>{ totalqty += item.qty; totalPrice += item.qty * item.price; } ) let orderSummary:IOrderSummary = { shipping: shipping, totalPrice: totalPrice, subTotal: totalPrice + shipping, totalQty : totalqty } this._orderSummaryObservable.next(orderSummary); } private removeShoppingCart(shoppingCart:ShoppingCart){ this.http.delete(this.baseUrl+"DeleteShoppingCart?id="+shoppingCart.id); this._shoppingCartObservable.next(null); this._orderSummaryObservable.next(null); localStorage.removeItem("shoppingCartId") } }
商品详情组件(ProductDetailsComponent)
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { IProduct } from 'src/app/shared/Interfaces/IProduct'; import { ProductService } from 'src/app/core/Services/ProductService'; import { ShoppingCartService } from 'src/app/core/Services/ShoppingCartService'; @Component({ selector: 'app-product-details', templateUrl: './product-details.component.html', styleUrls: ['./product-details.component.scss'] }) export class ProductDetailsComponent implements OnInit { product:IProduct; productId:number; constructor(private route:ActivatedRoute, private _productsService:ProductService, private _shoppingCartService:ShoppingCartService) { } ngOnInit(): void { this.route.params.subscribe( (params:Params)=>{ this.productId = params['id']; }) this.getProduct(); } getProduct(){ this._productsService.getProductById(this.productId).subscribe((data)=>{ this.product = data; }) } addToCart(){ if(this.product){ this._shoppingCartService.addItemToCart(this.product); console.log('added from productdetails'); } } }
Shop模块(ShopModule)
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ProductDetailsComponent } from './product-details/product-details.component'; import { ProductItemComponent } from './product-item/product-item.component'; import { ShopPageComponent } from './shop-page/shop-page.component'; import { SHAREDModule } from '../shared/shared.module'; import { RouterModule, Routes } from '@angular/router'; const routes:Routes=[ {path:'',component:ShopPageComponent}, {path:':id',component:ProductDetailsComponent} ] @NgModule({ declarations: [ProductDetailsComponent, ProductItemComponent, ShopPageComponent], imports: [ CommonModule, SHAREDModule, RouterModule.forChild(routes) ], exports:[RouterModule] }) export class ShopModule { }
应用路由模块(AppRoutingModule)
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HomePageComponent } from './home/home-page/home-page.component'; const routes: Routes = [ {path:'',component:HomePageComponent}, {path:"Shop", loadChildren:()=>import('./Shop/shop.module').then(m=>m.ShopModule)}, {path:"Account", loadChildren:()=>import('./authentication/authentication.module').then(m=>m.AuthenticationModule)} //{path:'**',redirectTo:"/",pathMatch:'full'} ]; @NgModule({ imports: [RouterModule.forRoot(routes) ], exports: [RouterModule] }) export class AppRoutingModule { }
应用模块(AppModule)
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import {HttpClientModule} from '@angular/common/http' import { AppRoutingModule } from './app-routing.module'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppComponent } from './app.component'; import { OrderModule } from './order/order.module'; import { HomeModule } from './home/home.module'; import { ShoppingCartModule } from './ShoppingCart/shopping-cart.module'; import { COREModule } from './core/core.module'; import { SHAREDModule } from './shared/shared.module'; import { ShopModule } from './Shop/shop.module'; @NgModule({ declarations: [ AppComponent, ], imports: [ BrowserModule, BrowserAnimationsModule, HttpClientModule, AppRoutingModule, OrderModule, HomeModule, ShoppingCartModule, COREModule, SHAREDModule, ], providers: [ ], bootstrap: [AppComponent] }) export class AppModule { }
应用根组件(AppComponent)
import { Component, OnInit } from '@angular/core'; import { AccountService } from './core/Services/account.service'; import { ShoppingCartService } from './core/Services/ShoppingCartService'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { cardId : string; token : string; title:string = 'Coffee Corner'; ngOnInit(): void { this.getCurrentCart(); this.getCurrentUser(); } constructor(private _shoppingCartService:ShoppingCartService, private _accountService:AccountService) { } getCurrentCart(){ this.cardId = localStorage.getItem("shoppingCartId") if(this.cardId!=null){ this._shoppingCartService.getShoppingCart(this.cardId); } } getCurrentUser(){ this.token = localStorage.getItem("token"); this._accountService.loadCurrentUser(this.token).subscribe(); } }
问题根源
你的ShoppingCartService没有被设置为全局单例。Angular中,懒加载模块会创建独立的注入器,如果懒加载模块的依赖链中能找到ShoppingCartService的提供方,就会生成新的服务实例,导致和AppComponent中使用的不是同一个实例,自然无法共享BehaviorSubject保存的购物车状态。
具体来看:
ShoppingCartService的@Injectable()装饰器没有指定providedIn: 'root',说明服务的提供方是在某个模块中声明的。- 你在
AppModule中导入了COREModule,假设ShoppingCartService是在COREModule中提供的,但如果COREModule没有设计成单例模式,或者ShopModule也导入了COREModule,就会创建多个服务实例。 - 另外,如果
ShopModule导入的SHAREDModule也提供了ShoppingCartService,同样会触发新实例的创建。
修复方案
1. 将服务设置为全局单例(推荐)
直接修改ShoppingCartService的@Injectable装饰器,指定providedIn: 'root',Angular会自动将其注册为全局单例,所有模块(无论预加载还是懒加载)都会共享同一个实例:
@Injectable({ providedIn: 'root' }) export class ShoppingCartService { // ... 原有代码保持不变 }
2. 确保COREModule是单例模块(若坚持用模块提供服务)
如果需要通过模块来提供服务,需将COREModule设计为单例模块,使用forRoot()模式避免重复导入:
- 修改
COREModule:
import { NgModule, Optional, SkipSelf } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ShoppingCartService } from './Services/ShoppingCartService'; @NgModule({ imports: [CommonModule], declarations: [] }) export class COREModule { // 防止重复导入 constructor(@Optional() @SkipSelf() parentModule: COREModule) { if (parentModule) { throw new Error('COREModule已加载,请仅在AppModule中导入一次'); } } // 提供服务的单例入口 static forRoot(): ModuleWithProviders<COREModule> { return { ngModule: COREModule, providers: [ShoppingCartService] }; } }
- 在
AppModule中导入COREModule.forRoot(),而非直接导入COREModule:
// AppModule的imports数组中替换原有COREModule COREModule.forRoot(),
- 确保所有懒加载模块(如
ShopModule)不要导入COREModule。
3. 检查懒加载模块的依赖链
确认ShopModule及其导入的模块(如SHAREDModule)没有重复提供ShoppingCartService,避免触发新实例的创建。
内容的提问来源于stack exchange,提问作者Omar Afify
相关产品推荐
相关产品推荐

