防止RequireJS缓存所需的脚本

304

RequireJS似乎在内部缓存所需的javascript文件。如果我对其中一个所需的文件进行更改,我必须重命名文件以应用更改。

将版本号作为查询字符串参数附加到文件末尾的常见技巧在requirejs中不起作用:<script src="jsfile.js?v2"></script>

我正在寻找一种方法,可以防止这种内部缓存RequireJS所需的脚本,而无需每次更新后重命名我的脚本文件。

跨平台解决方案:

现在我正在使用urlArgs: "bust="+(new Date())。getTime() 自动清除开发过程中的缓存,以及urlArgs: "bust=v2",生产中我会递增硬编码的版本号以更新所需的脚本。

注意:

@Dustin Getz 在最近的一个回答中提到,当像这样不断刷新Javascript文件时,Chrome开发工具将在调试期间丢弃断点。一个解决方法是在代码中编写debugger; 触发大多数Javascript调试器中的断点。

服务器特定解决方案:

针对您的服务器环境(如Node或Apache)可能效果更好的特定解决方案,请参见下面的一些答案。


你确定这不是服务器或客户端在缓存吗?(我们已经使用必需的JS几个月了,没有注意到类似的情况)IE被捕获了MVC操作结果的缓存,Chrome缓存了我们的HTML模板,但是JS文件似乎在浏览器缓存被重置后都会刷新。我想如果您想利用缓存但无法按照通常的方式进行操作,因为来自必需JS的请求正在删除查询字符串,那可能会引起问题? - PJUK
我不确定RequireJS是否会删除附加的版本号。这可能是我的服务器的问题。有趣的是,RequireJS有一个缓存破坏器设置,所以你可能是对的,它会删除我在所需文件上附加的版本号。 - BumbleB2na
我更新了我的答案,并提供了一个可能的缓存解决方案。 - Dustin Getz
现在我可以将以下内容添加到我今天早上在博客文章中提出的问题清单中:http://codrspace.com/dexygen/does-require-js-introduce-more-problems-than-it-solves-/。而且,不仅需要添加缓存破坏,而且require.js忽略了强制刷新。 - Dexygen
我对这个用例感到困惑...这是为了将AMD模块热重新加载到前端,还是其他什么? - Alexander Mills
在开发过程中,我只是在浏览器中禁用缓存。我认为这是最容易的开发方式。我认为大多数浏览器都有这个功能;我使用Firefox。 - Tigerware
12个回答

461

RequireJS可以被配置,以添加一个值到每个脚本URL上,用于缓存清除。

根据RequireJS文档(http://requirejs.org/docs/api.html#config):

urlArgs: 在RequireJS使用来获取资源的URL后附加额外的查询字符串参数。当浏览器或服务器未正确配置时,最有用的是清除缓存。

例如,将“v2”附加到所有脚本:

require.config({
    urlArgs: "bust=v2"
});

为了开发目的,您可以通过附加时间戳来强制RequireJS绕过缓存:

require.config({
    urlArgs: "bust=" + (new Date()).getTime()
});

46
非常有帮助,谢谢。我在开发过程中使用 urlArgs: "bust=" + (new Date()).getTime() 来自动清除缓存,在生产环境中,我使用 urlArgs: "bust=v2" 来增加硬编码版本号,在推出更新所需脚本后递增版本号。 - BumbleB2na
9
作为性能优化器,您可以使用Math.random()代替(new Date()).getTime()。 这样更加简洁,不会创建对象,并且速度稍微快一些。http://jsperf.com/speedcomparison。 - Vlad Tsepelev
2
你可以让它更小一点:urlArgs: "bust=" + (+new Date) - e382df99a7950919789725ceeec126
13
我认为每次都清空缓存是个糟糕的想法。不幸的是,RequireJS没有其他的替代方案。我们确实使用了urlArgs,但并没有使用随机数或时间戳。相反,我们使用当前的Git SHA码,这样只有在部署新代码时才会更改。 - Ivan Torres
6
如果提供“v2”字符串的文件本身已被缓存,那么“v2”变体如何在生产中工作?如果我发布一个新应用程序到生产环境中,并添加“v3”,那么这不会有任何效果,因为应用程序会继续使用缓存的v2文件,包括具有v2“urlArgs”的旧配置。 - Benny Bottema
显示剩余10条评论

54

不要使用urlArgs!

要求脚本加载遵守http缓存头。(脚本是通过动态插入的 <script>加载的,因此请求看起来就像任何其他加载的资源。)

在开发过程中,请使用正确的HTTP头文件服务于您的JavaScript资源以禁用缓存。

使用require的urlArgs意味着您设置的任何断点都无法在刷新后保留;这样您最终需要在代码中到处放置debugger语句。很糟糕。我使用urlArgs在生产升级期间进行缓存打破,使用git sha来设置我的资源可以永久缓存并且保证从不使用旧的资源。

在开发过程中,我使用一个复杂的mockjax配置来模拟所有的ajax请求,然后我可以将我的应用程序作为仅JavaScript模式提供,使用一个10行Python HTTP服务器关闭所有缓存。这对于我来说已经扩展到一个相当大的“企业”应用程序,其中有数百个restful Web服务端点。我们甚至有一个合同设计师可以与我们的实际产品代码库合作,而不需要让他访问我们的后端代码。


8
@JamesP.Wright,至少在Chrome浏览器中,当你设置一个在页面加载时发生的断点后,点击刷新按钮,由于URL已经改变且Chrome已经删除了断点,所以断点不会被命中。我希望能够知道一个仅限客户端的解决方法。 - Drew Noakes
1
谢谢,达斯汀。如果你找到了解决方法,请发布一下。同时,您可以在代码中使用 debugger; 来设置断点。 - BumbleB2na
2
对于使用Node上的http-server(npm install http-server)的任何人来说,您也可以通过-c-1禁用缓存(即http-server -c-1)。 - Yourpalal
5
您可以在Chrome中禁用缓存来解决开发期间的调试问题:https://dev59.com/rW025IYBdhLWcg3w_a6r - Deepak Joy Cheenath
6
在生产环境中不要使用urlArgs参数!可以想象一下,如果你的网站有1000个JS文件(是的,可能会有这么多!),并且它们的加载由requiredJS控制。现在你发布了v2版本或者更新了网站,只有少量的JS文件发生了改变!但是通过添加urlArgs=v2参数,你强制重新加载所有1000个JS文件!这将消耗大量的流量!应该只重新加载修改过的文件,其他文件应该以状态304(未修改)进行响应。 - walv
显示剩余6条评论

24
“urlArgs”解决方案存在问题。不幸的是,您无法控制可能位于您和用户Web浏览器之间的所有代理服务器。其中一些代理服务器可能被不幸地配置为在缓存文件时忽略URL参数。如果发生这种情况,将向用户提供错误版本的JS文件。
我最终放弃了并直接在require.js中实现了自己的修复。如果您愿意修改requirejs库的版本,则此解决方案可能适用于您。
您可以在此处查看补丁:

https://github.com/jbcpollak/requirejs/commit/589ee0cdfe6f719cd761eee631ce68eee09a5a67

添加后,您可以在您的 require 配置中执行以下操作:
var require = {
    baseUrl: "/scripts/",
    cacheSuffix: ".buildNumber"
}

使用您的构建系统或服务器环境将buildNumber替换为修订ID /软件版本/喜爱颜色。

像这样使用require:
require(["myModule"], function() {
    // no-op;
});

会导致需要请求此文件:
http://yourserver.com/scripts/myModule.buildNumber.js

在我们的服务器环境中,我们使用URL重写规则来剥离buildNumber,并提供正确的JS文件。这样,我们实际上不必担心重命名所有JS文件。
该补丁将忽略指定协议的任何脚本,并且不会影响任何非JS文件。
这对我的环境很有效,但我意识到有些用户更喜欢前缀而不是后缀,所以修改我的提交以适应您的需求应该很容易。
更新:
在拉取请求讨论中,requirejs作者建议将修订号作为前缀可能起作用。
var require = {
    baseUrl: "/scripts/buildNumber."
};

我没有尝试过这个,但暗示是会请求以下URL:
http://yourserver.com/scripts/buildNumber.myModule.js

这可能非常适合许多人使用前缀。
以下是一些可能重复的问题: RequireJS 和代理缓存 require.js - 如何在 URL 的一部分上设置所需模块的版本?

1
我非常希望看到你的更新进入官方的 requirejs 构建中。可能还会吸引 requirejs 的主要作者的兴趣(请参见上面 @Louis 的答案)。 - BumbleB2na
@BumbleB2na - 欢迎在 PullRequest (https://github.com/jrburke/requirejs/pull/1017) 上发表评论,jrburke 似乎不太感兴趣。他建议使用文件名前缀来解决问题,我会更新我的答案以包含这个建议。 - JBCP
1
不错的更新。我觉得我喜欢作者的建议,但那只是因为我最近一直在使用这种命名约定:/scripts/myLib/v1.1/。我曾尝试给我的文件名添加后缀(或前缀),可能是因为jQuery也是这样做的,但过了一段时间后,我变得懒惰并开始在父文件夹上递增版本号。我觉得这对于我在一个大型网站上进行维护更加方便,但现在你让我担心URL重写的噩梦。 - BumbleB2na
1
现代前端系统只需重写JS文件并在文件名中使用MD5校验和,然后在构建时重写HTML文件以使用新的文件名,但对于由服务器端提供前端代码的遗留系统来说,这变得棘手。 - JBCP
这段程序相关的内容是否能在jspx文件中嵌入一些JS代码?像这样: - masT
@masT - 是的,它可以工作,但是你需要使用以下代码替换data-main: <script> var require = { baseUrl: "/scripts/", cacheSuffix: ".buildNumber" }</script><script src="${pageContext.request.contextPath}/resources/scripts/require.js"> <jsp:text/> </script> <script> require([ 'main', 'dev/module' ]); </script> - JBCP

19

Expire cache on require.js data-main的启发,我们使用以下ant任务更新了我们的部署脚本:

<target name="deployWebsite">
    <untar src="${temp.dir}/website.tar.gz" dest="${website.dir}" compression="gzip" />       
    <!-- fetch latest buildNumber from build agent -->
    <replace file="${website.dir}/js/main.js" token="@Revision@" value="${buildNumber}" />
</target>

main.js 的开头看起来像:

require.config({
    baseUrl: '/js',
    urlArgs: 'bust=@Revision@',
    ...
});

11

在生产环境中

urlArgs 可能会引起问题!

requirejs 的首席作者不建议使用 urlArgs:

对于已部署的资源,我更喜欢将完整的版本或哈希值作为构建目录,并仅修改项目使用的 baseUrl 配置以将该版本化目录用作 baseUrl。然后没有其他文件会改变,它可以避免一些代理问题,其中代理可能不会缓存带有查询字符串的 URL。

[样式是我的加粗]

我会遵循这个建议。

在开发环境中

我更喜欢使用一个智能缓存可能经常发生变化的文件的服务器:一个根据需要发出 Last-Modified 并响应 If-Modified-Since 的 304 的服务器。即使基于 Node 的express设置为服务静态文件,也可以直接进行此操作。它不需要对浏览器进行任何操作,并且不会破坏断点。


好的观点,但是你的答案只适用于你的服务器环境。也许对于其他人来说,一个好的替代方案是最近的建议,即在文件名中添加版本号,而不是查询字符串参数。这里有更多关于这个主题的信息:http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/ - BumbleB2na
我们在生产系统中遇到了这个具体问题。我建议修改文件名而不是使用参数。 - JBCP

7
我从AskApache网站上复制了以下代码段,并将其放入我本地 Apache 网页服务器的一个单独的 .conf 文件中(在我的情况下是 /etc/apache2/others/preventcaching.conf):
<FilesMatch "\.(html|htm|js|css)$">
FileETag None
<ifModule mod_headers.c>
Header unset ETag
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "Wed, 11 Jan 1984 05:00:00 GMT"
</ifModule>
</FilesMatch>

对于开发阶段,这个方法可以正常工作,不需要改变代码。至于生产环境,我可能会使用 @dvtoever 的方法。


6

快速解决开发问题

针对开发,您可以在Chrome Dev Tools中禁用缓存禁用Chrome浏览器缓存以进行网站开发)。只有当Dev Tools对话框打开时才会禁用缓存,所以您不必担心每次进行常规浏览时都要切换此选项。

注意:在生产环境中使用“urlArgs”是正确的解决方案,以便用户获取最新代码。但这使得调试变得困难,因为每次刷新时Chrome都会无效化断点(因为每次服务的都是一个“新”的文件)。


3
我不建议使用 'urlArgs' 作为 RequireJS 缓存破裂的解决方案。这并没有完全解决问题。更新版本号将导致下载所有资源,即使您只更改了一个资源。
为了解决这个问题,我建议使用 Grunt 模块(如 'filerev')创建修订号。除此之外,我还在 Gruntfile 中编写了一个自定义任务,以在需要时更新修订号。
如果需要,我可以分享此任务的代码片段。

我使用grunt-filerev和grunt-cache-buster的组合来重写Javascript文件。 - Ian Jamieson

2
这是我在Django/Flask中的做法(可以轻松适应其他语言/VCS系统):
在你的config.py中(我使用python3,所以您可能需要调整python2中的编码),请按以下方式进行设置:
import subprocess
GIT_HASH = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip().decode('utf-8')

然后在您的模板中:

{% if config.DEBUG %}
     require.config({urlArgs: "bust=" + (new Date().getTime())});
{% else %}
    require.config({urlArgs: "bust=" + {{ config.GIT_HASH|tojson }}});
{% endif %}
  • 不需要手动构建过程
  • 应用程序启动时仅运行一次git rev-parse HEAD,并将其存储在config对象中

0

动态解决方案(无需urlArgs)

针对这个问题,有一个简单的解决方案,可以为每个模块加载唯一的修订号。

您可以保存原始的requirejs.load函数,用自己的函数覆盖它,并将修改后的URL解析到原始的requirejs.load中:

var load = requirejs.load;
requirejs.load = function (context, moduleId, url) {
    url += "?v=" + oRevision[moduleId];
    load(context, moduleId, url);
};

在我们的构建过程中,我使用了“gulp-rev”来构建一个包含所有正在使用的模块的所有版本的清单文件。以下是我的gulp任务的简化版本:
gulp.task('gulp-revision', function() {
    var sManifestFileName = 'revision.js';

    return gulp.src(aGulpPaths)
        .pipe(rev())
        .pipe(rev.manifest(sManifestFileName, {
        transformer: {
            stringify: function(a) {
                var oAssetHashes = {};

                for(var k in a) {
                    var key = (k.substr(0, k.length - 3));

                    var sHash = a[k].substr(a[k].indexOf(".") - 10, 10);
                    oAssetHashes[key] = sHash;
                }

                return "define([], function() { return " + JSON.stringify(oAssetHashes) + "; });"
            }
        }
    }))
    .pipe(gulp.dest('./'));
});

这将生成一个带有版本号的AMD模块到moduleNames,作为“oRevision”包含在main.js中,在那里您可以像之前展示的那样覆盖requirejs.load函数。


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