Monday 22 July 2019

HttpInterceptor to cache clientside in Angular 8

I have been working with a sample project of HttpInterceptor to cache data clientside in Angular 8 hosted on Github. The repository is here: https://github.com/toreaurstadboss/AngularHttpInterceptorSample First off, the data is delivered by a small backend using Express.Js.The server.js file looks like this:
const express = require('express');
const path = require('path');
const fs = require('fs');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var towns = require('./server/routes/towns');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, '/server/views'));
app.set('view engine', 'jade');

app.use(favicon(path.join(__dirname, 'dist/favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'dist')));

app.use(function (req, res, next) {
    if (!req.url.startsWith('/api')){
      req.url = '/api' + req.url;
    }
    console.log('got requst url: ' + req.url);
    console.log('Server handling request at Time:', new Date())
    next()
  })

// app.use('/', routes);
app.get('/api/towns', function(req, res) {
    let townsFileContents = fs.readFileSync(path.join(__dirname, 'server/data/townsandcities.json'), 'utf-8');
    let townsFound = JSON.parse(townsFileContents);
    console.log(townsFileContents);
    res.json(townsFound);
});

// app.get('*', function(req, res) {
//   res.sendFile(path.join(__dirname, 'dist/index.html'));
// });

// catch 404 and forward to error handler
// app.use(function(req, res, next) {
//     var err = new Error('Not Found');
//     err.status = 404;
//     next(err);
// });

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});

var debug = require('debug')('server');

app.set('port', process.env.PORT || 3000);

app.listen(app.get('port'));

console.log('Listening on port: ' + app.get('port'));

module.exports = app;

Angular bits is written in Typescript instead of just Js. The Http Interceptor itself is a ES 6 class which is exported and caches http responses according to route urls.
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CacheService } from './cache.service';


export class CacheInterceptor implements HttpInterceptor {

    constructor(private cacheService: CacheService) {
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {


        if (req.method !== 'GET') {
            this.cacheService.invalidateCache();
        }
        const cachedResponse: HttpResponse<any> = this.cacheService.get(req.url);
        if (cachedResponse !== undefined) {
         return of(cachedResponse);
        }

        return next.handle(req).pipe(
            tap(event => {
                if (event instanceof HttpResponse) {
                    this.cacheService.put(req.url, event);
                    let reponse: HttpResponse<any>;
                    reponse  = event;

                }
                return of(event);
            })
        );
    }

}

The CacheService caches responses to the client by route url as mentioned. It looks like this:
import { Injectable } from '@angular/core';
import { ICacheservice } from './icacheservice';
import { HttpResponse } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class CacheService implements ICacheservice {
  private requests = { };
  put(url: string, response: HttpResponse): void {
    this.requests[url] = response;
  }
  get(url: string): HttpResponse | undefined {
    return this.requests[url] ? this.requests[url] : undefined;
  }
  invalidateCache(): void {
    this.requests = {};
  }
  expireItem(url: string, timeoutInMilliseconds: number) {
    const itemFound = this.get(url);
    if (itemFound !== undefined) {
      setTimeout(() => {
        this.requests[url] = undefined;
      }, timeoutInMilliseconds);
    }

  }

  constructor() { }

}

We inject the HttpInterceptor to the app module like this:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { CacheInterceptor } from './core/cache.interceptor';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatTableModule } from  '@angular/material';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    MatTableModule,
    BrowserAnimationsModule
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

The index page then contains some markup including a Material Table using Google's Material UI. The app.component.ts handles the client side call back that triggers the retrieval and clearing of the data. If you click the load link, the data is retrieved and cached. If you open up Developer tools with F12, you will spot that the data is cached if you hit load many times. If you hit clear link, the data is cleared from cache and next click on load will fetch data again and inject into the cache. This sample shows how you can create a cache side feature of Angular 8 and support an HTTP interceptor. To check it out yourself run: git clone https://github.com/toreaurstadboss/AngularHttpInterceptorSample.git Afterwards, run: npm run start This will start both the Angular website on port 4200 and the Node.js Express.js backend on port 3000. The package.json script for npm run start looks like this: "start": "concurrently --kill-others \"ng build --watch --no-delete-output-path\" \"nodemon server.js\" \"ng serve --proxy-config proxy.conf.json \"", You can also just enter: npm start
Share this article on LinkedIn.

No comments:

Post a Comment