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).
localForage seems to be the best solution. Will send answer if it’d work.
2
Answers
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 :Thank You for answers.
@patryk-panek you could solve the caching issue just by avoiding the wildcard in the api path. e.g.