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
- Angular 4.0.1
- Webpack 2.3.3
- Webpack builds go into "/dist" folder
- 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
Post a Comment