Angular中NGXS IndexedDB持久化插件异常问题排查
Angular NGXS 结合 IndexedDB 持久化问题解决
问题描述
在Angular项目中使用idb(IndexedDB)作为NGXS Store的持久化存储插件时,遇到两个核心问题:
- 使用
*通配符表示所有状态键时,页面刷新后状态会被重置为默认值; - 指定具体状态(如代码中
storedStates数组包含的状态类)时,初始默认状态不会自动存入IndexedDB,导致后续状态变更无法正常生效。
相关代码
indexedDB.persistence.ts
import { Injectable } from '@angular/core'; import { ICN_INDEXEDDB } from '@igus/icn-ui'; import type { StorageEngine } from '@ngxs/storage-plugin'; import type { DBSchema, IDBPDatabase } from 'idb'; import { openDB } from 'idb'; /** * NGXS Indexed DB Store Interface */ interface StoreBD extends DBSchema { keyval: { key: string; value: unknown; }; } const KEY_VAL = 'keyval'; /** * Indexed DB Storage Engine */ @Injectable() export class IndexedDBPersistence implements StorageEngine { /** * Databased Store */ readonly #indexedDB: Promise<IDBPDatabase<StoreBD>>; constructor() { this.#indexedDB = openDB<StoreBD>(ICN_INDEXEDDB, 1, { upgrade: (db) => { db.createObjectStore(KEY_VAL); }, }); } /** * Get items from storage * * @param key key of item */ async getItem(key: string): Promise<unknown> { const db = await this.#indexedDB; return await db.get(KEY_VAL, key); } /** * Store item in storage * * @param key key of item * @param val value to store */ async setItem(key: string, val: unknown): Promise<void> { const db = await this.#indexedDB; await db.put(KEY_VAL, val, key); } }
ngxs.config.ts
import type { EnvironmentProviders } from '@angular/core'; import { ConfigurationState, TechnicalDrawingState } from '@igus/icn-app-state'; import { FileUploadState } from '@igus/icn-feature-file-upload'; import { MaterialState } from '@igus/icn-feature-material'; import { CadState } from '@igus/icn-feature-models'; import { withNgxsReduxDevtoolsPlugin } from '@ngxs/devtools-plugin'; import { withNgxsLoggerPlugin } from '@ngxs/logger-plugin'; import { withNgxsRouterPlugin } from '@ngxs/router-plugin'; import { withNgxsStoragePlugin } from '@ngxs/storage-plugin'; import { provideStore } from '@ngxs/store'; import { withNgxsWebSocketPlugin } from '@ngxs/websocket-plugin'; import { environment } from '../environments/environment'; /** * @description States to be persisted in the browser * FileUploadState is excluded so that users don't get stuck on 'processing' when an error occurs * even after refresh */ const storedStates = [ CadState, MaterialState, ConfigurationState, TechnicalDrawingState, ]; /** * Configuration for NGXS */ export const ngxsConfig: EnvironmentProviders = provideStore( [...storedStates, FileUploadState], withNgxsWebSocketPlugin({ url: environment.icnDataServiceWsUrl, typeKey: 'event', // NestJS websockets uses 'event' key }), withNgxsStoragePlugin({ keys: storedStates, }), withNgxsRouterPlugin(), withNgxsReduxDevtoolsPlugin({ disabled: environment.production, }), withNgxsLoggerPlugin({ disabled: environment.production, }), );
app.config.ts
import type { ApplicationConfig } from '@angular/core'; import { APP_INITIALIZER, ErrorHandler, importProvidersFrom, } from '@angular/core'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideRouter, Router, withInMemoryScrolling } from '@angular/router'; import { STORAGE_ENGINE } from '@ngxs/storage-plugin'; import * as Sentry from '@sentry/angular-ivy'; import { Angulartics2RouterlessModule } from 'angulartics2'; import { environment } from '../environments/environment'; import { CONTACT_ID, ICN_APP_VERSION, KOPLA_APP_VERSION, ROUTES, } from './app.di'; import { appRoutes } from './app.routes'; import { ngxsConfig } from './ngxs.config'; import { IndexedDBPersistence } from './services'; export const appConfig: ApplicationConfig = { providers: [ ngxsConfig, provideRouter( appRoutes, withInMemoryScrolling({ anchorScrolling: 'enabled' }), ), provideAnimations(), importProvidersFrom( FileUploadModule.forRoot({ fileUploadApi: environment.koplaUploadUrl, }), ), { provide: ErrorHandler, useValue: Sentry.createErrorHandler({ showDialog: false, }), }, { provide: Sentry.TraceService, deps: [Router], }, { provide: APP_INITIALIZER, useFactory: () => () => ({}), deps: [Sentry.TraceService], multi: true, }, { provide: CONTACT_ID, useValue: contactId, }, { provide: KOPLA_APP_VERSION, useValue: KOPLA_APP_VERSION_PLACEHOLDER, }, { provide: ROUTES, useValue: ROUTE_LIST, }, { provide: STORAGE_ENGINE, useClass: IndexedDBPersistence, } ], };
问题原因分析
通配符
*导致状态重置:- 使用
*会包含所有状态,包括不需要持久化的FileUploadState,这类状态的临时数据可能引发序列化/反序列化冲突; - NGXS存储插件初始化时,异步读取IndexedDB的时序和状态初始化时序不匹配,会导致读取失败,最终回退到默认状态。
- 使用
指定具体状态时初始状态不存入:
- NGXS存储插件默认仅在状态发生主动变更时才触发持久化写入,初始默认状态不会自动存入存储引擎;
- 首次加载时IndexedDB无对应状态记录,插件读取不到数据就会使用默认值,后续若无状态变更则不会写入,刷新后依然是默认状态。
解决方案
1. 修复通配符*的使用问题
如果坚持使用*,需排除不需要持久化的状态,并补全存储引擎的缺失方法:
// 在ngxs.config.ts的withNgxsStoragePlugin配置中添加exclude withNgxsStoragePlugin({ keys: '*', exclude: [FileUploadState] // 排除不需要持久化的状态 }),
补全IndexedDBPersistence的缺失方法:
// 在IndexedDBPersistence类中添加 async removeItem(key: string): Promise<void> { const db = await this.#indexedDB; await db.delete(KEY_VAL, key); } async clear(): Promise<void> { const db = await this.#indexedDB; await db.clear(KEY_VAL); }
2. 主动存入初始默认状态
方式一:通过APP_INITIALIZER在应用启动时写入
修改app.config.ts,添加初始化逻辑写入初始状态:
// 导入Store import { Store } from '@ngxs/store'; // 在providers数组中添加 { provide: APP_INITIALIZER, useFactory: (store: Store, indexedDB: IndexedDBPersistence) => async () => { const storedStateClasses = [CadState, MaterialState, ConfigurationState, TechnicalDrawingState]; for (const stateClass of storedStateClasses) { // 获取状态的当前初始值 const stateValue = store.selectSnapshot(stateClass); // NGXS默认用状态类的name作为存储键,若状态类有static key属性则用该属性 const storageKey = (stateClass as any).key || stateClass.name; await indexedDB.setItem(storageKey, stateValue); } }, deps: [Store, IndexedDBPersistence], multi: true, },
方式二:触发初始状态更新
在存储插件配置中启用递归初始化,并在状态类中触发初始更新:
// ngxs.config.ts中修改存储插件配置 withNgxsStoragePlugin({ keys: storedStates, initRecursively: true }),
在需要持久化的状态类中添加初始化动作:
import { State, StateContext, Action, InitState } from '@ngxs/store'; @State<YourStateType>({ name: 'cad', defaults: yourDefaultState }) export class CadState { @Action(InitState) init(ctx: StateContext<YourStateType>) { // 获取当前初始状态并重新patch,触发存储插件写入 const currentState = ctx.getState(); ctx.patchState(currentState); } }
最后在AppComponent的ngOnInit中分发InitState动作,触发初始状态写入。
内容的提问来源于stack exchange,提问作者Benjiro
相关产品推荐
相关产品推荐

