如何防止 @require 缓存外部 js 脚本。

12

我目前正在尝试弄清楚如何在我的Scriptish/Greasemonkey脚本中包含一个位于我的Web服务器上的JavaScript,并使它每次调用用户脚本时重新加载。

我正在编辑我的Web服务器上的脚本,我真的不想每次更改所包含的脚本时都要重新安装用户脚本。

有没有办法解决这个问题?我一直在寻找答案,但迄今为止没有运气。

因此,要明确我的用户脚本如下:

// ==UserScript==
// @id             HET
// @name           SettingsHandler
// @version        1.0
// @namespace      HET
// @require        http://urltoscript/scripts/he/lib.js
// @run-at         document-end
// ==/UserScript==

我的外部脚本看起来像这样:

alert('got it');

所有的测试都非常简单。这个设置是有效的,但只有第一次以及当我更改lib.js脚本时,用户脚本仍会读取旧的脚本。有没有办法防止用户脚本缓存外部脚本?或者有其他的元标记可以帮助我吗?

提前感谢, 戴夫


1
https://dev59.com/q2ox5IYBdhLWcg3wcTzr - Andreas
3
这如何适用于Scriptish或Greasemonkey? - DaMaGeX
1
“@require” 用于安装时的缓存。每个 @require 只会在脚本安装时下载一次,并与脚本一起存储在用户的硬盘上。如果您不想使用缓存,只需将 <script> 标签插入页面即可。 - rampion
1
@rampion 嗯?<script>标签不是有效的JS代码。你怎么在user.js文件中做到这一点?... - Ryan Lee
@RyanLee,Rob M的下面的回答完全符合我的说法。 - rampion
这个答案中大部分的解决方案,甚至在Github讨论中都没有效果,或者例如在刷新时删除断点。这是除了http CORS问题之外的另一个问题,并且还有Fox上缺乏编辑和继续功能。不幸的是,最实用的解决方案是使用file:///和Chrome。 - halt9k
5个回答

6

这是唯一可行的答案: https://github.com/Tampermonkey/tampermonkey/issues/475

推荐选项 4。但它们异步加载,因此加载顺序可能会有所不同。

有几种方法可以缓解您的痛苦。 :)

  1. 您可以在保存脚本之前增加版本号,所有外部资源都将重新加载。
  2. 在将“配置模式”设置为“高级”后,您可以配置外部更新间隔。注意: “始终”仍然意味着在使用资源之后。因此,您可能需要执行/加载页面两次。
  3. 如果您使用 Tampermonkey Beta (Chrome 或 Firefox),则可以直接编辑外部资源(因为现在除了删除按钮之外还有一个编辑按钮)。
  4. 复制资源并将其存储在本地。在启用了 Chrome 的扩展程序管理页面或 Tampermonkey 的设置页面上的“本地文件访问”后,您可以通过本地 file:// URI @require 它们。

5

不确定如何使用GM/userscript指令来完成此操作,但您可以自己添加脚本并将时间戳附加到URL上,以防止浏览器缓存它:

var remoteScript = document.createElement('script');
remoteScript.src = 'http://domain.com/path/to/script.js?ts='+(+new Date());
remoteScript.onload = init;
document.body.appendChild(remoteScript);

function init() {
  ... do stuff
}

4
由于 CSP 阻止您访问 domain.com,因此那样做行不通。 - coulix
@TomášZato:问题说@require指令将指向“他们的Web服务器”上的JS代码,因此我认为本地文件不是问题。 - Alex Quinn

2
Rob M.的答案对我没有用,因为Tampermonkey脚本位置和注入的目标网站可能不同。例如,在我的情况下,我还有一个本地运行的Web服务器,使用Tampermonkey for Firefox开发IDE,而不需要从浏览器中访问任何文件系统以获取Tampermonkey。
这个脚本应该被注入到第三方网站example.com中,然后进行修改。所以浏览器会阻止这个脚本,因为它来自于与example.com不同的域名。
在开发过程中,我想立即应用更改,而不需要任何缓存来获取我的脚本。通过使用`GM.xmlHttpRequest`获取脚本内容解决了这个问题。此外,带有当前时间戳的GET参数作为缓存清除器。
let url = `http://localhost/myscript.js?ts=${(+new Date())}`

GM.xmlHttpRequest({
    method: "GET",
    url: url,
    onload: function(response) {
        let remoteScript = document.createElement('script')
        remoteScript.id = 'tm-dev-script'
        remoteScript.innerHTML = response.responseText
        document.body.appendChild(remoteScript)
    }
})

请注意,由于GM.xmlHttpRequest可以绕过同源策略,因此必须在脚本的头部明确授予访问权限。
// @grant        GM.xmlHttpRequest

这是我唯一的解决方案(不仅因为我将其放在不同的域名上,例如localhost或类似的域名),而且还因为我的Firefox 82强制将http://转换为https://。非常感谢! - Tomáš Diviš
我刚刚弄明白了这个问题,真希望几天前就能找到这个答案。不过日期是个不错的补充。谢谢。 - TYPKRFT

1

对我而言有效的方法是通过 GM_xmlhttpRequest 以文本格式检索我的文件,然后将其包装在 eval() 中,并执行函数,就像使用了 @require 一样。

值得注意的是,使用 eval 是非常危险的,几乎 不应该被使用。尽管如此,在我的情况下,风险较低,这是个例外。

在我的案例中,我有几个不同的脚本需要在我的主应用程序脚本之前运行(例如实用函数)。因此,类似于您根据希望它们运行的时间顺序放置 <script> 标签的方式,我按照先后顺序将它们排列在一个 Array 中。

此外,我将所有的 TM 函数都包装在一个 initialize() 函数内,最后调用该函数以启动我的脚本。

(async function() {
    try {
        const scriptsToExecute = [
            { resource: 'waitForElement', url: 'https://www.example.org/waitForElement.js', },
            { resource: 'utils', url: 'https://www.example.org/utils.js', },
            { resource: 'main', url: 'https://www.example.org/mainApp.js'},
        ];

        const getScripts = await retrieveScripts(scriptsToExecute).catch(e => {debugger;console.error('Error caught @ retrieveScripts',e);});
        if (getScripts?.status !== "success" || !Array.isArray(getScripts?.scripts || getScripts?.find(f => f.status !== "success"))) throw {getScripts};

        try {
            const scripts = getScripts?.scripts;
            const mainAppScript = scripts?.find(f => f?.resource === "main");

            const scriptsToExecute = scripts?.filter(f => f?.resource !== "main");
            for (let i in scriptsToExecute){
                if (scriptsToExecute[i]?.status !== "success" || !scripts[i]?.retrieved) throw {"erroredScript": scripts[i]}
                const thisScript = scripts[i]?.retrieved;
                // eslint-disable-next-line
                eval(thisScript?.script);
            }
            // eslint-disable-next-line
            eval(mainAppScript);
            try {
                // once you've eval'd the script, you can call functions inside that script from within your UserScript environment
                // all my main app scripts are wrapped inside of a function called `initialize()`.
                // though once you've eval'd the script, you can call whatever you want.
                initialize();  
            } catch (err){debugger; console.error('Error caught @ attempting to initialize', err);}
        } catch(err){debugger; console.error('Error caught @ top level', err);}

    } catch (err) {debugger}

    async function retrieveScripts(scriptsToRetrieve){
        try {
            const scriptsContent = await Promise.all(scriptsToRetrieve.map(m => retrieveScript(m))).catch(e => {debugger;});
    
            if (!Array.isArray(scriptsContent) || scriptsContent?.length !== scriptsToRetrieve?.length && scriptsContent?.find(f => f.status !== "success")) {debugger;return {status: "error", msg: "unable to retrieve the script(s) requested.", scriptsContent,};}
            else return {status: "success", "scripts": scriptsContent};
        }
        catch (err){debugger;return {status: "error", msg: "(caught) unable to retrieve the script(s) requested.", scriptsToRetrieve, "error": err, "errorStringified": String(err)};}
    
        function retrieveScript(scriptToRetrieve){
            if (!scriptToRetrieve?.url) return {status: "error", msg: "no url found", scriptToRetrieve};
            try {
                    return new Promise((resolve,reject) => {
                        GM_xmlhttpRequest({
                            method: "GET",
                            url: scriptToRetrieve.url,
                            onerror: function (response) {debugger;return reject({status: "error", response, scriptToRetrieve });},
                            onload: function (response) {
                                if (response?.status !== 200) {debugger;return reject({status: "error", response, scriptToRetrieve });}
                                else {
                                    try {
                                        if (response?.response) {
                                            scriptToRetrieve.script = response.response;
                                            return resolve({status: "success", "retrieved": scriptToRetrieve})
                                        }
                                        else throw {status: "error", "response": response.response, scriptToRetrieve }
                                    } catch (err) {return reject(err);}
                                }
                            }
                        });
                    });
            } catch (err){debugger}
        }
    }

})();

一旦您对脚本进行了 eval ,您现在可以从用户脚本的上下文中运行它们。更重要的是,如果您在使用 eval 的函数内部初始化任何变量,这些变量也将对您可用(如特定于机器的代码)。或者,如果您之前 eval 过脚本,那么所有这些函数也将可用 - 本质上相当于导入它们。


1
这是最棒的解决办法!我把这些函数改成了类方法,并在TM启动时创建了一个类的实例。一切都运行得很顺利。Loader类不需要经常编辑,所以对它进行缓存是可以的,而其他脚本则直接从服务器加载。谢谢! - undefined

-1
您可以将以下内容添加到您的 .htaccess 文件中:
<FilesMatch "filename.js">
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
</FilesMatch>

当你完成开发后,请将其删除。


你知道 Tampermonkey 是什么吗? - Faizan Anwer Ali Rupani
当然,虽然我认为这与问题并不特别相关。 - Sidd
这个答案是正确的。问题中说他们的@require指令将指向“[他们的] Web服务器”上的JS代码。如果该服务器运行Apache,则此.htaccess代码应该可以很好地解决问题。 - Alex Quinn
1
不特定于Tampermonkey。当在Tampermonkey中使用require时,无法解决问题。例如,我的require从GitHib调用原始文件,在那里我管理我的脚本。 Tampermonkey没有更新它,因此需要一种解决方案来停止Tampermonkey缓存脚本并每次调用新的脚本。 - Laurence Cope

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