如何在部署后强制客户端重新加载?

21
我正在使用MEAN堆栈(mongo,express,angular和node)。我相对频繁地部署到生产环境,每隔几天。我的担忧是,有时我会更改客户端代码和API,并且我宁愿不必确保API向后兼容以前版本的客户端代码。
在这种情况下,如何有效地确保所有客户端在我推到生产环境时重新加载?例如,我已经看到Evernote有一个弹出窗口,说请重新加载浏览器以获取最新版本的Evernote。我想做类似的事情...是否需要使用socket.io或sock.js,或者我错过了一些简单的方法来实现这一目标?
6个回答

9

更新:AppCache在2015年夏季被弃用,因此以下内容不再是最佳解决方案。新的建议是使用Service Workers代替。然而,Service Workers目前仍处于实验阶段,在IE和Safari中支持不稳定(即:可能没有支持)。

或者,许多构建工具现在无缝地整合了缓存破坏和文件“版本控制”技术以解决OP的问题。在这个领域,WebPack可以说是当前的领导者。


这可能是使用HTML5的AppCache的一个好用例。

您可能希望将其中一些步骤自动化到部署脚本中,但是以下提供的代码可能会对您有所帮助。

首先,创建您的应用程序缓存清单文件。这也将使您能够在客户端的浏览器中缓存资源,直到您明确修改应用程序缓存清单文件的日期为止。

/app.appcache:

CACHE MANIFEST

#v20150327.114142

CACHE:
/appcache.js
/an/image.jpg
/a/javascript/file.js
http://some.resource.com/a/css/file.css

NETWORK:
*
/

app.appcache文件中,第#v20150327.114142行的注释是我们向浏览器表明清单已更改并且应重新加载资源的方式。它可以是任何内容,只要该文件与以前的版本在浏览器中看起来不同即可。在应用程序中部署新代码时,应修改此行。也可以使用构建ID代替。
其次,在您想要使用应用程序缓存的任何页面上,将标题标记修改为以下内容:
<html manifest="/app.appcache"></html>

最后,您需要添加一些Javascript代码来检查应用程序缓存是否有任何更改,如果有的话,请采取相应措施。这里是一个Angular模块。对于这个问题,这里有一个原始的示例:

appcache.js:

window.applicationCache.addEventListener('updateready', function(e) {
    if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
        // Browser downloaded a new app cache.
        // Swap it in and reload the page to get the latest hotness.
        window.applicationCache.swapCache();
        if (confirm('A new version of the application is available. Would you like to load it?')) {
            window.location.reload();
        }
    }
    else {
        // Manifest didn't changed. Don't do anything.
    }
}, false);

如果AppCache不能满足您的需求,一个更简单的解决方案是创建一个简单的API端点,返回当前构建ID或最后部署日期时间。您的Angular应用程序偶尔会访问此端点,并将结果与其内部版本进行比较,如果不同,则重新加载自身。

或者,您可以考虑使用实时重新加载脚本(示例),但是,在开发中非常有帮助,但我不确定在生产中使用实时/原地重载资产是否是一个好主意。


哇!AppCache。我觉得这可能有点过于复杂了,特别是因为我看到IE9似乎不支持,而Firefox似乎需要用户同意。虽然我真的很喜欢“贫民区”解决方案的想法——在设定的时间间隔内检查API端点,如果必要就调用window.location.reload()。实际上对我来说似乎很简单...虽然我肯定会有人告诉我,我可以用socket.io同样容易地做到同样的事情.... - Andrew
是的,你可以使用socket.io来实现这个功能。但为了实现“实时”的代码更新,这样做会增加很多额外的开销。通过在超时时向服务器发送请求似乎就足够了(而且更节省资源;不用担心大量开放的socket连接或者在扩展时需要负载均衡它们…)。当然,除非你的应用程序已经使用了socket.io,否则这么做就没有意义了。 - Marco Lüthy
是的 - 我也这么想...过于繁琐了。我们可能会在接下来几个月内添加socket.io,但我认为在实际添加之前还是使用API ping方法。非常感谢您的回答! - Andrew
那可能不应该是被接受的答案。在指向应用程序缓存的链接顶部:已弃用 此功能已从Web标准中删除。尽管某些浏览器仍可能支持它,但正在被淘汰。不要在旧项目或新项目中使用它。使用Service Workers代替。 - pmont
1
@pmont:我表示同意。我会相应地更新我的答案,并添加一个附录。 - Marco Lüthy

2
首先,我会告诉您我的问题,然后建议一个暂时的解决方案。我想在生产环境部署时强制用户退出登录,然后重新登录。在任何时候,生产环境上都将部署两个版本的软件。一种是前端知道的软件版本,另一种是后端知道的版本。大多数情况下,它们将是相同的。如果它们在某个时刻不同步,则需要重新加载客户端,以便让客户端知道新的生产构建已经推出。
我假设99.99%的时间后端将了解生产环境中部署软件的最新版本。
以下是我想推荐的两种方法:
  1. 后端API应始终在响应头中返回软件的最新版本。在前端,我们应该有一个通用的代码片段,检查API返回的版本和FE上的版本是否相同。如果不同,则重新加载。
  2. 每当用户登录时,BE应该在JWT中编码最新的软件版本。 FE应该随着每个API请求发送此作为Bearer令牌。 BE还应为每个API请求编写一个通用拦截器,该拦截器将比较从API请求接收到的JWT中的软件版本和

1
也许你可以在客户端代码文件名中添加哈希值,例如app-abcd23.js
这样浏览器就会重新载入文件而不是从缓存中获取。或者你可以只是在url中添加哈希值,例如app.js?hash=abcd23,但有些浏览器可能仍然使用缓存版本。
我知道Rails有assets-pipline来处理这个问题,但我对MEAN stack不熟悉。应该有一些npm包可以用于此目的。
如果你想通知用户他们的客户端代码已过期,我认为并不真正需要使用socket.io。你可以在html meta tagjs文件中定义你的版本号,如果不匹配,就弹出一个窗口告诉用户刷新。

2
谢谢!但这是一个SPA(单页应用程序)...所以我不确定HTML元标记和JS文件版本的方法是否可行。用户可以离开浏览器窗口一周,回来后开始使用应用程序而不重新加载,元标记和JS文件版本将保持不变。文件命名点是一个好建议,但同样存在问题-重新加载时修复缓存,但如果用户不重新加载则无法修复。对吧? - Andrew
是的,将哈希添加到文件名中是为了浏览器缓存和CDN。因此,我认为每小时或每天进行一次AJAX调用以查看是否有新版本,然后使用JS刷新页面可能适合您。对于这个来说,socket.io仍然太过复杂。如果您想向应用程序添加实时功能,我建议使用Firebase或Parse而不是使用socket.io。 - at15
非常感谢您的回答! - Andrew

0
  1. 尝试将您的js /文件限制为在较短的周期时间内过期,例如:1天。
  2. 但是,如果您想要一些弹出窗口并告诉用户重新加载(ctrl+f5)他们的浏览器,那么只需创建一个脚本,如果您刚刚更改了某些文件,则弹出该新闻,标记已重新加载/告知重新加载的ip /会话,以便他们不会被多个弹出窗口所困扰。

第一点是合理的。我已将静态资产在CDN上的到期时间从7天改为1天。 - Andrew

0

最近我也遇到了同样的问题。我通过将我的应用程序的构建号附加到我的js/css文件中来解决了这个问题。所有我的脚本和样式标签都是由一个常见的包含文件中的脚本包含的,因此在js/css文件路径的末尾添加“构建号”非常容易,就像这样

/foo/bar/main.js?123

这个 123 是我在同一个头文件中跟踪的数字。每当我想让客户端强制下载应用程序的所有 js 文件时,我都会将其递增。这使我能够控制何时下载新版本,但仍允许浏览器在第一次请求后利用缓存。直到我通过递增构建号来推送另一个更新为止。

这也意味着我可以拥有任意长的缓存过期标头。


0
在构建过程中为本地存储设置唯一键。 我正在使用React Static并加载自己的数据文件,在其中每次更改内容时设置ID。
然后前端客户端从本地存储中读取密钥 (如果密钥不存在,则必须是浏览器的第一次访问) 如果本地存储中的密钥不匹配,则表示内容已更改 触发下面的行以强制重新加载。
window.replace(window.location.href + '?' + key)

在我的情况下,我不得不再次运行这条相同的代码,稍后一秒钟,就像这样

setTimeout( (window.replace(window.location.href + '?' + key))=> {} , 1000)

完整代码如下:

const reloadIfFilesChanged = (cnt: number = 0, manifest: IManifest) => {
    try {
        // will fail if window does not exist
        if (cnt > 10) {
            return;
        }
        const id = localStorage.getItem('id');
        if (!id) {
            localStorage.setItem('id', manifest.id);
        } else {
            if (id !== manifest.id) {
                // manifest has changed fire reload
                // and set new id
                localStorage.setItem('id', manifest.id);
                location.replace(window.location.href + '?' + manifest.id);
                setTimeout(() => {
                    location.replace(window.location.href + '?' + manifest.id + '1');
                }, 1000);
            }
        }
    } catch (e) {
        // tslint:disable-next-line:no-parameter-reassignment
        cnt++;
        setTimeout(() => reloadIfFilesChanged(cnt, manifest), 1000);
    }
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接