【问题标题】:How do I cache bust imported modules in es6?如何在 es6 中缓存 bust 导入的模块?
【发布时间】:2018-05-20 09:48:28
【问题描述】:

ES6 模块允许我们像这样创建一个入口点:

// main.js

import foo from 'foo';

foo()
<script src="scripts/main.js" type="module"></script>

foo.js 将存储在浏览器缓存中。在我将 foo.js 的新版本推向生产之前,这是可取的。

通常的做法是添加具有唯一 id 的查询字符串参数以强制浏览器获取新版本的 js 文件 (foo.js?cb=1234)

如何使用 es6 模块模式实现这一点?

【问题讨论】:

  • 真棒的问题!终于找到了一个道理。只是谢谢你的提问!我对所有那些花哨的 ES 模块的主要担心是缓存破坏。每次网站更新时,它都会在竞争条件下失败。我喜欢 CommonJS 的动态导入:const a = require(b ? 'm1' : 'm2')import a from (b ? 'm1' :'m2') 不行。
  • 您是否设法为此@spinners 找到解决方案?我也有同样的问题!
  • 您找到解决方案了吗?这个问题目前对我来说是一个重大问题,我很惊讶这似乎是网络上唯一关于它的问题。我已经求助于在我的 Javascript 文件中的每个导入语句的末尾添加“?v=x.x.x”。所以我有 'import foo from 'foo?v=1.x.x';对于我所有的模块。这是一个非常烦人的解决方案,因为这意味着我在每个版本中都修改了源代码控制中的数百个文件,只是因为我必须修改版本号,即使文件中的实际代码没有更改。非常感谢任何信息。
  • ^^ 这就是我最终做的事情
  • 2020年还有没有更好的办法?我阅读了建议 Etag 的答案,它很优雅,但并不总是可以配置服务器。

标签: web browser-cache es6-modules


【解决方案1】:

HTTP 标头来拯救。使用作为文件校验和的ETag 提供您的文件。 S3 以 by default 为例。 当您尝试再次导入文件时,浏览器将请求该文件,这次将 ETag 附加到“if-none-match”标头:服务器将验证 ETag 是否与当前文件匹配并返回 304 Not Modified,节省带宽和时间,或文件的新内容(带有新的 ETag)。

这样,如果您更改项目中的单个文件,用户将不必下载所有其他模块的全部内容。明智的做法是添加一个简短的max-age 标头,这样如果同一模块在短时间内被请求两次,则不会有额外的请求。

如果您添加缓存破坏(例如,通过捆绑程序附加 ?x={randomNumber},或将校验和添加到每个文件名),您将强制用户在每个新项目版本中下载每个必要文件的完整内容。

在这两种情况下,您无论如何都要对每个文件进行请求(级联上导入的文件会产生新的请求,如果您使用 etags,至少可能以小 304 结尾)。为避免这种情况,您可以使用动态导入 e.g if (userClickedOnSomethingAndINeedToLoadSomeMoreStuff) { import('./someModule').then('...') }

【讨论】:

  • 使用ETag当然仍然会导致每个模块有1个HTTP请求,因此可能会有很多开销 - 添加max-age会导致更多问题,因为模块会单独过期,这意味着有人可能会得到不兼容的模块版本部署更新时。在文件名中使用内容校验和进行缓存破坏只会强制更新更改的模块,因此这是我所知道的最佳策略 - 并且可能是唯一让您永久缓存、没有 HTTP 开销和精确更新的策略。太糟糕了,我们仍然必须使用捆绑器来获得适当的缓存。 ?
【解决方案2】:

对于所有这些,有一个不涉及查询字符串的解决方案。假设您的模块文件位于/modules/ 中。导入模块时使用相对模块分辨率./../,然后在服务器端重写路径以包含版本号。使用/modules/x.x.x/ 之类的东西,然后将路径重写为/modules/。现在您可以通过包含您的第一个模块来获得模块的全局版本号 <script type="module" src="/modules/1.1.2/foo.mjs"></script>

或者如果您无法重写路径,则只需在开发过程中将文件放入文件夹/modules/version/,并将version 文件夹重命名为版本号并在发布时更新脚本标签中的路径。

【讨论】:

  • 恕我直言,这是最好的选择。新版本只是进入一个新目录,这样旧的和新的都可以工作并得到支持。然后当你决定是时候删除旧版本了。
【解决方案3】:

在我看来,dynamic imports 可能是这里的解决方案。

步骤 1) 使用 gulp 或 webpack 创建清单文件。你有这样的映射:

export default {
    "/vendor/lib-a.mjs": "/vendor/lib-a-1234.mjs",
    "/vendor/lib-b.mjs": "/vendor/lib-b-1234.mjs"
};

步骤 2) 创建一个文件函数来解析你的路径

import manifest from './manifest.js';

const busted (file) => {
 return manifest[file];
};

export default busted;

步骤 3) 使用动态导入

import busted from '../busted.js';

import(busted('/vendor/lib-b.mjs'))
  .then((module) => {
    module.default();
});

我在 Chrome 中试了一下,它可以工作。处理相对路径在这里很棘手。

【讨论】:

    【解决方案4】:

    目前只是一个想法,但您应该能够让 Webpack 将内容哈希放入所有拆分包中,并将该哈希写入您的导入语句中。我相信它默认是第二个。

    【讨论】:

    • 感谢您的回答。您有可以用来支持您的主张的示例或文档吗?
    【解决方案5】:

    我创建了一个Babel plugin,它为每个模块名称添加了一个内容哈希(静态和动态导入)。

    import foo from './js/foo.js';
    
    import('./bar.js').then(bar => bar());
    

    变成

    import foo from './js/foo.abcd1234.js';
    
    import('./bar.1234abcd.js').then(bar => bar());
    

    然后您可以使用 Cache-control: immutable 让 UA(浏览器、代理等)无限期地缓存这些版本化的 URL。一些max-age 可能更合理,具体取决于您的设置。

    您可以在开发(和测试)期间使用原始源文件,然后转换和缩小文件以用于生产。

    【讨论】:

      【解决方案6】:

      我所做的是处理网络服务器中的缓存破坏(在我的实例中为 nginx)

      而不是服务

      <script src="scripts/main.js" type="module"></script>
      

      像这样提供它,其中 123456 是您的缓存破坏键

      <script src="scripts/123456/main.js" type="module"></script>
      

      并在 nginx 中包含一个位置,例如

      location ~ (.+)\/(?:\d+)\/(.+)\.(js|css)$ {
        try_files $1/$2.min.$3 $uri;
      }
      

      请求 scripts/123456/main.js 将提供 scripts/main.min.js 和一个更新密钥将导致提供新文件,此解决方案也适用于 cdns。

      【讨论】:

        【解决方案7】:

        您可以使用 ETags,正如之前的回答所指出的那样,或者将Last-ModifiedIf-Modified-Since 结合使用。

        这是一种可能的情况:

        1. 浏览器首先加载资源。服务器以Last-Modified: Sat, 28 Mar 2020 18:12:45 GMTCache-Control: max-age=60 响应。
        2. 如果第二次发起请求的时间早于第一次发起的 60 秒,则浏览器会从缓存中提供文件,并且不会向服务器发出实际请求。
        3. 如果在 60 秒后发起请求,浏览器将认为缓存文件已过期,并发送带有 If-Modified-Since: Sat, 28 Mar 2020 18:12:45 GMT 标头的请求。服务器将检查此值并:
          • 如果文件在上述日期之后被修改,它将发出 200 响应,并在正文中包含新文件。
          • 如果文件在该日期之后没有修改,服务器将发出304“未修改”状态,正文为空。

        我最终为 Apache 服务器设置了这个:

        <IfModule headers_module>
          <FilesMatch "\.(js|mjs)$">
            Header set Cache-Control "public, must-revalidate, max-age=3600"
            Header unset ETag
          </FilesMatch>
        </IfModule>
        

        您可以根据自己的喜好设置max-age

        我们必须取消设置 ETag。否则 Apache 每次都会以200 OK 响应(it's a bug)。此外,如果使用基于修改日期的缓存,则不需要它。

        【讨论】:

          【解决方案8】:

          如果您使用 Visual Studio 2022 和 TypeScript 编写代码,则可以遵循在脚本文件名中添加版本号的约定,例如MyScript.v1.ts。当您进行更改并将文件重命名为 MyScript.v2.ts 时,Visual Studio 会显示类似于以下内容的以下对话框:

          如果您单击Yes,它将继续并更新所有导入此模块的文件以引用MyScript.v2.ts 而不是MyScript.v1.ts。浏览器也会注意到名称更改并按预期下载新模块。

          这不是一个完美的解决方案(例如,如果您重命名一个频繁使用的模块,最终可能会更新很多文件)但它是一个简单的解决方案!

          【讨论】:

            【解决方案9】:

            一个让我想到但我不会使用的解决方案,因为我不喜欢它,哈哈

            window.version = `1.0.0`;
            
            let { default: fu } = await import( `./bar.js?v=${ window.version }` );
            

            使用导入“方法”允许您传入模板文字字符串。我还将它添加到窗口中,以便无论我导入 js 文件有多深都可以轻松访问它。我不喜欢它的原因是我必须使用“await”,这意味着它必须被包装在一个异步方法中。

            【讨论】:

              【解决方案10】:

              使用相对路径对我有用:

              import foo from './foo';
              

              import foo from './../modules/foo';
              

              而不是

              import foo from '/js/modules/foo';
              

              编辑

              由于此答案被否决,因此我对其进行了更新。模块并不总是重新加载。第一次,您必须手动重新加载模块,然后浏览器(至少 Chrome)将“理解”文件被修改,然后每次更新时重新加载文件。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2018-02-07
                • 2016-05-16
                • 2016-06-10
                • 2017-01-04
                • 1970-01-01
                • 1970-01-01
                • 2017-03-07
                相关资源
                最近更新 更多