首页 > 解决方案 > Angular,在 AppRoutingModule 加载之前加载 json 文件

问题描述

我想在 App 启动之前加载一个 json 配置文件,这样我就可以根据配置选项提供一些服务或其他服务。我使用 APP_INITIALIZER 令牌加载它并且一切正常,直到我需要根据相同的配置选项进行条件路由。我试图通过模块中的 APP_INITIALIZER 加载配置文件并将此模块导入 AppRoutingModule 和主 AppModule 确保提供相同的实例以不加载配置文件两次。

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ],
  providers: [
    ConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: (cs: ConfigService, http: HttpClient) => cs.loadConfig(http),
      deps: [ConfigService, HttpClient],
      multi: true
    },
  ]
})
export class CoreModule {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: CoreModule,
      providers: [
        ConfigService
      ]
    };
  }
}

这是配置服务:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable()
export class ConfigService {

  proccesId: number;

  constructor() {}

  loadConfig(http: HttpClient) {
    return () => {
      return (http.get('/assets/app-config.json') as Observable<any>)
        .toPromise()
        .then((config) => {
          this.processId = config.processId
        });
    };
  }
}

这是我在 AppModule 中导入 CoreModule 的方法(在 AppRoutingModule 中以相同的方式导入):

export function OptionServiceFactory (cs: ConfigService, injector: Injector): OptionService {
  if(cs.processId === 1) {
    return injector.get(OneOptionService);
  } else {
    return injector.get(OtherOptionService);
  }
}

@NgModule({
  imports: [
    CoreModule.forRoot(),
  ],
  providers: [       
    OneOptionService,
    OtherOptionService,
    {
      provide: OptionService,
      useFactory: OptionServiceFactory,
      deps: [ConfigService, Injector]
    }],
  bootstrap: [AppComponent]
})
export class AppModule { }

这适用于主 AppModule,应用程序等待 ConfigService 加载 JSON 配置文件,因此我能够在工厂中使用 JSON 填充的 ConfigService。但是 AppRoutingModule 不会等待,因此任何访问 ConfigService 属性的操作都会失败。这是 AppRoutingModule:

const routes: Routes = [];

function routesFactory(cs: ConfigService): Routes {

  let rutas: Routes = [
    { path: '', redirectTo: '/same-path', pathMatch: 'full' }
  ];

  /* This fails because processId is undefined because JSON file has not been loaded jet */
  if(cs.processId === 1) {
    rutas.push({ path: 'same-path', component: OneComponent });
  } else {
    rutas.push({ path: 'same-path', component: OtherComponent });
  }

  return rutas;
}

@NgModule({
  imports: [
    CoreModule.forRoot(),
    RouterModule.forRoot(routes)
  ],
  exports: [RouterModule],
  providers: [
    { provide: ROUTES, multi: true, useFactory: routesFactory, deps: [ConfigService] }
  ]
})
export class AppRoutingModule { }

作为一种解决方法,我进行了同步 http 调用以在 AppRoutingModule 中加载 json 文件。现在 AppRoutingModule 中的 routesFactory 是:

function routesFactory(cs: ConfigService): Routes {

  let rutas: Routes = [
    { path: '', redirectTo: '/same-path', pathMatch: 'full' }
  ];

  /* Syncronous http request */
  var request = new XMLHttpRequest();
  request.open('GET', '/assets/app-config.json', false);
  request.send(null);

  if (request.status === 200) {
    let config = JSON.parse(request.responseText);
    cs.processId = config.processId;
  }
  
  /* Now it doesn't fail because we loaded json syncronously */
  if(cs.processId === 1) {
    rutas.push({ path: 'same-path', component: OneComponent });
  } else {
    rutas.push({ path: 'same-path', component: OtherComponent });
  }

  return rutas;
}

它有效,但不鼓励这样做,现在我在浏览器控制台中收到警告。

[弃用] 主线程上的同步 XMLHttpRequest 已弃用,因为它对最终用户的体验产生不利影响。如需更多帮助,请查看https://xhr.spec.whatwg.org/

有没有办法在 AppRoutingModule 加载之前加载 JSON 文件?

提前致谢

标签: angulartypescriptdependency-injectionxmlhttprequest

解决方案


Romain Manni-Bucau在他的博客文章https://rmannibucau.metawerx.net/angular-ng-10-create-update-route-at-runtime.html中解释了如何以编程方式加载和更新路由

我在这里复制基本步骤,以防博客不可用。

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router, Routes } from '@angular/router';
import { from } from 'rxjs';
import { switchMap, map } from 'rxjs/operators';

@Injectable({
    providedIn: 'root',
})
export class RouteLoader {
    constructor(
        private client: HttpClient,
        private router: Router) /* 1 */ { }

    public load() {
        return this.client.get('/my-routes')
            .pipe(switchMap(json => this.createRoutes(json)));
    }

    private createRoutes(json) {
        return from(json.routeSpecs).pipe( // 2
            map(spec => this.toRoutes(spec)), // 3
            map(routes => ([ // 4
                ...this.router.config,
                ...routes,
            ])),
            map(newRoutes => this.router.resetConfig(newRoutes)) // 5
        );
    }

}
  1. 我们在服务构造函数中注入路由,

  2. 一旦我们检索到我们的后端模型,我们就会从中创建一个 observable(使 DSL 更容易,并更好地与 load() 函数集成,我们很快就会看到,但如果您愿意,可以使用普通循环来完成),

  3. 我们将后端路由规范转换为实际路由(我们将在之后解释),

  4. 我们用新的路线扁平化现有/引导路线,

  5. 最后,我们用新路由更新路由器。请注意,这也是您可以存储您加载的路线并将它们存储在 RouteLoader 服务中的地方。如果您从获取的内容动态构建菜单,并且它可以将服务注入任何组件并动态创建菜单(例如使用 *ngFor),而无需使用全局变量(作为原始提取),这将非常有用选项将需要)。

       providers: [ // 1
         {
           provide: APP_INITIALIZER,
           useFactory: ensureRoutesExist, // 2
           multi: true,
           deps: [HttpClient, RouteLoader], // 3
         }
       ],
       bootstrap: [AppComponent]
     })
     export class AppModule { }
    

1.我们使用令牌 APP_INITIALIZER 注册一个自定义提供程序(确保将 multi 设置为 true,因为您可以为每个应用程序获取多个初始化程序),

2.我们将其工厂绑定到自定义方法,

3.我们绑定工厂参数——在这种情况下这是一个普通的函数,需要这个显式的注入绑定。

export function ensureRoutesExist( // 1
    http: HttpClient,
    routeLoader: RouteLoader) {
  return () => routeLoader.load() // 2
    .toPromise(); // 3
}
  1. 工厂是一个普通函数,它返回一个返回承诺的函数,它将我们在提供者注册期间绑定在应用程序模块中的服务/提供者作为参数,

  2. 我们想等待路由加载,所以我们在应用程序初始化期间调用 load 函数来执行它,

  3. 我们将 observable 转换为 Promise,以确保 Angular 在完成开始之前等待此逻辑。


推荐阅读