Angular reload page when new version is deployed

When developing a Single Page Application we want to deploy frequent changes, and do not want a user stuck at a stale version. We also do not want to interrupt the user if they are in the middle of a process to refresh the entire website. Here is my current solution to solve this problem:

System Specs/Configuration

  1. Angular 4.0.1
  2. Webpack 2.3.3
  3. Webpack builds go into "/dist" folder
  4. webpack.config.json is in the "/config" folder

Step 1: Use Webpack to generate a hash.json after every build

My webpack config adds different plugins based on environment. It always adds the following plugin to grab the hash:
plugins.push(
    function() {
        this.plugin("done", function(stats) {
            require("fs").writeFileSync(
                path.join(__dirname, "../dist", "hash.json"),
                JSON.stringify(stats.toJson().hash));
        });
    }
);
/dist/hash.json is my version. When any code file changes, that hash will change. The contents of the file look like this: "9363b1ba4e6a8ec5f47c"

Step 2:Write an Angular Service to check current version

We will do a GET to hash.json, and break any caching using current time-stamp as a cache buster.

ServerResponse is a reusable object I use for all http requests so I can handle errors.

common.ts:
export class ErrorItem {
    constructor(public msg: string, public param: string = "") {

    }
}

export class ServerResponse {
    public data: T;
    public errors: ErrorItem[];
    public status: number;
}
version.service.ts:
import { Injectable } from '@angular/core';
import { Http, Headers, Response  } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import { ServerResponse } from '../models/common'
@Injectable()

export class VersionService {

    public needsRefresh : boolean = false;

    constructor(private http: Http) {

    }
    getVersion() : Observable> {

        let headers = new Headers();
        headers.append('Content-Type', 'application/json');

        let output = new ServerResponse();

        return this.http
            .get(
                'hash.json?v=' + (new Date()).getTime(),
                { headers }
            )
            .map((res : Response) => {
                output.status = res.status;
                var response = res.json();
                output.data = response;
                output.errors = [];

                return output;
            }).catch(err => {
                output.status = err.status;
                output.data = null;
                return Observable.of(output)
            });

    }
}

Step 3: Periodically check for version update

In my main app component, I call the VersionService and store the latest hash value. If the new value does not match the current value, a refresh is needed. The needsRefresh boolean is stored in the shared singleton service and it can be accessed by any module to get the latest value.

app.component.ts: 
import { Component, OnInit, ViewEncapsulation} from '@angular/core';
import { VersionService } from './shared/services/version.service';

@Component({
    selector: 'app',    
})
export class AppComponent implements OnInit{

    version : string = "";

    constructor(private versionService: VersionService) {

    }

    checkVersion() {
        this.versionService.getVersion().subscribe((result) => {
            if (result.data) {

                if (this.version && this.version != result.data) {
                    this.versionService.needsRefresh = true;
                }
                this.version = result.data;
            }
        });

        setTimeout(() => {
            this.checkVersion();
        }, 15000);
    }

    ngOnInit() {
        this.checkVersion();
    }
}

Step 4: On route change reload the entire page to new route if needsRefresh

I use a a routing module to handle all my routing needs. One of its jobs is to do a full page reload and not just change routes when a new version is deployed. The redirect takes the user to the the page they were trying to go to so the entire process is very smooth for the user.

app.routing.ts:

import { NgModule } from '@angular/core';
import { SharedGlobalModule} from './shared/modules/shared.global.module'
import { Routes, RouterModule, Router, NavigationEnd, ActivatedRouteSnapshot, NavigationStart } from '@angular/router';
import { NgIdleKeepaliveModule } from '@ng-idle/keepalive';

import { PublicComponent} from './public/public.component'
import { PublicRoutes } from './public/public.routing'
import { PublicAuthGuard } from './public/public.guard'
import { SecureComponent} from './secure/secure.component'
import { SecureRoutes } from './secure/secure.routing'
import { SecureAuthGuard } from './secure/secure.guard'
import { VersionService } from './shared/services/version.service'

const routes: Routes = [
    { path: '', redirectTo: '/public/login', pathMatch: 'full' },
    { path: 'public', component: PublicComponent, children: PublicRoutes, canActivate: [PublicAuthGuard] },
    { path: 'secure', component: SecureComponent, children: SecureRoutes, canActivate: [SecureAuthGuard] },
    { path: 'p/:token', redirectTo: '/public/reset/:token', pathMatch: 'full' },
    { path: '**', redirectTo: '/public/login' },
];

@NgModule({
    imports: [RouterModule.forRoot(routes),SharedGlobalModule, NgIdleKeepaliveModule.forRoot()],
    exports: [RouterModule],
    declarations: [PublicComponent,SecureComponent],
    providers: [SecureAuthGuard,PublicAuthGuard,VersionService]
})
export class AppRoutingModule {
    watchRouteChanges() {
        this.router.events.subscribe((val) => {
            if (val instanceof NavigationStart && this.versionService.needsRefresh === true) {
             location.href = val.url;
            }
        });
    }

    constructor (private router : Router, private versionService: VersionService) {
        this.watchRouteChanges();
    }

}

Notes:


  • I am a little hesitant polling every 15 seconds, but it is a tiny static file, so I am currently OK with it.
  • Some code was omitted from the actual files I use in production, to focus on the topic of this post.

Comments

Popular posts from this blog

Selenium on Heroku

Angular directory structure for large projects