几年来,几乎所有有关高级缓存方法的文章都建议在生产中使用以下技术:
- 将有关文件中包含的数据版本的信息添加到文件名中(通常以文件中数据的哈希形式)。
- 设置HTTP标头
Cache-Control: max-age
和Expires
,它们控制材料的缓存时间(这消除了对返回到资源的访问者的相关材料的重新验证)。

我所知道的所有用于构建项目的工具都支持将其添加到哈希文件中来命名其内容。 使用简单的配置规则即可完成此操作(如下所示):
filename: '[name]-[contenthash].js'
对这项技术的如此广泛的支持导致了这种实践变得极为普遍的事实。
Web项目性能专家还建议使用
代码分离技术。 这些技术允许将JavaScript代码分解为单独的捆绑包。 这样的捆绑包可以由浏览器并行下载,或者仅在必要时才根据浏览器的要求下载。
尤其是与高级缓存技术相关的代码分离的众多优点之一是,对单独的源文件进行更改不会使整个包的缓存无效。 换句话说,如果为开发人员“ X”创建的npm软件包发布了安全更新,并且开发人员对
node_modules
的内容
node_modules
分段,则只有包含“ X”创建的软件包的片段才需要更改。
这里的问题是,如果将所有这些组合在一起,那么很少会导致长期数据缓存的效率提高。
实际上,对源代码文件之一的更改几乎总是导致软件包组装系统的多个输出文件失效。 正是由于这样的事实,即已在文件名中添加了散列,以反映这些文件内容的版本。
文件名版本问题
假设您已经创建并部署了一个网站。 您已使用代码拆分功能,因此,大多数站点的JavaScript代码均应请求加载。
在下一个依赖关系图中,您可以看到代码库入口点
dep1
的根片段,以及三个异步加载的依赖关系片段
dep1
,
dep2
和
dep3
。 还有一个
vendor
片段,其中包含来自
node_modules
所有站点依赖
node_modules
。 根据缓存准则,所有文件名都包含这些文件内容的哈希值。
典型的JavaScript模块依赖关系树由于
dep2
和
dep3
代码段是从
vendor
代码
dep3
导入模块的,因此在项目
dep3
生成的代码的上部,我们很可能会找到类似于以下内容的导入命令:
import {...} from '/vendor-5e6f.mjs';
现在让我们考虑一下如果
vendor
片段的内容发生更改会发生什么。
如果发生这种情况,相应文件名称中的哈希也会更改。 并且由于此文件名称的链接位于
dep2
和
dep3
的导入命令中,因此有必要更改这些导入命令:
-import {...} from '/vendor-5e6f.mjs'; +import {...} from '/vendor-d4a1.mjs';
但是,由于这些导入命令是
dep2
和
dep3
内容的一部分,因此更改它们意味着
dep2
和
dep3
文件内容的哈希也
dep2
dep3
。 这意味着这些文件的名称也会更改。
但这还不止于此。 由于
main
片段导入了
dep2
和
dep3
,并且它们的文件名已更改,因此
main
的import命令也将更改:
-import {...} from '/dep2-3c4d.mjs'; +import {...} from '/dep2-2be5.mjs'; -import {...} from '/dep3-d4e5.mjs'; +import {...} from '/dep3-3c6f.mjs';
最后,由于
main
文件的内容已更改,因此该文件的名称也必须更改。
这就是依赖关系图的外观。
依赖树中的模块受树的叶子节点之一的代码的单个更改的影响此示例说明了仅在一个文件中进行的少量代码更改如何导致80%的捆绑包片段的缓存无效的情况。
在理想情况下,虽然并非所有更改都会导致这种可悲的后果(例如,使叶节点缓存无效会导致使所有直到根节点的缓存无效,但使根缓存无效不会导致级联无效到达叶捕获),这是事实。我们将不必处理任何不必要的缓存失效。
这导致我们面临以下问题:“是否有可能获得不可变资源和长期缓存的好处,而又不会遭受级联的缓存失效的困扰?”
解决问题的方法
从技术的角度来看,文件名中文件内容的哈希值存在问题,并不是哈希值存在于名称中。 这是因为这些哈希值出现在其他文件中。 因此,在更改它们所依赖的文件名中的哈希值时,将禁用这些文件的缓存。
解决此问题的方法是使用上述示例的语言,从而可以通过
dep2
和
dep3
导入
vendor
片段,而无需指定
vendor
片段文件的版本信息。 这样做时,您必须确保下载的
vendor
版本正确,并考虑了
dep2
和
dep3
的当前版本。
事实证明,有几种方法可以实现此目标:
考虑这些机制。
方法1:导入卡
导入映射是级联缓存失效的最简单解决方案。 另外,此机制最容易实现。 但是,不幸的是,只有Chrome支持此功能(此外,必须显式
启用此功能)。
尽管如此,我还是想从有关进口卡的故事开始,因为我确信这个决定将来会成为最常见的。 另外,使用导入卡的描述将有助于解释解决我们问题的其他方法的功能。
使用导入映射来防止级联的缓存失效包括三个步骤。
▍步骤1
您需要配置捆绑程序,以便在构建项目时它不包含文件名称中的哈希值。
如果组装的项目的模块显示在上一个示例的图表中,而没有在文件名中包含其内容的哈希,则项目输出目录中的文件将如下所示:
dep1.mjs dep2.mjs dep3.mjs main.mjs vendor.mjs
相应模块中的导入命令也将不包含哈希:
import {...} from '/vendor.mjs';
▍步骤2
您需要使用诸如
rev-hash之类的工具,并使用该工具生成每个文件的副本,并在其名称上添加一个哈希值以指示其内容的版本。
完成这部分工作之后,输出目录的内容应类似于以下所示(请注意,现在每个文件都有两个选项):
dep1-b2c3.mjs", dep1.mjs dep2-3c4d.mjs", dep2.mjs dep3-d4e5.mjs", dep3.mjs main-1a2b.mjs", main.mjs vendor-5e6f.mjs", vendor.mjs
▍步骤3
您需要创建一个JSON对象,该对象存储有关名称中没有哈希的每个文件与名称中没有哈希的每个文件的对应关系的信息。 该对象需要添加到HTML模板中。
此JSON对象是导入映射。 可能是这样的:
<script type="importmap"> { "imports": { "/main.mjs": "/main-1a2b.mjs", "/dep1.mjs": "/dep1-b2c3.mjs", "/dep2.mjs": "/dep2-3c4d.mjs", "/dep3.mjs": "/dep3-d4e5.mjs", "/vendor.mjs": "/vendor-5e6f.mjs", } } </script>
此后,每当浏览器看到位于与导入映射的键之一相对应的地址处的文件的导入命令时,浏览器都会导入与键值匹配的文件。
如果使用此导入映射作为示例,则可以发现引用
/vendor.mjs
文件的import命令实际上将查询并加载
/vendor-5e6f.mjs
文件:
这意味着模块的源代码可以很容易地引用不包含哈希的模块的文件名,并且浏览器将始终下载名称包含有关其内容版本信息的文件。 并且,由于模块的源代码中没有散列(它们仅存在于导入映射中),因此对这些散列的更改不会导致模块的无效,除非其内容确实发生了变化。
也许您现在想知道为什么我要创建每个文件的副本而不是仅重命名文件。 这对于支持无法使用导入地图的浏览器是必需的。 在前面的示例中,此类浏览器只会看到
/vendor.mjs
文件,而只是像通常一样下载该文件,遇到类似的结构。 结果,事实证明这两个文件必须在服务器上存在。
如果您想查看导入映射的实际作用,这里有
一些示例 ,这些
示例演示了解决本文中显示的级联缓存失效问题的所有方法。 另外,如果您有兴趣学习如何为每个文件生成导入映射和版本哈希,请查看
项目程序集的
配置 。
待续...
亲爱的读者们! 您是否知道级联的缓存失效?
