How to prevent Browser cache on Angular 2 site?

后端 未结 6 1601
慢半拍i
慢半拍i 2020-12-04 05:37

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 fac

相关标签:
6条回答
  • 2020-12-04 06:02

    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

    0 讨论(0)
  • 2020-12-04 06:06

    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.

    0 讨论(0)
  • 2020-12-04 06:11

    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

    0 讨论(0)
  • 2020-12-04 06:11

    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

    0 讨论(0)
  • 2020-12-04 06:22

    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.

    0 讨论(0)
  • 2020-12-04 06:28

    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?

    0 讨论(0)
提交回复
热议问题