大部分评论区系统(如 Valine、Twikoo、Disqus 等)默认都采用 Gravatar 作为头像源。其工作流程是:系统根据访客留下的邮箱地址,计算出一个唯一的 MD5 哈希值,然后将这个哈希值拼接到 Gravatar 的官方路径或自建的 CDN 路径后,形成一个可访问的图片 URL。浏览器通过这个 URL 去请求头像。

如果 Gravatar 服务器上找不到对应邮箱注册的头像,就会 301 返回一张预设的系统默认图片 /static/img/nobody/80.png。这正是我一直感到不舒服的地方:明明每个评论者都可以拥有独特的视觉标识,却因为这套机制的默认行为,导致大量无头像访客在评论区里顶着千篇一律的“匿名蝴蝶”图标,破坏了页面的整体视觉一致性。

并不是所有中国用户都会特意去 Gravatar 官网注册并上传头像,毕竟这个服务在国内的知晓度和使用习惯都不算高。有人可能会说,那 GitHub 不是也有头像吗?按理说 Gravatar 可以从 GitHub 账号抓取头像,但实际情况是这需要用户提前在 Gravatar 中绑定 GitHub 账号,流程相对繁琐,绝大多数人并不会这么做。

所以最终结果就是,在我博客评论区留言的朋友们,十有八九都顶着那个蓝蓝背景的默认头像。每次翻看留言,看着一串串相同的面孔,心里总归不是滋味。针对这个痛点,其实是有解决方案的——既然默认规则不友好,那我干脆为这些常来留言的熟人写一个私有的头像解析服务。这个方案具备通用性,不依赖某款评论系统,只要是基于 Gravatar 机制的系统都可以适用。

这类评论系统有一个共同的技术特点:评论区是直接嵌入在宿主网页中的 DOM 节点,而不是通过 <iframe> 隔离的独立文档。这就意味着,网站原有的 CSS 样式规则能够穿透并作用于评论区域内的所有元素。利用 CSS 的属性选择器 img[],我们可以筛选出页面上所有图片标签。

若要定位到那些默认头像或特定 Gravatar 头像,只需在中括号内写入 src= 并跟上对应的图片地址即可。更进一步,如果在 CSS 规则中加入 content: url() 声明,原本指向 Gravatar 的图片虽然 HTML 结构中的 src 属性未变,但在实际渲染时,浏览器会强制用新指定的图片替代显示。例如下面这段代码:

1
2
3
4
img[src="https://seccdn.libravatar.org/avatar/6446a267c0b621de63b89db1aa25e8da"]
{
content: url(https://avatar.090909.top/leyili.webp);
}

写到这里,我很快又发现了新的问题。我的域名早已配置了缓存规则,其中优先级最高的一条就是“缓存指定扩展名”,而 CSS 文件赫然在列。这意味着,一旦这个包含大量头像替换规则的 CSS 被浏览器或 CDN 缓存,后续任何针对规则的新增、删除或修改都不会立即生效,用户必须强制刷新甚至等待缓存过期才能看到更新。

另外,如果未来不断有新的无头像朋友来留言,我就得不停往 CSS 里追加新的替换规则,这个文件会变得越来越臃肿,维护起来也极不优雅。难道要为了每次加一个头像而修改整个样式表?这显然不够干净,也缺乏扩展性。于是我想到了另一种思路:把映射关系写成 JSON 格式,然后用 JavaScript 从服务器拉取这个 JSON,再动态生成 CSS 注入页面。例如下面这种结构:

1
2
3
4
5
6
7
{
"https://seccdn.libravatar.org/avatar/6446a267c0b621de63b89db1aa25e8da": "https://avatar.090909.top/leyili.webp",
"https://seccdn.libravatar.org/avatar/1282b10159ed97b0521d1bcb1f0fbd86": "https://avatar.090909.top/UpXuu.webp",
"https://seccdn.libravatar.org/avatar/f16b32da9897b53465386cb45613387c": "https://avatar.090909.top/wrCBDjdh.webp",
"https://seccdn.libravatar.org/avatar/17f76b042419b8cd3825c2c6f6ab8817": "https://avatar.090909.top/秋葵笔记.webp",
"https://seccdn.libravatar.org/avatar/9dfa9ac4472951f12b90b20d07ac608d": "https://avatar.090909.top/lm-xiao-fen.webp"
}

现在方案中有了成熟的 CSS 格式和 JSON 格式,接下来就是考虑如何实现功能了。服务器的选择可以非常简单:直接用 GitHub Pages 就够了,因为我只需要存储一些静态文件,并保证它们可以通过 HTTP/HTTPS 被公开访问。整个脚本的核心思想是“无头函数”风格的自执行代码。

它会首先从我的头像服务域拉取 avatar.json 这个映射文件,然后逐条提取出每一个“键”和对应的“值”(即自定义替换头像地址)。拿到这些键值对之后,脚本动态生成一段 CSS 样式,其中每条规则都是用 img[src=""] 选择器匹配原始头像,再用 content: url("") 将其替换成新头像。头像的来源是朋友的博客头像,如果没有则,是他们的 GitHub 账号头像:

1
2
3
4
5
6
7
8
9
10
11
12
(async function () {
const response = await fetch('https://avatar.090909.top/avatar.json');
const map = await response.json();

const rules = Object.entries(map)
.map(([key, val]) => `img[src="${key}"]\n{\n content: url("${val}");\n}`)
.join('\n');

const style = document.createElement('style');
style.textContent = rules;
document.head.appendChild(style);
})();

至此,我的“梁栋烨头像联盟”就正式完工了。你只需在自己的博客网站中引入并执行这个脚本,就能共享我的头像解析服务,让那些原本没有头像的评论者自动获得对应的个性头像。当然,这件事还有一个更重要的前提:这里面必须包含你的头像映射才行。不过,比起直接依赖我的服务,我更鼓励你自己动手搭建一套。

毕竟我已经把完整的技术思路和实现细节都写成了教程,自己搭建不仅能完全掌控数据,还能随时按需增删规则。如果你是我博客的长期访客,并且发现你用邮箱留言时始终没有头像显示,非常欢迎点击工单按钮,向我提交头像解析工单。我会把你的邮箱哈希和你想使用的头像地址加入 JSON 映射和服务器缓存中,从此你的每一次留言都将拥有专属的视觉标识。