Hugo 优化:为图片添加正在加载动画
一直以来都想在图片加载时添加一个加载动画,以增强用户体验。使用 Hugo 以来,关于图片的添加方式也进行了自定义。针对历史数据,没功夫一遍遍改了,就采用 Markdown 原生语法进行图片的插入,这样形成的图片就是一行一个简单进行罗列。后来为了让自己的照片显示 exif 信息,以及实现一行多图的方式,通过自定义短代码的方式实现,详细可见 通过Hugo短代码功能实现图片及其EXIF信息展示 。
因此,本站加入图片的方式除了 Markdown 语法以外,还有 figure 和 figure-group 短代码的方式进行插入。此外,还有近期做的足迹地图里面的坐标卡片中,也有图片,当然已经在 Github 中实现了卡片内图片加载动画效果。在这次 Hugo 的优化中,也一并将对应的效果应用进来。
目标场景
我的博客中有两种主要的图片展示场景:
- 文章页面:Markdown 图片、figure 短代码、figure-group 图片组
- 足迹地图:地图卡片中的缩略图、点击缩略图后的全屏查看
实现方式
1. HTML 层:添加懒加载属性
对于 Markdown 图片,使用 Hugo 的 Render Hook 自动添加 loading="lazy" 属性:
{{- $src := .Destination | safeURL -}}
{{- $alt := .Text -}}
{{- $title := .Title -}}
<img src="{{ $src }}"
alt="{{ $alt }}"
{{- with $title }} title="{{ . }}"{{ end }}
loading="lazy">
位置:layouts/_default/_markup/render-image.html
优势:
- 自动为所有 Markdown 图片添加原生懒加载
- 无需手动修改每篇文章
- 浏览器原生支持
2. CSS 层:纯 CSS 实现加载动画
使用CSS 的 :has() 选择器和伪元素实现旋转圆环:
// 定义可复用的圆环样式
@mixin loading-spinner($light: rgba(0,0,0,.1), $dark: rgba(0,0,0,.6)) {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 40px;
height: 40px;
margin: -20px 0 0 -20px;
border: 3px solid $light;
border-top-color: $dark;
border-radius: 50%;
animation: image-loading-spin .8s linear infinite;
z-index: 1;
transition: opacity .3s;
}
// 文章和足迹地图卡片图片
article :is(a, p, figure):has(> img[loading="lazy"]),
.footprint-popup__slide:has(> img[loading="lazy"]) {
position: relative;
display: block;
&::before { @include loading-spinner; }
}
// 图片加载完成后隐藏圆环
article :is(a, p, figure):has(> img.loaded)::before,
.footprint-popup__slide:has(> img.loaded)::before {
opacity: 0;
pointer-events: none;
}
// 足迹地图放大显示(白色圆环)
.footprint-photo-viewer__dialog:has(> img) {
position: relative;
&::before {
@include loading-spinner(rgba(255,255,255,.2), rgba(255,255,255,.8));
}
}
.footprint-photo-viewer__dialog:has(> img.loaded)::before {
opacity: 0;
pointer-events: none;
}
// 暗色模式适配
.dark {
article :is(a, p, figure):has(> img[loading="lazy"])::before,
.footprint-popup__slide:has(> img[loading="lazy"])::before {
border-color: rgba(255, 255, 255, .1);
border-top-color: rgba(255, 255, 255, .6);
}
}
@keyframes image-loading-spin {
to { transform: rotate(360deg); }
}
核心原理:
:has()选择器:检测父元素是否包含特定子元素:has(> img[loading="lazy"])- 检测是否有懒加载图片:has(> img.loaded)- 检测图片是否已加载
::before伪元素:在图片容器上显示加载动画- 不能直接在
<img>上使用伪元素(规范限制) - 通过父容器
::before实现覆盖层效果
- 不能直接在
SCSS Mixin:复用圆环样式,支持参数化配置
- 普通场景:黑色圆环
- 放大显示:白色圆环(深色背景)
3. JavaScript 层:监听加载状态
监听图片加载完成,添加 .loaded 类触发 CSS 隐藏动画:
// 图片加载动画
(function() {
const SELECTORS = 'article img[loading="lazy"], .footprint-popup__slide img[loading="lazy"], .footprint-photo-viewer img';
function handle(img) {
if (img.complete && img.naturalHeight > 0) {
img.classList.add('loaded');
} else {
const mark = () => img.classList.add('loaded');
img.addEventListener('load', mark, { once: true });
img.addEventListener('error', mark, { once: true });
}
}
function init() {
document.querySelectorAll(SELECTORS).forEach(handle);
new MutationObserver(mutations => {
mutations.forEach(m => m.addedNodes.forEach(node => {
if (node.nodeType === 1) {
if (node.tagName === 'IMG') handle(node);
node.querySelectorAll?.(SELECTORS).forEach(handle);
}
}));
}).observe(document.body, { childList: true, subtree: true });
}
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();
})();
兼容性说明
:has() 选择器支持
| 浏览器 | 最低版本 |
|---|---|
| Chrome | 105+ |
| Firefox | 121+ |
| Safari | 15.4+ |
| Edge | 105+ |
降级方案:不支持 :has() 的浏览器不会显示加载动画,但不影响图片正常加载。
loading="lazy" 支持
| 浏览器 | 最低版本 |
|---|---|
| Chrome | 77+ |
| Firefox | 75+ |
| Safari | 15.4+ |
| Edge | 79+ |
最终效果
文件结构
themes/xiaoten/
├── assets/sass/
│ └── _image-loading.scss (54 行)
├── static/js/
│ └── image-loading.js (27 行)
└── layouts/
├── _default/
│ └── _markup/
│ └── render-image.html (8 行)
└── partials/
└── footer.html (引入 JS)
总计:约 90 行代码实现全站图片加载动画。
© 转载需附带本文链接,依据 CC BY-NC-SA 4.0 发布。
猜你喜欢
评论