为什么这个 Astro/Svelte 组件中的图标在刷新时闪烁?


<script lang="ts">
import { onMount } from "svelte";  
let theme = localStorage.getItem('theme') ?? 'light';
let flag = false;
onMount(()=>{    
flag = true
})
$: if (flag) {
if ( theme === 'dark') {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}     
localStorage.setItem("theme", theme);
}

const handleClick = () => {    
theme = (theme === "light" ? "dark" : "light");    
}; 
</script>
<button on:click={handleClick}>{theme === "dark" ? "  " : "  "}</button>

启用暗模式时图标闪烁,在浅色模式下不会发生这种情况,我假设发生这种情况是因为它在最初渲染时默认为浅色模式,我该如何解决这个问题?

问题规范

闪烁是由MPA(多页应用程序)的意外副作用引起的。在 SPA(单页应用程序)中,路由发生在客户端,启动时仅获取单个页面,因此菜单或主题等内容的状态在客户端保持一致。

Astro 作为 MPA 在从一个页面跳转到另一个页面时需要服务器页面获取。如果服务器不知道客户端设置为主题(深色或浅色),则必须发送默认主题,然后只有当<script>标签在客户端上执行时,客户端上的持久状态才会启动。

可以将此问题称为服务器/客户端状态同步

为了使问题更加棘手,我们必须记住,多个客户可能正在使用该网站,而不仅仅是一个。

解决 方案

出于解决方案目的,如果我们将主题深/亮视为计数器 0/1

使用客户端路由的 SSG

如果静态站点受到限制,我仍然想再次提及这一点,然后接下来的两个解决方案不起作用,然后需要回退到客户端路由=> SPA

下一个解决方案适用于服务器端渲染 SSR

使用饼干的 SSR

由客户端设置,并由服务器根据请求读取,以确保将正确的状态发回单页加载

这是服务器端的主要代码片段

let counter = 0
const cookie = Astro.cookies.get("counter")
if(cookie?.value){
counter = cookie.value
}

这里是如何使用Vanialla JS在客户端设置和获取cookie的功能

function get_counter(){
const entry = document.cookie.split(';').find(entry=>entry.replace(' ','').startsWith('counter='))
if(entry){
return parseInt(entry.split('=')[1])
}else{
return 0
}
}
function set_counter(counter){
document.cookie = `counter=${counter}`
console.log(`new counter value = ${counter}`)
}

SSR 避免使用存储和 URL 参数的 cookie

Cookie 是一个更简单的解决方案,但如果由于某种原因(被阻止、不想通知,...)这不起作用,可以使用存储和 url 参数

在客户端设置 URL 参数并管理存储

...
window.history.replaceState(null, null, `?session_id=${session_id}`);
...
let session_id = sessionStorage.getItem("session_id")
...
sessionStorage.setItem("counter",counter)

这就是服务器管理会话 ID 的方式

let session_id = suid()
if(Astro.url.searchParams.has('session_id')){
session_id = Astro.url.searchParams.get('session_id')
console.log(`index.astro> retrieved session_id from url param '${session_id}'`)
}else{
console.log(`index.astro> assigned new session_id '${session_id}'`)
}

完整示例参考

与饼干

GitHub : https://github.com/MicroWebStacks/astro-examples#13_client-cookie-counter

Cookie没有得到一些VM服务提供,这里是Gitpod上的工作

Gtipod : https://gitpod.io/?on=gitpod#https://github.com/MicroWebStacks/astro-examples/tree/main/13_client-cookie-counter

没有带有存储和 URL 参数的 cookie

GitHub : https://github.com/MicroWebStacks/astro-examples#14_client-storage-counter

这在任何地方运行,所以这里是堆栈闪电战的例子:https://stackblitz.com/github/MicroWebStacks/astro-examples/tree/main/14_client-storage-counter

我遇到了同样的问题:每次页面加载后,"默认"主题图标都会短暂闪烁

主题本身已正确加载(深色主题中没有闪光背景,反之亦然),在<head>元素中使用了这个简短的渲染阻止脚本,该脚本在<html>元素上设置"dark"类(如果在localStorage中找到):

<!DOCTYPE html>
<html lang="en">
<head>
<!-- etc. -->
<script is:inline>
const theme = (() => {
if (
typeof localStorage !== "undefined" &&
localStorage.getItem("theme")
) {
return localStorage.getItem("theme");
}
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "light";
})();

if (theme === "light") {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
</script>
</head>
<body>
<!-- etc. -->
</body>
</html>

作为一个切换器,我使用了一个简单的Preact组件,用法如下:

<header>
<ThemeToggle client:load/>
</header>

正是这个元素在闪烁。我不会把整个代码放在这里,只放重要的部分:

// ThemeToggle.tsx
export default function ThemeToggle() {
const theme = signal(localStorage.getItem("theme") ?? "light");  
// event handler, etc. omitted…
return (
<button>
{theme.value === "light" ? <MoonIcon /> : <SunIcon />}
</button>
);
}

我怀疑我的代码逻辑中存在问题,但没有 - 即使我扔掉了除返回块之外的所有内容,它也会闪烁。

正是@wassfila的回答帮助我弄清楚发生了什么。我使用的是SSG,所以我从服务器得到的是一个静态的HTML字符串。由于 Astro 的client:load指令实现了水化,因此只有在页面最初呈现后才会延迟加载。这就是它闪烁的原因!我不知道 Astro 是如何进行静态渲染的,但由于它是在服务器上完成的,我只猜测localStorage.getItem("theme") ?? "light"导致主题设置为"浅色",因为服务器运行时没有 localStorage。

解决方案是什么?

我放弃了Preact组件,因为尽管Preact是一个很小的库,但3kb的JS对于一个简单的小切换来说仍然太多了。而且由于我在呈现时已经在页面上具有主题的值(作为<html>元素上的类),因此我只使用 CSS 并根据当前主题在图标上设置不同的可见性(默认/隐藏)。

---
// themeToggle.astro
---
<button class="relative w-8 h-8" id="theme-toggle">
<svg class="absolute inset-0 dark:invisible">
<!-- etc -->
</svg>
<svg class="absolute inset-0 invisible dark:visible">
<!-- etc -->
</svg>
</button>
<script>
document.getElementById("theme-toggle")?.addEventListener(
"click",
() => { // theme switch logic, omitted for brevity …}
);
}
</script>

这是使用Tailwind和"dark"类,但它与vanilla CSS和数据属性而不是类一起工作。

最新更新