Search code examples
angulartypescriptservice-workerangular-moduleangular-service-worker

Service worker registration when environment comes from the backend


Angular v14. I’m using https://timdeschryver.dev/blog/angular-build-once-deploy-to-multiple-environments#platformbrowserdynamic implementation to get environment from backend in main.ts and I add it into providers.

To access environment in services I use (this._config.environment.isProduction):

export class AppInitService {
  constructor(
    @Inject(APP_CONFIG) private _config: IAppConfig,
  ) {}
}

But with this implementation I can’t figure how to enable/disable service worker based on environment in app.module.ts.

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ServiceWorkerModule.register('ngsw-worker.js', {
       enabled: true, // should use from providers environment variable
      registrationStrategy: 'registerImmediately',
    })  ]})
export class AppModule {}

As it’s module, I couldn’t find how to inject and access my _config.environment.isProduction boolean.

I tried removing service worker from app.module.ts and registering service worker in main.ts

    platformBrowserDynamic(providers)
      .bootstrapModule(AppModule)
      .then(() => {
          // isProduction is boolean from API response
        if ('serviceWorker' in navigator && isProduction) {
          navigator.serviceWorker.register('ngsw-worker.js');
        }
      })
      .catch((err) => console.log(err));

But then services that depend on service worker (such as SwUpdate for handling updates) give error in browser

NullInjectorError: R3InjectorError(AppModule)[UpdateService -> SwUpdate -> SwUpdate -> SwUpdate]: NullInjectorError: No provider for SwUpdate!

Update.service.ts

@Injectable({
  providedIn: 'root',
})
export class UpdateService {
  constructor(private swUpdate: SwUpdate) {
    if (swUpdate.isEnabled) {
      swUpdate.versionUpdates.subscribe((event) => {
        switch (event.type) {
          case 'VERSION_READY':
            swUpdate.activateUpdate().then(() => {
              document.location.reload();
            });
            break;
        }
      });
    }
  }
}

I register it in App.component.ts constructor

export class AppComponent implements OnInit {
  constructor(
    private updateService: UpdateService,
  ) {
  }

So my question is how do I either:

  1. inject variable from providers into app.module.ts to use in enabled: my_injected_isProduction_boolean keep registering manually in
  2. main.ts but have SwUpdate work in my services

If I keep my registrering in main.ts and in app.modules I disable registration

ServiceWorkerModule.register('ngsw-worker.js', {
  enabled: false,
}),

it does register service worker based on main.ts and loads ok but it seems confusing and not sure if it would brake something along the way. Would want something clear and in one place.


Solution

  • Apparently it's as simple as just using https://angular.io/api/service-worker/SwRegistrationOptions

      imports: [
        ServiceWorkerModule.register('ngsw-worker.js'),
      ],
      providers: [
        {
          provide: SwRegistrationOptions,
          useFactory: (config: ConfigService) => ({enabled: this.config.isEnabled}),
          deps: [ConfigService]
        },
      ],
    

    But I get response from back-end in main.ts before bootstrapModule() is even called, I have created ConfigService which simply exposes my response by injecting InjectionToken and a method returning the result.

    To use your service as APP_INITIALIZER seems to by tricky as ServiceWorkerModule and SwRegistrationOptions are ran on initial bootstrapping using APP_INITIALIZER provider https://github.com/angular/angular/blob/main/packages/service-worker/src/module.ts and I didn't find order in modules/providers to affect execution (my service was awaiting the API result).

    Another way could be enabling serviceWorker all the time but for registrationStrategy utilize observable factory function and return response from a stream when it has to be registered.

      useFactory: (config: ConfigService) => ({
        enabled: true,
        registrationStrategy: () => config.shouldEnableServiceWorker$,
      }),