45

I have a VueJS app. Whenever I run npm run build it creates a new set of dist/* files, however, when I load them on the server (after deleting the old build), and open the page in browser, it loads the old build (from cache i assume). When I refresh the page, it loads the new code no problem.

This is my index.html:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
        <meta http-equiv="cache-control" content="max-age=0" />
        <meta http-equiv="cache-control" content="no-cache" />
        <meta http-equiv="expires" content="-1" />
        <meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
        <meta http-equiv="pragma" content="no-cache" />
        <link rel="stylesheet" href="/static/css/bootstrap.min.css"/>
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>

Is there a way to force it to load new code every time or (ideally) to check if the old files are gone from the server, then refresh the browser?

ierdna
  • 4,056
  • 6
  • 39
  • 66
  • Have you tried adding random hashes to the file names for each build? This will force the browser to "refresh" and load the new version, yet still cache it if the same file hash is requested. Angular 2 does this. For example, `build.js` will be `build.32048uueo02348324.js`, where the hash represents the current "version" of the build, and when a new build is created the old file is destroyed in place of a new file with a different hash. This way you don't need to worry about any HTTP headers unless you want more control. – Lansana Camara Aug 24 '17 at 17:08
  • this could help https://stackoverflow.com/questions/1011605/clear-the-cache-in-javascript – jakob Aug 24 '17 at 17:10
  • 1
    the build already does that, here's example of my latest build: `app.c6831a12f10f0ece2c67.js` – ierdna Aug 24 '17 at 17:10
  • @andrei if that's the case, try removing any headers you've set for caching. The browser should handle the caching/serving of new content by default based on those hashes. But the other thing is you need to make sure you are referencing the new files in your HTML. – Lansana Camara Aug 24 '17 at 17:10
  • 1
    You might even have to hash your index.html file if that's not already being done for you, because if that is modified to reference new files but it is cached, then the index.html will always be the old version until cache is cleared. – Lansana Camara Aug 24 '17 at 17:12
  • @andrei Yeah precisely, we spoke at the same time. Does Vue do the hashing for index.html? – Lansana Camara Aug 24 '17 at 17:13
  • @Lansana no it doesn't (at least not out of the box). But if I hash index.html, how will the browser know its name? – ierdna Aug 24 '17 at 17:14
  • @andrei Well that is for your web server to determine. For every new deployment, your web server could manually grab index.html, change it's name to some new hashed name, and then use that newly hashed index.html file name as the file it serves when people request `/` or any other URL on your app. Does that make sense? – Lansana Camara Aug 24 '17 at 17:15
  • @Lansana I was trying to avoid fidgeting with Nginx (that's what I'm running). But I guess I'll have to give it a try. – ierdna Aug 24 '17 at 17:16
  • @andrei That is just one solution. If you prefer to go with the approach in question (adding meta tags in your HTML or using HTTP headers in your response), you could probably achieve it that way as well. I wouldn't be much help there, though, as I don't quite know how to do it without any additional research. – Lansana Camara Aug 24 '17 at 17:17
  • But just keep in mind if you go with the Nginx approach, all you have to do is hash the index.html every time the Nginx server is first loaded and use that file name when serving your HTML. That will be a working approach. – Lansana Camara Aug 24 '17 at 17:18

6 Answers6

18

We struggled with this same issue and found that some people's browsers would not even pull the latest version unless they manually refreshed. We had problems with caching at various layers, including the CDN where we hosted files.

We also struggled with maintaining versions and being able to rapidly redeploy a previous version if something goes wrong.

Our solution (using project based on vue-cli Webpack):

1) We build the distribution to have a version specific folder instead of 'static'. This also helps us track builds and 'undo' a deployment if needed. To change the 'static' directory, change 'assetsSubDirectory' under 'build' in index.js and change 'assetsPublicPath' to your CDN path.

2) We use Webpack Assets Manifest to build a manifest.json file pointing to all the assets. Our manifest includes a hash of all files, as its a high security application.

3) We upload the versioned folder (containing the js and css) to our CDN.

4) (Optional) We host a dynamic index.html file on the backend server. The links to the stylesheet and scripts are filled in by the backend server using a template system pulled from the data on the manifest.json (see #5). This is optional as you could use the force-reload option as in the comment below, which isn't a great experience but does work.

5) To publish a new version, we post the manifest.json to the backend server. We do this via a GraphQL endpoint but you could manually put the json file somewhere. We store this in the database and use it to populate the index.html and also use it to verify files using the file hash (to validate our CDN was not hacked).

Result: immediate updates and an easy ability to track and change your versions. We found that it will immediately pull the new version in almost all user's browsers.

Another bonus: We are building an application that requires high security and hosting the index.html on our (already secured) backend enabled us to more easily pass our security audits.


Edit 2/17/19

We found that corporate networks were doing proxy caching, despite no-cache headers. IE 11 also seems to ignore cache headers. Thus, some users were not getting the most up to date versions.

We have a version.json that is incremented/defined at build time. Version number is included in manifest.json. The build bundle is automatically uploaded to S3. We then pass the manifest.json to the backend (we do this on an entry page in Admin area). We then set the "active" version on that UI. This allows us to easily change/revert versions.

The backend puts the "currentVersion" as a Response Header on all requests. If currentVersion !== version (as defined in version.json), then we ask the user to click to refresh their browser (rather than force it on them).

For the Name
  • 2,032
  • 12
  • 16
  • 24
    I think I got away with much less pain. I get the client to request server version, then it compares current server version with what client had stored in `localStorage` variable. if they're different, i do `window.location.reload(true);` forcing the browser to get new file from server. at least it seems to work. – ierdna Sep 28 '17 at 20:06
  • @ierdna How are you comparing versions exactly? – Cole W Jan 25 '21 at 19:47
  • i added a route to my api called `/version/` – ierdna Jan 26 '21 at 01:34
9

Based on this comprehensive answer on cache headers, your best bet is going to be solving this on the server side if you have control of it, as anything in the <meta> tags will get overridden by headers set by the server.

The comments on the question indicate that you are serving this app with nginx. Using the linked answer above, I was able to set the Cache-Control, Expires and Pragma headers for any requests for files ending in .html this way in my nginx config:

server {

  ...other config

  location ~* \.html?$ {
    expires -1;
    add_header Pragma "no-cache";
    add_header Cache-Control "no-store, must-revalidate";
  }
}

This successfully forces the browser to request the latest index.html on every page reload, but still uses the cached assets (js/css/fonts/images) unless there are new references in the latest html response.

Sean Ray
  • 735
  • 1
  • 9
  • 13
  • Setting must-revalidate does not make sense because in order to go through revalidation you need the response to be stored in a cache, which no-store prevents. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control – zub0r May 29 '20 at 08:26
6

This problem is annoying, no doubt. Had to solve it using a custom Front end version endpoint sent in the header. I am using a rails backend and Vue + Axios as front-end. Note I don't need a service worker and hence not using one.

Essentially I am just reloading whenever there is a get request and that the version of the application has changed (the server can inform the same)

axiosConfig.js

axios.interceptors.response.use(
  (resp) => {
    const fe_version = resp.headers['fe-version'] || 'default'
    if(fe_version !== localStorage.getItem('fe-version') && resp.config.method == 'get'){
      localStorage.setItem('fe-version', fe_version)
      window.location.reload() // For new version, simply reload on any get
    }
    return Promise.resolve(resp)
  },
)

Rails Backend

application_controller.rb

after_action :set_version_header

def set_version_header
  response.set_header('fe-version', Setting.key_values['fe-version'] || 'default')
end

application.rb (CORS config assuming Vue running on port 8080)

config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins ['localhost:8080', '127.0.0.1:8080']
    resource '*', expose: ['fe-version'], headers: :any, methods: [:get, :post, :delete, :patch], credentials: true
  end
end if Rails.env.development?

Wrote a detailed article here: https://blog.francium.tech/vue-js-cache-not-getting-cleared-in-production-on-deploy-656fcc5a85fe

bragboy
  • 32,353
  • 29
  • 101
  • 167
3

To delete the cache you can run rm -rf node_modules/.cache

This deletes your cache. You can run a new build before deploying.

I was having the same issue where I ran a production build, but then even when running locally my code would point to the production build instead of my latest changes.

I believe this is a related issue: https://github.com/vuejs/vue-cli/issues/2450

Connor Leech
  • 15,156
  • 27
  • 91
  • 133
1

If you use asp.net core you can try the following trick among with the webpack which generates js files with the hash at the end of the name eg. my-home-page-vue.30f62910.js. So your index.html contains: <link href=/js/my-home-page-vue.30f62910.js rel=prefetch> which means whenever you change the my-home-page.vue it will generate a new hash in the filename.

The only thing you need is to add a cache restriction against the index.html

In your Startup.cs:

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
    // ....
    app.UseStaticFiles(new StaticFileOptions
    {
      // Make sure your dist folder is correct
      FileProvider = new PhysicalFileProvider(Path.Combine(_env.ContentRootPath, "ClientApp/dist")),
      RequestPath = "",
      OnPrepareResponse = context =>
      {
        if (context.Context.Request.Path.StartsWithSegments("/index.html", StringComparison.OrdinalIgnoreCase))
        {
          context.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store");
          context.Context.Response.Headers.Add("Expires", "-1");
        }
      },
    });
    // ....
}
ADM-IT
  • 2,287
  • 17
  • 19
1

I serve a Nuxt app alongside a very lightweight Express app that handles server-side authentication and other things. My solution is to let Express store the current git hash in a cookie when a user logs in. This value is then placed in a Vuex store along with the current time (and other info about the current user).

On a route change, a router middleware checks how long it's been since we last compared the store' hash with the actual one. If it's been more than a minute, we request the current Hash from Express and force a server-side render for the current route if the value has changed.

Here's the relevant code:

server/index.js

const getRevision = function() {
  return require('child_process')
    .execSync('git rev-parse --short HEAD')
    .toString()
    .trim()
}

app.get(
  '/login',

  async function(req, res, next) {
    // (log in the user)

    const revision = getRevision()
    res.cookie('x-revision', revision)

    return res.redirect('/')
  }
)

app.get(
  '/revision',

  function(req, res) {
    const revision = getRevision()
    return res.send(revision)
  }
)

store/index.js

export const actions = {
  nuxtServerInit({ dispatch }) {
    const revisionHash = this.$cookies.get('x-revision')
    dispatch('auth/storeRevision', revisionHash)
  }
}

store/auth.js

export const state = () => ({
  revision: { hash: null, checkedAt: null }
})

export const mutations = {
  setRevision(store, hash) {
    store.revision = { hash: hash, checkedAt: Date.now() }
  }
}

export const actions = {
  storeRevision({ commit }, revisionHash) {
    commit('setRevision', revisionHash)
  },

  touchRevision({ commit, state }) {
    commit('setRevision', state.revision.hash)
  }
}

middleware/checkRevision.js

export default async function({ store, route, app, redirect }) {
  const currentRevision = store.state.auth.revision
  const revisionAge = Date.now() - currentRevision.checkedAt

  if (revisionAge > 1000 * 60) {
    // more than a minute old
    const revisionHash = await app.$axios.$get(
      `${process.env.baseUrl}/revision`
    )

    if (revisionHash === currentRevision.hash) {
      // no update available, so bump checkedAt to now
      store.dispatch('auth/touchRevision')
    } else {
      // need to render server-side to get update
      return redirect(`${process.env.baseUrl}/${route.fullPath}`)
    }
  }

  return undefined
}
Devin
  • 926
  • 9
  • 29