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

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,
    }
  ],
};

问题原因分析

  1. 通配符*导致状态重置:

    • 使用*会包含所有状态,包括不需要持久化的FileUploadState,这类状态的临时数据可能引发序列化/反序列化冲突;
    • NGXS存储插件初始化时,异步读取IndexedDB的时序和状态初始化时序不匹配,会导致读取失败,最终回退到默认状态。
  2. 指定具体状态时初始状态不存入:

    • 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 09:49:58