Angular reload page when new version is deployed
在類似於angular程式的專案,部署到生產環境後,如果後續還有程式碼更新的版本升級,程式碼需要重新壓縮,打包,之後在部署到生產環境。
注: 注意這個打包後的檔案,類似於js,css等檔案的檔名可以採取hash命名,就是每次打包的同一個js檔案的檔名都是包含hash字串的,這樣的好處是,可以強制瀏覽強制從服務端讀取最新的更改過的js檔案,但是前提是使用者的瀏覽器需要主動重新整理。但是對於有些頁面或者說單頁面的應用來說,各個頁面之間的遷移,可以不必重新整理即可完成,換句話說瀏覽器不會主動去apache伺服器讀新版本的js檔案。 換句話說這個hash檔名的打包方式解決瀏覽器主動重新整理就可以取新版本檔案的問題。
但是對於瀏覽器不主動重新整理的情況:
假設這樣一種情況,使用者在使用應用的某一頁面,因為前端的程式碼最後都是html,js,css等,並且已經載入到或者說cache到瀏覽器裡,只要使用者的瀏覽器沒有主動重新整理,當有新版本的程式碼放到apache server上時,使用者用的js等檔案仍然是舊版本的檔案內容。
- 要解決上面的問題,下面的文章是一種思路,大概的思路是,當應用打包時,用webpack的外掛把整個工程的檔案內容hash出一個字串(When any code file changes, that hash will change),以靜態檔案儲存到包的某一個位置。
- 之後在angular程式裡,寫個server可以讀取這個檔案的內容。之後在某個主元件比如app-component.ts裡每隔一段時間去呼叫這個server返回這個hash串的內容。讀取後把這個值在放到這個component的以全域性變數裡,之後每隔一個一段時間在去呼叫這個server返回hash的值,如果兩次的hash值不同,即為有新版本的程式碼產生。設定一個需要重新整理的標誌位。
- 這時在主AppRoutingModule裡讀#2中的需要重新整理的標誌位,如果標誌位的值滿足條件,即可取使用者當前的url在重新reload網頁。location.href = val.url;
這個思路很不錯。
其實我理解像這樣的跟業務無關的功能,Angular本身完全可以提供一個相應的解決方案,寫程式時,只要配置一下,是否需要當服務端有新版本產生時,自動重新整理頁面取新版程式碼。這是很實用的功能。
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.