Nuxt主题加载/切换闪烁解决方法
经验之道!
闪烁问题
如果你用的是SSR,可能会遇到这种情况,在第一次进入网页或是刷新后,主题从默认主题闪烁一下变到当前主题(比如浅色→深色)。
出现闪烁问题是因为SSR环节,Nuxt一侧没有客户端环境,没有DOM也没有LocalStorage去检查并应用主题,导致主题相关的逻辑只能在浏览器运行时上边跑,而不是让服务端直接吐出来一个包含已应用主题的骨架。
所以,问题的关键在于如何把主题检查的逻辑跑在服务端。
这得分为两种主题实现来说(虽然说底层差不多的:
ThemeClass (.dark or .xxxx 动态添加类名切换主题)
媒体查询 (window.matchMedia)
解决主题闪烁的方法
第一种 Head标签内联JS
先丢出来实现代码(这是网上用的最多的方法)
ThemeClass的版本↓
// IIFE 避免污染全局
(function () {
function initTheme() {
const _theme = localStorage.getItem('theme') || 'dark';
document.documentElement.classList.toggle('dark', _theme === 'dark');
}
// 如果你用了
if (_theme === 'dark') {
document.documentElement.setAttribute('arco-theme', 'dark');
} else {
document.documentElement.removeAttribute('arco-theme');
}
initTheme()
})();
媒体查询的版本↓
// IIFE 避免污染全局
(function () {
function initTheme() {
const _theme =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
document.documentElement.classList.toggle('dark', _theme === 'dark');
}
// 如果你用了
if (_theme === 'dark') {
document.documentElement.setAttribute('arco-theme', 'dark');
} else {
document.documentElement.removeAttribute('arco-theme');
}
initTheme()
})();
这里有两种内联方式
// nuxt.config,js
app: {
head: {
script: [
{
type: 'text/javascript',
children: "刚刚的脚本填这里"
}
// 或者单独放到一个文件里
{
src: "脚本文件路径"
}
]
}
}
第二种 useHead()
首先得讲讲useHead。useHead支持 去重、合并、优先级控制(避免重复 meta 标签), 并且它有多种行为模式:
| 场景 | 运行环境 | 行为 |
|---|---|---|
| SSR(服务端渲染 / 水合前) | Node.js | 在服务端收集 head 数据,注入到服务器渲染的骨架里 |
| CSR(客户端渲染 / 水合后) | 浏览器 | 在浏览器中执行,动态操作document.head |
| 静态站点生成(SSG) | Node.js(构建时) | 构建阶段运行,生成带正确 head 的静态 HTML 文件 |
最重要的是,在服务段渲染的时候它不依赖DOM,只对虚拟DOM操作,这意味着服务端可以通过useHead的方式来实现 documentElement.setAttribute('arco-theme', 'dark') 这种操作。水合后骨架上已经有对应的标签了,不会重复运行document.head。
那么主题变量的持久化如何解决呢?答案是Pinia Plugin Persistedstate,这个插件会把piniaStore的变量持久化到cookie里面,注意这个不要用多了。
// 主题控制
const theme = computed(()=> themeMode)
useHead({
bodyAttrs: {
'arco-theme': theme,
},
htmlAttrs: {
class: theme,
}
})
效果优劣
我用下来的感受是,useHead添加属性>内联JS。前者没有一丝浏览器运行时损失,是直接服务端添加到标签上的,意味着在水合前主题就已经挂载上去了。而后者内联JS依赖浏览器运行时,并且有时会失效,尤其是第一次加载页面的时候,首屏体验差。所以最后我博客用的是useHead+Pinia 这个方法。
