Hexo fluid 全屏背景图随日夜模式切换以及正文底页毛玻璃效果
本文最后编辑于:2025年5月31日 中午
实现背景图全屏效果
设置背景图为全屏效果,可以使用 fluid 提供的注入代码功能,可以将代码无侵入式加入到主题里。
在根目录下新建一个 scripts
目录,在目录内新建:injector.js
建好之后,博客的根目录大致如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
├─📁 .github/
├─📁 scaffolds/
├─📁 scripts/
├─📁 source/
├─📁 test/
├─📁 themes/
├─📄 _config.fluid.yml
├─📄 _config.landscape.yml
├─📄 _config.yml
├─📄 .gitignore
├─📄 package-lock.json
├─📄 package.json
├─📄 template.md
└─📄 yarn.lock
⚠️注意:
该文件不需要在_config.fluid.yml
中进行引用,hexo 自动调用执行里面的 js 文件
1
2
3
const { root: siteRoot = "/" } = hexo.config;
hexo.extend.injector.register("body_begin", `<div id="web_bg"></div>`);
hexo.extend.injector.register("body_end",`<script src="${siteRoot}js/backgroundize.js"></script>`);
文件名其实可以随意,但接下来的 js 文件名请与 src="${siteRoot}js/backgroundize.js"
中保持一致。
如果你不需要背景图随日夜主题切换而切换的话,那么你只需要新建 backgroundize.js
并写入以下代码即可:(source\js\backgroundize.js
)
1
2
3
4
5
6
7
8
9
10
11
document
.querySelector('#web_bg')
.setAttribute('style', `background-image: ${document.querySelector('.banner').style.background.split(' ')[0]};position: fixed;width: 100%;height: 100%;z-index: -1;background-size: cover;`);
document
.querySelector("#banner")
.setAttribute('style', 'background-image: url()')
document
.querySelector("#banner .mask")
.setAttribute('style', 'background-color:rgba(0,0,0,0)')
第一段代码的功能是:找到根节点,在页面的 body
开始部分注入一个 div
元素,其 id
为 web_bg
,作为背景图像的容器,然后在页面的 body
结束部分注入 js 文件(backgroundize.js
)进行引用,确保脚本在页面加载完成后执行。
第二段代码的功能是:提取现有.banner
元素的图像 URL,将提取到的图像 URL 应用到 web_bg
元素上,同时设置背景样式为 fixed
固定定位、覆盖整个页面、z-index: -1
并置于其他元素之下。
至此,背景图就全屏且固定了。
实现背景图随日夜主题切换效果
看到这也就说明你需要标题所述功能。那么请将_config.fluid.yml
中涉及到 banner_img
的部分全部配置为空,因为我们要手动设置背景图。
例如首页 banner 和文章页 banner(可以通过搜索 banner_img
提高效率):
在 source/js
目录中新建或修改 backgroundize.js
文件,我的代码文件如下:backgroundize.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
/**
* author: 4rozeN
* file: backgroundize.js
* last modified: 2025-05-19
* description: 背景图随日夜模式切换处理。
*/
/**
* 获取颜色模式切换按钮(深色 / 浅色)。
* @type {HTMLElement | null}
*/
const colorToggleButton = document.querySelector("#color-toggle-btn .nav-link");
/**
* 背景容器元素(统一为 #web_bg)。
* @type {HTMLElement}
*/
const backgroundContainer = document.querySelector("#web_bg");
// === 背景图片配置 ===
/**
* BACKGROUND_IMAGES 说明:
* - dark / light:按颜色模式分类;再区分 desktop / mobile 两张图。
* - fallback:在排除页面、或桌面 / 移动切换时的默认背景。
*/
const BACKGROUND_IMAGES = {
dark: {
desktop:
"URl1",
mobile:
"mobileURL",
},
light: {
desktop:
"URL2",
mobile:
"mobileURL",
},
fallback:
"fallbackURL",
};
// === 页面排除列表 ===
/**
* 在以下路径(前缀匹配)不跟随颜色模式切换背景。
* 说明:主页 ("/") 也被视为排除页面。
* @type {string[]}
*/
const PAGE_EXCLUSIONS = [
"/links/",
"/about/",
"/categories/",
"/tags/",
"/collections/",
];
// === 工具函数 ===
/**
* 判断当前页面是否在排除列表中。
* @param {string} path - location.pathname
* @returns {boolean}
*/
const isExcludedPage = (path) => {
if (path === "/") return true;
return PAGE_EXCLUSIONS.some((excluded) => path.startsWith(excluded));
};
/**
* 判断是否为移动端视口。
* @returns {boolean}
*/
const isMobileViewport = () => window.innerWidth < 768;
/**
* 移除 banner 遮罩,避免背景图层被半透明黑层覆盖。
*/
const removeBannerMask = () => {
const mask = document.querySelector("#banner .mask");
if (mask) mask.style.backgroundColor = "rgba(0,0,0,0)";
};
/**
* 预加载所有背景图片,降低首帧闪烁。
*/
const preloadBackgroundImages = () => {
const urls = new Set();
// 收集所有图片 URL
Object.values(BACKGROUND_IMAGES).forEach((entry) => {
if (typeof entry === "object") {
urls.add(entry.desktop);
urls.add(entry.mobile);
} else {
urls.add(entry);
}
});
// 创建 Image 对象触发加载
urls.forEach((url) => {
const img = new Image();
img.src = url;
});
};
/**
* 根据当前模式 / 视口或自定义 URL 设置背景图。
* @param {"dark" | "light" | null} mode - 当前颜色模式;null 表示排除页面
* @param {string} [customUrl] - 强制使用的自定义链接(优先级最高)
*/
const applyBackgroundImage = (mode, customUrl) => {
const mobile = isMobileViewport();
let imageUrl;
if (customUrl) {
// 若明确指定自定义 URL,则直接使用
imageUrl = customUrl;
} else if (mode === null) {
// 排除页面:桌面使用 fallback,移动端统一 dark.mobile
imageUrl = mobile
? BACKGROUND_IMAGES.dark.mobile
: BACKGROUND_IMAGES.fallback;
} else if (!isExcludedPage(location.pathname) && mode) {
// 非排除页面:根据模式 + 视口选择
imageUrl = mobile
? BACKGROUND_IMAGES[mode].mobile
: BACKGROUND_IMAGES[mode].desktop;
} else {
// 处理从移动端切换回桌面端时可能出现的背景错乱
imageUrl = mobile
? BACKGROUND_IMAGES.dark.mobile
: BACKGROUND_IMAGES.fallback;
}
backgroundContainer.style.backgroundImage = `url(${imageUrl})`;
};
/**
* 获取用户当前的颜色模式(深/浅)。
* @returns {"dark" | "light"}
*/
const getUserColorScheme = () => {
const mode = document.documentElement.getAttribute("data-user-color-scheme");
return mode === "dark" ? "dark" : "light";
};
/**
* 颜色模式切换后的回调:重新应用背景并移除遮罩。
*/
const handleColorSchemeToggle = () => {
const mode = getUserColorScheme();
if (!isExcludedPage(location.pathname)) {
applyBackgroundImage(mode);
}
removeBannerMask();
};
// === 视口 Resize 处理(防抖) ===
let resizeTimer; // 定时器句柄
/**
* 窗口大小变化事件的处理函数(含 200ms 防抖)。
*/
const handleViewportResize = () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
applyBackgroundImage(getUserColorScheme());
}, 200);
};
// === 监听颜色模式属性变化 ===
const observeColorSchemeChange = () => {
const targetNode = document.documentElement;
const config = {
attributes: true,
attributeFilter: ["data-user-color-scheme"],
};
const callback = (mutationsList) => {
for (const mutation of mutationsList) {
if (
mutation.type === "attributes" &&
mutation.attributeName === "data-user-color-scheme"
) {
handleColorSchemeToggle();
}
}
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
};
// === 初始化 ===
/**
* 初始化背景:预加载 → 根据当前页面 & 模式设置 → 去掉遮罩。
*/
const initializeBackgrounds = () => {
preloadBackgroundImages();
const currentPath = location.pathname;
const currentMode = getUserColorScheme();
if (PAGE_EXCLUSIONS.includes(currentPath)) {
applyBackgroundImage(null);
} else {
applyBackgroundImage(currentMode);
}
removeBannerMask();
};
// === 事件监听 ===
if (!colorToggleButton) {
console.warn("未找到颜色模式切换按钮");
}
// 监听颜色模式属性变化
observeColorSchemeChange();
// 监听窗口大小变化
window.addEventListener("resize", handleViewportResize, { passive: true });
// DOMContentLoaded → 初始化背景
window.addEventListener("DOMContentLoaded", () => {
console.log("DOM 完成,开始初始化背景图。");
initializeBackgrounds();
});
// 加载完毕之后清除控制台日志
window.addEventListener("load", () => {
console.clear();
});
该代码文件还需结合一个自定义的 css 文件使用:source/css/backgroundize.css
1
2
3
4
5
6
7
8
9
#web_bg {
position: fixed;
width: 100%;
height: 100%;
z-index: -1;
background-size: cover;
/* 添加过渡效果,你也可以自定义 */
transition: all 0.5s;
}
在 backgroundize.js
文件中,我们手动指定了背景图、切换逻辑,所以上一节的第二段代码已不再有作用,故在此删去。
在 backgroundize.js
文件中,若你有更多自定义页面图片需求,你将能够很方便的实现它。
在 backgroundize.css
中,我们依然将 web_bg
设置为 fixed
将其固定,并设置 z-index: -1
保持沉底。
因为在_config.fluid.yml
中我们将 banner_img
配置项全部配置为空,那么控制台将会出现 404 错误:
眼不见心不烦,所以在 backgroundize.js
中存在下面的代码:
1
2
3
4
// 加载完毕之后清除控制台日志
window.addEventListener("load", () => {
console.clear();
});
⚠️注意:
以上文件,别忘记在_config.fluid.yml
文件中的 custom_css
中进行引用。URl1
、mobileURL
和 fallbackURL
等字符,仅作占位使用,实际应为真实图片链接。
因为 injector.js
的存在,所以 backgroundize.js
不需要在 custom_js
中引入,否则控制台中将会出现以下报错:
1
Uncaught SyntaxError: Identifier 'colorToggleButton' has already been declared (at backgroundize.js:1:1)
_config.fluid.yml
配置项:
1
2
custom_css:
- /css/backgroundize.css
预加载的可能问题
如果你遇到了第一次切换主题,图片与图片之间的切换有些生硬,有点闪屏的感觉,但后续的所有切换却十分丝滑,那么很可能是预加载的失败。
可能的问题来源
如果 OSS/Bucket
设置了响应头 Cache-Control: no-store
或 max-age=0
或没有启用 Expires
,浏览器会认为每次访问都需要重新拉取。也就是尽管你预加载了,但切换时图片链接仍然会触发一次真正的请求。
我遇到的问题是,我使用的图片托管在阿里云的 oss 上,阿里云返回的响应头中存在:
1
2
x-oss-force-download: true
Content-Disposition: attachment
而 Content-Disposition: attachment
的含义是:这个资源是供下载用的,而不是网页内容的一部分。
浏览器遇到这种响应头时,通常会绕过图片的常规缓存路径,或者不会把它作为正常的预加载资源处理。
这就导致:
- 图片不会进入
memory
或disk cache
- 即使预加载代码写得很好,浏览器仍然会每次点击切换时发起新请求
- 导致首次切换背景图时出现 “闪屏或延迟加载”
翻阅文档,发现阿里云在很久之前就已经做出了调整:
出于安全考虑,通过OSS默认 Bucket域名访问文件时,OSS会强制增加下载响应头,导致浏览器强制下载文件。
也就是强制增加了 x-oss-force-download: true
使得 Content-Disposition
为附件类型。
如果遇到其他问题,欢迎讨论。
解决办法
如果你的托管平台可以更改元信息,那么请更改所需文件的元信息:
Content-Disposition: attachment
改为Content-Disposition: inline
或留空x-oss-force-download: true
改为false
阿里云平台是无法通过更改文件元信息来解决的。
阿里云的推荐方案是为 oss/bucket
绑定自定义域名,不再使用阿里云提供的默认域名。详见:绑定自定义域名至 Bucket 默认域名
或者,你也可以将图片托管在 GitHub 上,就直接使用仓库的 img 目录用于存放,GitHub 的响应头是符合要求的。
非预期的切换效果
如果你遇到了第一时间显示的不是预期的背景图,那么很可能是你在_config.fluid.yml
文件中的 banner_img
没有全部配置为空。
无法匹配页面 / 排除页面不生效
这一般是因为你的网站的永久链接生成方式与我不同。
该配置项通常在_config.yml
文件中可以找到。我的配置如下:
1
2
3
4
5
6
7
8
9
# URL
## 设置你的网站 URL,例如 GitHub Pages 上设置为 'https://username.github.io/project'
url: https://4rozeN.github.io # 网站的完整 URL
permalink: archives/:category/:abbrlink.html # 文章永久链接的格式
abbrlink: alg # 算法:crc16(default) and crc32
permalink_defaults: defaultPermaLink # 默认永久链接的名称
pretty_urls: # 优化 URL 美观性
trailing_index: true # 如果设为 false,将去掉链接结尾的 'index.html'
trailing_html: true # 如果设为 false,将去掉链接结尾的 '.html'
详见 Hexo 官方文档:【Hexo】- 永久链接(Permalinks)
什么意思呢?
我的代码中,写死了部分排除在外不会跟随日夜模式切换背景图的页面,这些页面都是能够被
1
2
3
4
5
6
7
8
9
10
11
12
13
// 排除在外的页面
const PAGE_EXCLUSIONS = [
"/links/",
"/about/",
"/categories/",
"/tags/",
"/collections/",
];
// 校验
const isExcludedPage = (path) => {
if (path === "/") return true;
return PAGE_EXCLUSIONS.some((excluded) => path.startsWith(excluded));
};
校验语句判断为真返回 true
的。但如果你的网站使用如:
2013/07/14/hello-world/
2013-07-14-hello-world.html
foo/bar/hello-world/
- ……
等等方案,我很难每个都适配,只得你自己进行修改。
修改其实也很简单,比如日期类的永久链接:/2025/04/26/xxx/
你可以修改 excludePages
中的字符串为正则匹配符:/^\/\d{4}\/\d{2}\/\d{2}\//
校验函数也可以修改为:
1
2
3
4
// 用于判断当前页面是否在排除范围内
const isExcludedPage = (path) => {
return excludePathPatterns.some(pattern => pattern.test(path));
};
总之,方法有很多。如果你没有这方面的基础,你也可以借助 AI 工具实现。当然,如果你的网站刚建立,更换永久链接是一件损失非常小的事情,如果你不想折腾的话,你也可以更换永久链接来适配代码。
⚠️ 注意:
更换永久链接意味着您的网站在各搜索引擎中的收录索引将会失效,重新索引可能需要一段时间,重新索引完成的时间将取决于您网站的流量高低。当然,您也可以手动前往提交索引请求,但这只是起到告知作用,重新索引完成的时间仍然未知。若您仍不明白这将有何影响,建议您不要更换永久链接。
毛玻璃底页效果
修改
_config.fluid.yml
文件的主面板背景色1
2
3
4
5
6# 主面板背景色 # Color of main board -board_color: "#ffffff" -board_color_dark: "#252d38" +board_color: "#ffffff80" +board_color_dark: "#00000080"
source/css
目录下新建glassBg.css
文件1
2
3
4
5
6
7
8
9
10
11
12
13
14/* 正文底页毛玻璃效果 */ #board { -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); } /* 侧边目录毛玻璃效果 */ #toc { padding: 10px; top: 4rem; background-color: var(--board-bg-color); border-radius: 10px; -webkit-backdrop-filter: blur(var(--background-blur)); backdrop-filter: blur(var(--background-blur)); }
侧边的目录区域毛玻璃效果如果不需要可以删除。
一些好用的插件
Hexo 官方推荐插件就足够:https://fluid-dev.github.io/hexo-fluid-docs/advance/#hexo - 插件
致谢
感谢 @banyee19 在日夜背景图切换方面的探究,我一度以为我的切换效果是正常的,但使用其他设备进行查看后才发现效果无法正确实现,故此重构。
相关文章: