121

We're currently working on a new project with regular updates that's being used daily by one of our clients. This project is being developed using angular 2 and we're facing cache issues, that is our clients are not seeing the latest changes on their machines.

Mainly the html/css files for the js files seem to get updated properly without giving much trouble.

Rikku121
  • 2,171
  • 2
  • 19
  • 34
  • 2
    Very good question. I have the same problem. What is the best way to solve this problem ? Is this possible with gulp or any similar tool for publishing Angular 2 application ? – jump4791 May 19 '17 at 14:07
  • 2
    @jump4791 Best way is to use webpack and compile the project using production settings. I´m currently using this repo, just follow the steps and you should be good: https://github.com/AngularClass/angular2-webpack-starter – Rikku121 May 19 '17 at 14:15
  • I also have same issue. – Ziggler Oct 30 '17 at 18:39
  • 3
    I know this is an old question but I wanted to add the solution I found, for anyone who happens over this. When building with `ng build`, adding the `-prod` tag adds a hash to the generated file names. This forces the reload of everything but `index.html`. [This github post](https://github.com/angular/angular-cli/issues/4320) had some hints on getting that to reload. – Tiz Jan 26 '18 at 14:54
  • 2
    index.html is the root cause. Because it doesn't have hashcode, when it's cached, everything else is used from the cache. – Fiona May 23 '19 at 05:06

6 Answers6

196

angular-cli resolves this by providing an --output-hashing flag for the build command (versions 6/7, for later versions see here). Example usage:

ng build --output-hashing=all

Bundling & Tree-Shaking provides some details and context. Running ng help build, documents the flag:

--output-hashing=none|all|media|bundles (String)

Define the output filename cache-busting hashing mode.
aliases: -oh <value>, --outputHashing <value>

Although this is only applicable to users of angular-cli, it works brilliantly and doesn't require any code changes or additional tooling.

Update

A number of comments have helpfully and correctly pointed out that this answer adds a hash to the .js files but does nothing for index.html. It is therefore entirely possible that index.html remains cached after ng build cache busts the .js files.

At this point I'll defer to How do we control web page caching, across all browsers?

Markus Pscheidt
  • 5,297
  • 4
  • 44
  • 63
Jack
  • 8,742
  • 11
  • 60
  • 97
  • 16
    This is the proper way to do this and should be the selected answer! – jonesy827 Dec 13 '17 at 06:23
  • 1
    This did not work for our app. Its too bad the templateUrl with a query string parameter does not work with CLI – DDiVita Feb 15 '18 at 17:50
  • 1
    Totally busts the issue. `--aot ` — enable Ahead-of-Time compilation. This will become a default setting in future versions of Angular CLI but for now we have to enable this manually `--output-hashing all` — hash contents of the generated files and append hash to the file name to facilitate browser cache busting (any change to file content will result in different hash and hence browser is forced to load a new version of the file) [ source : https://medium.com/@tomastrajan/6-best-practices-pro-tips-for-angular-cli-better-developer-experience-7b328bc9db81 ] – Paramvir Singh Karwal Jun 02 '18 at 19:00
  • 10
    This won't work if your index.html is cached by the browser, hence won't see new hashed names for your javascript resources. I think this a combination of this and the answer @Rossco gave would make sense. It also makes sense to make this consistent with HTTP headers sent. – stryba Feb 01 '19 at 13:19
  • 2
    @stryba This is why html caching should be handled differenty. You should specify the Cache-Control, Pragma, and Expires response headers so that no caching should take place. This is easy if you are using a backend framework, but I believe you can also handle this in .htaccess files for Apache (idk how it works in nginx though). – OzzyTheGiant Feb 08 '19 at 18:58
  • 3
    This answer adds a hash to the js files, which is great. But as stryba said, you also need to make sure index.html is not cached. You shouldn't do this with html meta tags, but with response header cache-control: no-cache (or other headers for more fancy caching strategies). – Noppey Apr 29 '19 at 11:56
  • Perfect, thank you! Althought consider removing the --aot flag as this might not be applicable to everyone – NiallMitch14 Oct 16 '19 at 11:34
  • As per numerous recommendations, I've removed the `--aot` flag. – Jack Oct 16 '19 at 12:41
35

Found a way to do this, simply add a querystring to load your components, like so:

@Component({
  selector: 'some-component',
  templateUrl: `./app/component/stuff/component.html?v=${new Date().getTime()}`,
  styleUrls: [`./app/component/stuff/component.css?v=${new Date().getTime()}`]
})

This should force the client to load the server's copy of the template instead of the browser's. If you would like it to refresh only after a certain period of time you could use this ISOString instead:

new Date().toISOString() //2016-09-24T00:43:21.584Z

And substring some characters so that it will only change after an hour for example:

new Date().toISOString().substr(0,13) //2016-09-24T00

Hope this helps

Rikku121
  • 2,171
  • 2
  • 19
  • 34
  • 3
    So my implementation actually didn't end up working. caching is a strange issue. sometimes works and sometimes not. oh the beauty of intermittent issues. So I actually adapted your answer to as such: `templateUrl: './app/shared/menu/menu.html?v=' + Math.random()` – Rossco Dec 02 '16 at 08:03
  • I'm getting 404 for my templateUrls. For example: GET http://localhost:8080/app.component.html/?v=0.0.1-alpha 404 (Not Found) Any idea why? – Shenbo May 03 '17 at 00:06
  • @Rikku121 No it doesn't. It's actually without the / in the url. I might have accidentally added it in when I post the comment – Shenbo May 04 '17 at 00:27
  • Turned out it could be because I have to give a full path rather than a relative path. For example: `templateUrl: 'app/app.component.html?v=' + VERSION` works. But `templateUrl: './app.component.html?v=' + VERSION` does not. Even though they are in the same directory – Shenbo May 04 '17 at 01:00
  • Then it's more of a why are my templates not loading question, thought you meant it wasn't loading after adding the ?v=... which was rather weird – Rikku121 May 04 '17 at 13:49
  • Yes that is what I meant. And yes that is quite weird @Rikku121 – Shenbo May 04 '17 at 22:48
  • To use this i have to go through to each component and append that "?v=".I have around 60 components. So it tedious job to go each component and do that. Is there a global way so we can define one variable and we can use in each templateUrl. Because i don't want that user every time take html from disk if there is no change in html. – RAVI PATEL Jun 22 '17 at 11:09
  • @RAVIPATEL best way is to use webpack using angular CLI, it's working really well and you get aot compilation too – Rikku121 Jul 04 '17 at 20:32
  • 18
    What is the point of caching when you are busting cache every time even when there is no code change? – Apurv Kamalapuri Feb 23 '18 at 06:36
  • @ApurvKamalapuri I agree with you. I have similar kind of issue, I want to cache bust only when my application is updated and the new build is triggered. – Jayesh Dhandha Apr 15 '19 at 07:06
  • 1
    ng build --aot --build-optimizer=true --base-href=// gives error --- Couldn't resolve resource ./login.component.html?v=${new Date().getTime()} – Pranjal Successena May 29 '19 at 07:27
  • i hardly believe busting caching for no reason other than elapsed time is sufficient in this context – Dan Dohotaru Aug 12 '19 at 13:12
  • I got an error ``` ERROR in HostResourceResolver: could not resolve ./settings.component.html?v=1 in context of /src/app/account/components/settings/settings.component.ts) ``` – Omar Salem Dec 24 '20 at 05:02
  • I cannot believe this is the accepted answer. This is just the worst recommendation I have ever seen. I'd rather suggest solving this problem from your static content service perspective (Apache, Nginx, IIS, ... ) – Alexus May 10 '21 at 13:31
28

In each html template I just add the following meta tags at the top:

<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">

In my understanding each template is free standing therefore it does not inherit meta no caching rules setup in the index.html file.

Rossco
  • 2,933
  • 2
  • 20
  • 34
8

A combination of @Jack's answer and @ranierbit's answer should do the trick.

Set the ng build flag for --output-hashing so:

ng build --output-hashing=all

Then add this class either in a service or in your app.module

@Injectable()
export class NoCacheHeadersInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler) {
        const authReq = req.clone({
            setHeaders: {
                'Cache-Control': 'no-cache',
                 Pragma: 'no-cache'
            }
        });
        return next.handle(authReq);    
    }
}

Then add this to your providers in your app.module:

providers: [
  ... // other providers
  {
    provide: HTTP_INTERCEPTORS,
    useClass: NoCacheHeadersInterceptor,
    multi: true
  },
  ... // other providers
]

This should prevent caching issues on live sites for client machines

Mazz
  • 1,739
  • 21
  • 37
NiallMitch14
  • 1,045
  • 1
  • 11
  • 22
4

I had similar issue with the index.html being cached by the browser or more tricky by middle cdn/proxies (F5 will not help you).

I looked for a solution which verifies 100% that the client has the latest index.html version, luckily I found this solution by Henrik Peinar:

https://blog.nodeswat.com/automagic-reload-for-clients-after-deploy-with-angular-4-8440c9fdd96c

The solution solve also the case where the client stays with the browser open for days, the client checks for updates on intervals and reload if newer version deployd.

The solution is a bit tricky but works like a charm:

  • use the fact that ng cli -- prod produces hashed files with one of them called main.[hash].js
  • create a version.json file that contains that hash
  • create an angular service VersionCheckService that checks version.json and reload if needed.
  • Note that a js script running after deployment creates for you both version.json and replace the hash in angular service, so no manual work needed, but running post-build.js

Since Henrik Peinar solution was for angular 4, there were minor changes, I place also the fixed scripts here:

VersionCheckService :

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class VersionCheckService {
    // this will be replaced by actual hash post-build.js
    private currentHash = '{{POST_BUILD_ENTERS_HASH_HERE}}';

    constructor(private http: HttpClient) {}

    /**
     * Checks in every set frequency the version of frontend application
     * @param url
     * @param {number} frequency - in milliseconds, defaults to 30 minutes
     */
    public initVersionCheck(url, frequency = 1000 * 60 * 30) {
        //check for first time
        this.checkVersion(url); 

        setInterval(() => {
            this.checkVersion(url);
        }, frequency);
    }

    /**
     * Will do the call and check if the hash has changed or not
     * @param url
     */
    private checkVersion(url) {
        // timestamp these requests to invalidate caches
        this.http.get(url + '?t=' + new Date().getTime())
            .subscribe(
                (response: any) => {
                    const hash = response.hash;
                    const hashChanged = this.hasHashChanged(this.currentHash, hash);

                    // If new version, do something
                    if (hashChanged) {
                        // ENTER YOUR CODE TO DO SOMETHING UPON VERSION CHANGE
                        // for an example: location.reload();
                        // or to ensure cdn miss: window.location.replace(window.location.href + '?rand=' + Math.random());
                    }
                    // store the new hash so we wouldn't trigger versionChange again
                    // only necessary in case you did not force refresh
                    this.currentHash = hash;
                },
                (err) => {
                    console.error(err, 'Could not get version');
                }
            );
    }

    /**
     * Checks if hash has changed.
     * This file has the JS hash, if it is a different one than in the version.json
     * we are dealing with version change
     * @param currentHash
     * @param newHash
     * @returns {boolean}
     */
    private hasHashChanged(currentHash, newHash) {
        if (!currentHash || currentHash === '{{POST_BUILD_ENTERS_HASH_HERE}}') {
            return false;
        }

        return currentHash !== newHash;
    }
}

change to main AppComponent:

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
    constructor(private versionCheckService: VersionCheckService) {

    }

    ngOnInit() {
        console.log('AppComponent.ngOnInit() environment.versionCheckUrl=' + environment.versionCheckUrl);
        if (environment.versionCheckUrl) {
            this.versionCheckService.initVersionCheck(environment.versionCheckUrl);
        }
    }

}

The post-build script that makes the magic, post-build.js:

const path = require('path');
const fs = require('fs');
const util = require('util');

// get application version from package.json
const appVersion = require('../package.json').version;

// promisify core API's
const readDir = util.promisify(fs.readdir);
const writeFile = util.promisify(fs.writeFile);
const readFile = util.promisify(fs.readFile);

console.log('\nRunning post-build tasks');

// our version.json will be in the dist folder
const versionFilePath = path.join(__dirname + '/../dist/version.json');

let mainHash = '';
let mainBundleFile = '';

// RegExp to find main.bundle.js, even if it doesn't include a hash in it's name (dev build)
let mainBundleRegexp = /^main.?([a-z0-9]*)?.js$/;

// read the dist folder files and find the one we're looking for
readDir(path.join(__dirname, '../dist/'))
  .then(files => {
    mainBundleFile = files.find(f => mainBundleRegexp.test(f));

    if (mainBundleFile) {
      let matchHash = mainBundleFile.match(mainBundleRegexp);

      // if it has a hash in it's name, mark it down
      if (matchHash.length > 1 && !!matchHash[1]) {
        mainHash = matchHash[1];
      }
    }

    console.log(`Writing version and hash to ${versionFilePath}`);

    // write current version and hash into the version.json file
    const src = `{"version": "${appVersion}", "hash": "${mainHash}"}`;
    return writeFile(versionFilePath, src);
  }).then(() => {
    // main bundle file not found, dev build?
    if (!mainBundleFile) {
      return;
    }

    console.log(`Replacing hash in the ${mainBundleFile}`);

    // replace hash placeholder in our main.js file so the code knows it's current hash
    const mainFilepath = path.join(__dirname, '../dist/', mainBundleFile);
    return readFile(mainFilepath, 'utf8')
      .then(mainFileData => {
        const replacedFile = mainFileData.replace('{{POST_BUILD_ENTERS_HASH_HERE}}', mainHash);
        return writeFile(mainFilepath, replacedFile);
      });
  }).catch(err => {
    console.log('Error with post build:', err);
  });

simply place the script in (new) build folder run the script using node ./build/post-build.js after building dist folder using ng build --prod

Aviko
  • 949
  • 9
  • 17
2

You can control client cache with HTTP headers. This works in any web framework.

You can set the directives these headers to have fine grained control over how and when to enable|disable cache:

  • Cache-Control
  • Surrogate-Control
  • Expires
  • ETag (very good one)
  • Pragma (if you want to support old browsers)

Good caching is good, but very complex, in all computer systems. Take a look at https://helmetjs.github.io/docs/nocache/#the-headers for more information.

ranieribt
  • 1,164
  • 14
  • 28