5

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.

Patryk Panek
  • 405
  • 4
  • 20
  • 1
    The same issue happened to me, what i did to solve is, i used ngforage a localForage bindings for Angular 4 and 5. It helped me saving my api data locally in browser and once we go offlline we can use the data. Let me know if you find any other way. Thanks – nithalqb Feb 02 '18 at 11:15
  • Do you think it will solve dynamic rest api calls ? I think that URL's are only know where browser is navigating to specified page. How to skip it and download rest of data in background ? – Patryk Panek Feb 02 '18 at 11:52
  • it solved for me, as i fetched data from api and stored locally using localForage and once the app is offline the data will be fetched from local. But i am still not cleared regarding the role of urls in dataGroups.. i think you may be right. – nithalqb Feb 02 '18 at 11:56
  • 1
    Okey, maybe it is possible without using external tools. I hope someone here will know a solution. If won't, I will try to do it by yours way. In touch ! – Patryk Panek Feb 02 '18 at 12:05

2 Answers2

2

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.

Patryk Panek
  • 405
  • 4
  • 20
  • May you help me with this -> https://stackoverflow.com/questions/62504521/angular-pwa-how-can-i-make-http-server-running-on-localhost8080-to-work-with – Donovant Jun 21 '20 at 21:48
0

@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",
]
rewath
  • 41
  • 1
  • 8