skip to Main Content

I’m trying to setup Progressive Web App in my Angular 5 project. I’m also using Angular Universal for server side rendering.

I have a problem with caching data from API. I did a Rest API which looks like https://example.net/getContent/param_1/param_2/param_3. Where param_1 is page name from route param, param_2 is a lang url, param_3 is a lang code. In ngsw-config.json I’m doing it like :

  "dataGroups": [{
    "name": "api-performance",
    "urls": [
      "https://example.net/getMenus/**",
      "https://example.net/getContent/**",
      "https://example.net/getLayout/**",
      "https://example.net/getFooter/**"
    ],
    "cacheConfig": {
      "maxSize": 10000,
      "maxAge": "3d",
      "strategy": "performance"
    }
  }]

I think it should cache every requests like “https://example.net/getMenus/anything/anything/anything/” but it don’t. I can’t run application offline, service worker don’t preload all pages data before. How to make it work ? How to preload all api calls from all pages ? Maybe dynamic api calls are making problem ?

Here is my code from SW and example component.

app.module

// Core
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { RouterModule, Routes, PreloadAllModules } from '@angular/router';
import { ServiceWorkerModule } from '@angular/service-worker';

// Guards
import { AuthGuard } from './guards/auth.guard.service';

// Resolvers
import { LayoutResolver } from './resolvers/layout.resolver.service';

// Config
import { Config } from './config';


// Compontents
import { AppComponent } from './app.component';
import { ContainerComponent } from './container/container.component'
import { FooterComponent } from './footer/footer.component'


// Modules
import { MenuModule } from './menu/menu.module';
import { ContainerModule } from './container//container.module'


// Environment
import { environment } from '../environments/environment';




  const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    component: ContainerComponent,
    canActivate: [AuthGuard],

  },
  {
    path: ':lang',
    component: ContainerComponent,
    resolve: { layout : LayoutResolver }
  },
  {
    path : ':lang/:index',
    component: ContainerComponent,
    resolve: { layout : LayoutResolver }
  }
];


@NgModule({
  declarations: [
    AppComponent,
    FooterComponent
  ],
  imports: [
    RouterModule.forRoot(routes, {preloadingStrategy: PreloadAllModules}),
    BrowserAnimationsModule,
    BrowserModule.withServerTransition({ appId: 'main-app' }),
    ServiceWorkerModule.register('/ngsw-worker.js', {enabled: environment.production}),
    MenuModule,
    ContainerModule

  ],
  providers: [
    AuthGuard, 
    Config, 
    LayoutResolver
  ],
  bootstrap: [AppComponent]
})



export class AppModule { }

ngsw-config.json

{
  "index": "/index.html",
  "assetGroups": [{
    "name": "app",
    "installMode": "prefetch",
    "resources": {
      "files": [
        "/index.html"
      ],
      "versionedFiles": [
        "/*.bundle.css",
        "/*.bundle.js",
        "/*.chunk.js"
      ]
    }
  }, {
    "name": "assets",
    "installMode": "lazy",
    "updateMode": "prefetch",
    "resources": {
      "files": [
        "/assets/**",
        "favicon.ico",
        "**.png"
      ]
    }
  }],
  "dataGroups": [{
    "name": "api-performance",
    "urls": [
      "https://example.org/getMenus/**",
      "https://example.org/getContent/**",
      "https://example.org/getLayout/**",
      "https://example.org/getFooter/**"
    ],
    "cacheConfig": {
      "maxSize": 10000,
      "maxAge": "3d",
      "strategy": "performance"
    }
  }]
}

.angular-cli.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "project": {
    "name": "main-app",
    "ejected": false
  },
  "apps": [
    {
      "root": "src",
      "outDir": "dist/browser",
      "assets": [
        "assets",
        "manifest.json",
        "favicon.ico",
        "robots.txt"
      ],
      "index": "index.html",
      "main": "main.ts",
      "polyfills": "polyfills.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.app.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "serviceWorker": true,
      "styles": [
        "./assets/css/bootstrap.min.css",
        "./assets/css/styles.less"
      ],
      "scripts": [
        "./assets/js/jquery-1.12.4.min.js",
        "../node_modules/bootstrap/dist/js/bootstrap.min.js",
        "./assets/js/functions.js"
      ],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/browser/environment.ts",
        "prod": "environments/browser/environment.prod.ts"
      }
    },
    {
      "root": "src",
      "outDir": "dist/server",
      "assets": [
        "assets",
        "favicon.ico",
        "robots.txt"
      ],
      "platform": "server",
      "index": "index.html",
      "main": "main.server.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.server.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/server/environment.ts",
        "prod": "environments/server/environment.prod.ts"
      }
    }
  ],
  "e2e": {
    "protractor": {
      "config": "./protractor.conf.js"
    }
  },
  "lint": [
    {
      "project": "src/tsconfig.app.json",
      "exclude": "**/node_modules/**"
    },
    {
      "project": "src/tsconfig.spec.json",
      "exclude": "**/node_modules/**"
    },
    {
      "project": "e2e/tsconfig.e2e.json",
      "exclude": "**/node_modules/**"
    }
  ],
  "test": {
    "karma": {
      "config": "./karma.conf.js"
    }
  },
  "defaults": {
    "styleExt": "less",
    "component": {
    }
  }

}

One component for example:

news.component

import { Component } from '@angular/core';
import { ActivatedRoute } from "@angular/router";
import { Config } from "../../config";
import { ServerService } from "../../services/server.service";
import { SeoService } from "../../services/seo.service";
import { OnDestroy } from '@angular/core/src/metadata/lifecycle_hooks';
import { ISubscription } from 'rxjs/Subscription';





interface pageData {
  banner: string;
  data: any;
  html: string;
  text: string;
  title: string;
}

@Component({
  selector: 'app-news',
  templateUrl: './news.component.html',
  styleUrls: ['./news.component.less'],
  providers : [Config, ServerService, SeoService],
})


export class NewsComponent implements OnDestroy {
  subscription: ISubscription;
  subscriptionHTTP: ISubscription;

  URL: string;
  langUrl: string;
  active: string;
  pageData: pageData;
  headerText: Object;



  constructor(private config: Config, private route: ActivatedRoute, private service: ServerService, private seo: SeoService) {
    this.URL = this.config.impressURL;
    this.langUrl = this.config.getLanguage();

    this.subscription = this.route.params.subscribe( params => {

      if(params.lang != this.langUrl) {
        this.langUrl = params.lang;
      }

      let siteTitle = params.index;

      if(typeof siteTitle != 'undefined') {
          siteTitle = siteTitle.replace('.html', ' ');
          siteTitle = siteTitle.replace(/-/g,' ');
      }


     this.subscriptionHTTP = this.service.getResponse(`${this.URL}/getContent/${params.index}/${this.langUrl}/0`).subscribe(
        (response: any) => {
            this.pageData = response;
            this.seo.generateTags({
              lang: this.langUrl,
              title : siteTitle,
              image : `${this.URL}/file/repository/${this.pageData.banner}`,
              slug : params.index
          })
        }, (error) => {
            console.log(error);
        }
      ); 
   });
  }

  ngOnInit(): void {

  }

  ngOnDestroy() {
    if(this.subscription) this.subscription.unsubscribe();
    if(this.subscriptionHTTP)  this.subscriptionHTTP.unsubscribe();
  }

  hideOnClick(element, target) {
    element.parentNode.parentNode.classList.remove('in');
  }
}

EDIT
It’s visible in Cache tab, after setting up Server Transfer State for Angular Universal, but still not working offline (screen with cache tab).

Cache table

localForage seems to be the best solution. Will send answer if it’d work.

2

Answers


  1. Chosen as BEST ANSWER

    Okey, finally I found a solution. Thank You @nithalqb for the best idea. ngforage-ng5 is working fine! I added to API getAllPages site where I'm returning all pages list. Then I'm inserting it into IndexedDB in background, like that :

    private async saveData(url, data) {
        if (data) {
            for (let element of data) {
                await this.ngf.getItem(element.urlPath).then(async res => {
                    if (!await res) {
                        await this.http.get(`${url}/getContent/${element.urlPath}/${element.languageCode}/0`).toPromise().then(async response => {
                            await this.ngf.setItem(element.urlPath, await response);
                        })
                        await this.http.get(`${url}/getLayout/${element.urlPath}/${element.languageCode}`).toPromise().then(async response => {
                            await this.ngf.setItem(`${element.urlPath}/layout`, await response);
                        })
                    }
                })
            }
        };
    }
    

    Thank You for answers.


  2. @patryk-panek you could solve the caching issue just by avoiding the wildcard in the api path. e.g.

    "urls": [
      "https://example.net/getMenus",
      "https://example.net/getContent",
    ]
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search