在类似于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.