VueJS/browser 缓存生产版本

VueJS/browser caching production builds

我有一个 VueJS 应用程序。每当我 运行 npm run build 它都会创建一组新的 dist/* 文件,但是,当我将它们加载到服务器上时(删除旧版本后),并在浏览器中打开页面,它加载旧版本(我假设来自缓存)。当我刷新页面时,它加载新代码没问题。

这是我的 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>

有没有办法强制它每次都加载新代码或(理想情况下)检查旧文件是否从服务器上消失,然后刷新浏览器?

我们为解决同样的问题而苦苦挣扎,发现有些人的浏览器甚至不会提取最新版本,除非他们手动刷新。我们在各个层都存在缓存问题,包括我们托管文件的 CDN。

我们还努力维护版本,并在出现问题时能够快速重新部署以前的版本。

我们的解决方案(使用基于vue-cli Webpack的项目):

1) 我们将分发版构建为具有特定于版本的文件夹而不是 'static'。如果需要,这也有助于我们跟踪构建和 'undo' 部署。要更改 'static' 目录,请在 index.js 中的 'build' 下更改 'assetsSubDirectory' 并将 'assetsPublicPath' 更改为您的 CDN 路径。

2) 我们使用 Webpack Assets Manifest 构建一个指向所有资产的 manifest.json 文件。我们的清单包括所有文件的哈希值,因为它是一个高安全性应用程序。

3) 我们将版本化文件夹(包含 js 和 css)上传到我们的 CDN。

4)(可选)我们在后端服务器上托管一个动态 index.html 文件。样式表和脚本的链接由后端服务器使用从 manifest.json 上的数据中提取的模板系统填充(参见#5)。这是可选的,因为您可以使用下面评论中的 force-reload 选项,这不是很好的体验,但确实有效。

5) 要发布新版本,我们post manifest.json 到后端服务器。我们通过 GraphQL 端点执行此操作,但您可以手动将 json 文件放在某处。我们将其存储在数据库中并使用它来填充 index.html 并使用它来使用文件哈希来验证文件(以验证我们的 CDN 未被黑客入侵)。

结果:立即更新并轻松跟踪和更改您的版本。我们发现它会在几乎所有用户的浏览器中立即拉取新版本。

另一个好处:我们正在构建一个需要高安全性的应用程序,并在我们(已经安全的)后端托管 index.html 使我们能够更轻松地通过安全审核。


编辑 2/17/19

尽管 no-cache headers,我们发现企业网络仍在进行代理缓存。 IE 11 似乎也忽略了缓存 headers。因此,一些用户没有获得最新版本。

我们有一个 version.json 在构建时是 incremented/defined。版本号包含在 manifest.json 中。构建包会自动上传到 S3。然后我们将 manifest.json 传递给后端(我们在管理区域的入口页面上执行此操作)。然后我们在 UI 上设置 "active" 版本。这使我们可以轻松地 change/revert 版本。

后端将 "currentVersion" 作为对所有请求的响应 Header。如果 currentVersion !== version(在 version.json 中定义),那么我们要求用户点击刷新他们的浏览器(而不是强制他们)。

基于 this comprehensive answer on cache headers,如果您可以控制它,您最好的选择是在服务器端解决这个问题,因为 <meta> 标签中的任何内容都会被 headers 由服务器设置。

对该问题的评论表明您正在使用 nginx 为该应用提供服务。使用上面的链接答案,我能够以这种方式为以 .html 结尾的文件的任何请求设置 Cache-ControlExpiresPragma headers nginx 配置:

server {

  ...other config

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

这成功地强制浏览器在每次重新加载页面时请求最新的 index.html,但仍然使用缓存的资产 (js/css/fonts/images),除非最新的 html 中有新的引用响应。

要删除缓存你可以运行rm -rf node_modules/.cache

这会删除您的缓存。您可以 运行 在部署之前构建一个新版本。

我在 运行 生产版本时遇到了同样的问题,但即使在本地 运行 我的代码也会指向生产版本而不是我的最新更改。

我认为这是一个相关问题:https://github.com/vuejs/vue-cli/issues/2450

如果您使用 asp.net 核心,您可以在 webpack 中尝试以下技巧,它生成的 js 文件的名称末尾带有哈希值,例如。 my-home-page-vue.30f62910.js。 所以你的 index.html 包含: <link href=/js/my-home-page-vue.30f62910.js rel=prefetch> 这意味着每当您更改 my-home-page.vue 时,它都会在文件名中生成一个新的散列。

您唯一需要做的就是针对 index.html

添加缓存限制

在你的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");
        }
      },
    });
    // ....
}

我提供一个 Nuxt 应用程序和一个非常轻量级的 Express 应用程序,用于处理服务器端身份验证和其他事情。我的解决方案是让 Express 在用户登录时将当前 git 哈希值存储在 cookie 中。然后将该值与当前时间(以及有关当前用户的其他信息)一起放入 Vuex 存储中。

在路由更改时,路由器中间件会检查自从我们上次将商店的哈希值与实际哈希值进行比较以来已经过去了多长时间。如果超过一分钟,我们会从 Express 请求当前哈希值,如果值发生变化,我们会强制服务器端渲染当前路由。

相关代码如下:

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
}

毫无疑问,这个问题很烦人。必须使用 header 中发送的自定义前端版本端点来解决它。我正在使用 rails 后端和 Vue + Axios 作为 front-end。请注意,我不需要服务人员,因此不使用服务人员。

基本上我只是在有get请求并且应用程序版本发生变化时重新加载(服务器可以通知相同)

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 后端

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 配置假设 Vue 运行 在端口 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?

在这里写了一篇详细的文章:https://blog.francium.tech/vue-js-cache-not-getting-cleared-in-production-on-deploy-656fcc5a85fe

如果您的应用程序是 PWA(渐进式 Web 应用程序)并且您正在使用服务工作者来缓存构建,则需要稍微复杂一些的设置才能获得最新版本。

关键部分是在设置服务人员时更新 register.updated 函数。而不是复制粘贴解决方案,you can find the article here。我不是这篇文章的作者,只是添加这篇文章是为了节省人们寻找解决方案以获取最新内容的时间 post-部署 PWA 构建。