最近在尝试改善这套博客主题的可访问性,这意味着我会经常需要调整样式和脚本。每次调整都得登录 CDN 后台重置资源文件的缓存,实在太麻烦了。有没有更简单的方法?
你戳了戳 Bird
rt, send help
很简单,你只要给资源文件带上查询参数就可以了,比如加个版本号啥的
你的页面中只需要指定不同的版本号,自然就不需要手动刷新 CDN 缓存了
我希望你没有自作聪明地去改 CDN 的缓存策略去忽略查询参数,不过最好检查一下有没有谁替你改了
那岂不是我每改动一下 CSS 就得改动一下 HTML 里的查询参数?
不想动手的话就得多动脑
这个参数可以是一个时间戳,也可以是一个版本号,甚至可以是一个随机数
至于怎么选择,以及怎么让 11ty 替你干活,就看你的具体用例了
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) {
return conditionalScripts
.filter((item) => item.predicate(data))
.map(({ path }) => importScript(path))
.join('')
}
不过……
Bird 将状态设置为「空闲」
这样做其实会有一个问题
也不能算是问题吧,只能说是一种必然
因为你的内容页仍然缓存在 CDN 里,所以即使你修改了资产文件,它们也不会立刻应用到既有的页面上
所以在必要的情况下(比如修了什么恶性 bug 之后),你还是需要手动清理全站缓存
或者等页面缓存过期
那绕了这么一大圈,不还是白给
可以考虑让 CI 去清理缓存
不过就目前你的架构来看,单纯地在 CI 构建完成后清理缓存是不行的
应当是你的源站服务器更新好容器之后,再从 CDN 上清理缓存
话说你一个静态博客为啥要设计这么复杂的架构啊
因为我需要在不同的电脑上写东西?
所以肯定得用 git
所以自然就会通过 git 触发 CI 构建
Jenkins 太沉了所以就用了 Actions, 那么自然就会上 Docker 之类的各种东西
许多实现并不是设计指派的,而是因为需求变化而自然演进出来的
这玩意能跑起来本身就是计算机魔法的奇迹
why, thank you!
It's not a compliment.
这中间主要是需要等一个异步的 Docker 镜像更新太麻烦
如果你能同步地部署这个站点,那么就可以把缓存清理集成到 CI 里面
i'll try to figure that out
正在加载评论……