惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
量子位
D
DataBreaches.Net
博客园 - 司徒正美
J
Java Code Geeks
博客园 - 【当耐特】
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
aimingoo的专栏
aimingoo的专栏
B
Blog
The Cloudflare Blog
D
Docker
I
InfoQ
爱范儿
爱范儿
MongoDB | Blog
MongoDB | Blog
腾讯CDC
月光博客
月光博客
Hugging Face - Blog
Hugging Face - Blog
Microsoft Azure Blog
Microsoft Azure Blog
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
小众软件
小众软件
S
SegmentFault 最新的问题
GbyAI
GbyAI
有赞技术团队
有赞技术团队

豆沙工作室

不想(二)十七岁 小点阵字 Get an Oscilloscope 杰理 WTS 格式音频转换和打包 [11ty] 增量式地刷新 CDN 缓存 站起来,为你的成果答辩吧 公理还是定理? [11ty] 无障碍设计:正确地标识图和表 停止 Mac 上的时间机器备份 从 Gitea 迁移到 Forgejo 评论里的私信消息 记得清理你的日志文件 招魂 Annihilation [11ty] 使用 Pagefind 实现静态站点的搜索 直面死亡 [11ty] 修复 RSS 里的数学公式 看到了一个神奇的 UA PreFound:10B - 四次击键 一点小小的 AI 震撼 PreFound:10A - 文本之外,还有(绘)文字 PreFound:09 - 色彩运算子 计算机不予申辩 Re-Game 2.0 Addendum C - Press START PreFound:08 - 动效与仿射 PreFound:07 - 图片拼贴报 PreFound:06 - 漫游雪花海 PreFound:05 - 写出到屏幕 And Trek I Will PreFound:04 - 举起渲染器
[11ty] 处理资产文件的 CDN 缓存问题
dousha99 · 2026-08-05 · via 豆沙工作室

最近在尝试改善这套博客主题的可访问性,这意味着我会经常需要调整样式和脚本。每次调整都得登录 CDN 后台重置资源文件的缓存,实在太麻烦了。有没有更简单的方法?

你戳了戳 Bird

Bird:

很简单,你只要给资源文件带上查询参数就可以了,比如加个版本号啥的

Bird:

你的页面中只需要指定不同的版本号,自然就不需要手动刷新 CDN 缓存了

Bird:

我希望你没有自作聪明地去改 CDN 的缓存策略去忽略查询参数,不过最好检查一下有没有谁替你改了

dousha99:

那岂不是我每改动一下 CSS 就得改动一下 HTML 里的查询参数?

Bird:

这个参数可以是一个时间戳,也可以是一个版本号,甚至可以是一个随机数

Bird:

至于怎么选择,以及怎么让 11ty 替你干活,就看你的具体用例了

Bird:

someone's calling me. toodles

Bird 将状态设置为「会议中」

好吧,自己动手!

最符合直觉的设计参数自然是自增的版本号,比如 ?v=1. 当然,这么做就会落入到之前的需要手动修改的问题里。所以我们需要想办法自动地生成设计参数。

一个比较理想的方案是使用文件本身的摘要值,这样有改动的文件会生成一个新的参数;而之前没有改动的文件就会保留原有的参数。这样一来,我们既不会因为每次小改动都导致 CDN 需要重新拉取全部的资源文件;也不需要手动指定哪些文件的查询参数需要修改。

在 11ty 下,不同语言的模板之间可以通过 renderFile 指令混用。考虑到计算文件的摘要值会不可避免地用到 JavaScript, 我们就需要用到 11ty 的 JS 模板了。

新建一个文件 lib.11ty.cjs 作为生成对应 HTML 片段的模板。

从 NodeJS 中调取 crypto 来计算摘要值

const { createHash } = require('crypto')

function calculateHashValue(str) {
  const hash = createHash('sha256');
  hash.update(str);
  return hash.digest('hex');
}

为了 URL 简短一些,我们可以只取摘要值开头的 8 位数:

function calculateShortHash(str) {
  return calculateHashValue(str).substring(0, 8);
}

再从 NodeJS 中调取 fs 来读文件内容:

const { readFileSync, existsSync } = require('fs');

function calculateFileShortHash(path) {
  if (!path) {
    return ''
  }

  if (!existsSync(path)) {
    return ''
  }

  return calculateShortHash(readFileSync(path));
}

在我的 11ty 结构中,CSS 等资产文件是放在工程目录的 assets/ 文件夹内,并直接复制到产物目录中的。这使得我可以直接写 assets/css/style.css 来在 HTML 和 11ty 编译时指代同一份文件,不需要做额外的转换。所以生成 URI 的函数也十分好写:

function generateLibraryIncludeUri(path) {
  return `${path}?v=${calculateFileShortHash(path)}`
}

然后就是比较狂野的 JS/HTML 混成时间:

function importStyleSheet(path) {
  return `<link rel="stylesheet" href="/${generateLibraryIncludeUri(path)}">`
}

function importScript(path) {
  return `<script src="/$generateLibraryIncludeUri(path)}></script>`
}

11ty 的 JavaScript 模板通过 render 函数返回渲染结果,我们只需要返回组装好的 HTML 字符串即可:

const stylesheets = [ 'assets/css/style.css', 'assets/css/icon.css', /* ... */ ]
const scripts = [ 'assets/js/jquery.min.js', 'assets/js/script.js', /* ... */ ]

function render() {
  const styleString = stylesheets.map(importStyleSheet).join('')
  const scriptString = scripts.map(importScript).join('')
  
  return `${styleString}${scriptString}`
}

module.exports = render;
module.exports.default = render;
// ^ 别忘了导出渲染函数

在其他模板文件里即可使用,比如在 Liquid 模板下就可以按如下语法调用:

{% renderFile 'lib.11ty.js' %}

需要注意到:renderFile 的路径是相对于工程本身的,而不是相对于 _include 目录的。

条件导入

之前选择 11ty 的一个爽点就是可以通过文件头指定哪些库是必要的,尤其是比较沉的库(比如 Graphviz 或者 abc.js)就不需要每个页面都得加载。

但是 JavaScript 模板由于是通过 renderFile 调用的,不会自动收到文件头数据。我们需要将文件头整理一下再转发过去。

eleventy.config.js 中新建一个 Liquid 函数:

export default function (eleventyConfig) {
  eleventyConfig.addLiquidFilter('allData', function () {
    return this.context.getAll()
  })
}

然后修改模板中的调用方式:

{% assign renderData = page | allData %}
{% renderFile 'lib.11ty.js', renderData %}

再修改一下 lib.11ty.js 来接收这个数据:

function render(data) {
  // data 将包含文件头中的数据,比如 data.viz 对应文件头中 viz 的值
}

这样,我们就可以根据文件头的信息动态地引入资产文件了:

const conditionalScripts = [
  {
    predicate: (data) => !!data.viz,
    path: 'assets/js/viz-standalone.js',
  },
  {
    predicate: (data) => !!data.viz,
    path: 'assets/js/viz-invoker.js',
  },
]

function renderConditionalScript(data) {
  retrun conditionalScripts
    .filter((item) => item.predicate(data))
    .map(({ path }) => importScript(path))
    .join('')
}