如何在使用Angular进行HMR时保留状态

18
在Angular中,是否有一种方法可以在模块进行热重载后保留应用程序状态?类似于VueJS中发生的情况:

VueJS HMR Example

到目前为止,我已经按照几个教程使HMR工作了,但它只是重新加载应用程序而不进行实际页面刷新。比完全加载更快,是的。但仍然没有达到它可以达到的地步。

有人真正做到了这一点吗?

PS:它与https://github.com/beeman/tutorial-angular-cli-hmr/issues/4有关


1
我认为有一些旧的HMR解决方案,但它们大多数都没有得到维护。 - ForestG
2个回答

10

我尝试了上面Seabass的方法,但在实践中遇到了一些困难。不过,我发现这对我的帮助很大,并且提供了很多信息。使用他的思路,我成功创建了一个新的Angular 6应用程序,并使应用程序状态在HMR构建中持久化。我在Github上创建了一个项目,以便其他人可以下载它进行实验,因为这是最好的学习方式。阅读代码注释并检查控制台日志以了解事物发生的顺序以及它们的工作原理。

https://github.com/ermcgrat/NgStarter

克隆该项目,进行npm安装,然后使用“npm run start”运行应用程序。尝试更改AppComponent中的代码,看看hmr如何工作。

简而言之,我通过创建状态服务并在我的AppModulehmrBootstrap中利用它来实现状态持久化。首先,我从Angular CLI团队指定的基本HMR功能开始:

https://github.com/angular/angular-cli/wiki/stories-configure-hmr

这将使HMR工作,但不会持久保存状态。我扩展了hmr.ts文件,以在模块被卸载(取消加载)时保存我们的状态。当评估新模块时,它将从HMR模块中读取此保存的状态并将其注入到我们的新模块中:

hmr.ts

export const hmrBootstrap = (module: any, bootstrap: () => Promise<NgModuleRef<any>>) => {
  module.hot.accept();

  bootstrap().then(mod => {

    // Attach a dispose handler. When this module is replaced, we will first run this code before
    // evaluating the new module. (eg. running main.ts)
    module.hot.dispose(data => {

      if (mod.instance.hmrOnDestroy) {
        mod.instance.hmrOnDestroy(data);
      }

      const appRef: ApplicationRef = mod.injector.get(ApplicationRef);
      const elements = appRef.components.map(c => c.location.nativeElement);
      const makeVisible = createNewHosts(elements);
      mod.destroy();
      makeVisible();
    });

    // Does this module have an hmrOnInit method for us to run?
    // And is there state data from previous unloaded module to initalize?
    let prevData;
    if (module.hot.data && module.hot.data.appState) {
      prevData = module.hot.data.appState;
    }
    if (mod.instance.hmrOnInit && prevData) {
      mod.instance.hmrOnInit(prevData);
    }

  });
};

这是我们的AppModule,实现了上面使用的hmrOnInithmrOnDestroy方法。值得注意的是,hmrOnInit通过状态服务恢复应用程序状态(如果存在)。

app.module.ts

export class AppModule {

  constructor(private appRef: ApplicationRef, private stateService: AppStateService) { }

  hmrOnInit(prevState: any) {
    if (prevState) {
      this.stateService.saveAppState(prevState);
      // change detection.
      this.appRef.tick();
    }
  }

  hmrOnDestroy(data: any) {
    // Here we will increment our hmrBuilds counter, and then save our state to
    // data (module.hot.data), so that it will be available to the new module.
    const hmrBuilds = this.stateService.getHmrBuilds() + 1;
    this.stateService.saveHmrBuilds(hmrBuilds);
    data.appState = this.stateService.getAppState();
  }
}

最后是AppStateService。唯一比较棘手的是,我们实际上在两种形式中维护应用程序状态。其中之一是普通的JavaScript对象,用于同步访问(这对于HMR重建是必要的,因为模块销毁中的异步函数不能保证在新模块被评估之前完成)。第二个是应用程序状态的可观察版本,以便各个组件可以轻松地观察状态的更改/更新。

app.state.service.ts

export class AppStateService {

  // attach various component states to this object
  // We maintain an object for synchronous use by the HMR, and an Observable for use by the application and its templates.
  private appState: IAppState = { hmrBuilds: 0 };
  private appStateSubject = new BehaviorSubject<IAppState>({ hmrBuilds: 0 });
  public appState$: Observable<IAppState> = this.appStateSubject.asObservable();

  constructor() { }

  public getAppState() {
    return this.appState;
  }

  public getHmrBuilds(): number {
    return this.appState.hmrBuilds ? this.appState.hmrBuilds : 0;
  }

  public saveAppState(newState: IAppState) {
    this.appState = newState;
    this.appStateSubject.next(newState);
  }

  public saveHmrBuilds(buildNum: number) {
    this.appState.hmrBuilds = buildNum;
  }

}

最后,任何应用程序组件现在都可以观察这个 appState$ 并在它们的组件代码或模板中使用它。

我还想指出,这种维护HMR构建之间状态的方法基本上导致了一个唯一真理源。 我认为,像ngrx这样的状态库与这样的东西完全集成。


似乎需要UI状态管理才能使热模块重新加载重新启动应用程序,对吧? - techguy2000

7
很好的问题。经过很多困难,我最终让 webpack hmr 能够正常工作并保留 Angular 状态。我最终实现了一个新的 environment.hmr.ts 文件来禁用生产环境或测试开发服务器上的 hmr。我没有实现建议中的 .hmr 文件,而是将代码留在 main.ts 中。
请考虑这个场景:您更改了文件并保存。Webpack 进行编译,魔法发生了,您的 Angular 应用程序即将被拆除并与新更改交换。
Webpack 在 module['hot']['dispose'] 上调用我们的函数,这反过来会调用上一个应用程序的 AppModule 类中的 OnDestroy,让我们有机会保存状态。
然后 Webpack 加载我们的新应用程序,它启动并在 AppModule 上调用 OnInit,传递给它 module['hot']['data'],这就是我们的状态。 您可以阅读更多关于这些 “hot” 事物的信息。

main.ts

    import { Injectable } from '@angular/core';
@Injectable() export class StateService { private _state: any = {};
constructor() {}
GetState() { return this._state; }
SetState(state) { this._state = state; } }

这里我们可以将之前应用的状态存储在传递为 store 参数的 module['hot']['data'] 中。将此相同的 store 参数传递到新应用的 OnInit(store) 中,使我们能够在新应用程序中维护任何状态对象。

app.module.ts

     export class AppModule {
          constructor(private _state: StateService) { }
OnInit(store) { if (store !== undefined) { this._state.SetState(store.State); } }
OnDestroy(store) { store.State = this._state; } }

这是我的基本状态服务。您可能更喜欢在这里使用 ngrx 来管理状态,但我觉得它对我的项目来说过于复杂了。

state.service.ts

    import { Injectable } from '@angular/core';
    import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Injectable() export class StateService {
public SideNavIsOpen: BehaviorSubject;
constructor() { this.SideNavIsOpen = new BehaviorSubject(false);
}
public SetState(_state: StateService) { this.SideNavIsOpen = _state.SideNavIsOpen; } }
这段代码是Angular框架中的服务声明。它使用BehaviorSubject类来处理侧边栏是否打开的状态,并提供了一个SetState()方法来设置服务的状态。当创建StateService对象时,侧边栏的初始状态为关闭。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接