最近在尝试改善这套博客主题的可访问性,这意味着我会经常需要调整样式和脚本。每次调整都得登录 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('')
}











