惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Palo Alto Networks Blog
P
Proofpoint News Feed
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
量子位
Blog — PlanetScale
Blog — PlanetScale
有赞技术团队
有赞技术团队
腾讯CDC
博客园 - Franky
The Cloudflare Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hugging Face - Blog
Hugging Face - Blog
H
Heimdal Security Blog
L
LangChain Blog
V
V2EX
Jina AI
Jina AI
美团技术团队
V2EX - 技术
V2EX - 技术
V
Visual Studio Blog
Google Online Security Blog
Google Online Security Blog
人人都是产品经理
人人都是产品经理
Security Archives - TechRepublic
Security Archives - TechRepublic
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Know Your Adversary
Know Your Adversary
Hacker News: Ask HN
Hacker News: Ask HN
Apple Machine Learning Research
Apple Machine Learning Research
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Threat Research - Cisco Blogs
Y
Y Combinator Blog
博客园_首页
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Spread Privacy
Spread Privacy
C
Cisco Blogs
博客园 - 三生石上(FineUI控件)
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog
PCI Perspectives
PCI Perspectives
MongoDB | Blog
MongoDB | Blog
A
About on SuperTechFans
B
Blog RSS Feed
N
News and Events Feed by Topic
L
LINUX DO - 热门话题
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threatpost
月光博客
月光博客
Recent Announcements
Recent Announcements
D
DataBreaches.Net
IT之家
IT之家
C
Cyber Attacks, Cyber Crime and Cyber Security

Sehnsucht

记一次博客换图床的过程 观《花束般的恋爱》 读《活过》 东京 旅行篇 亲人逝去 我为什么想养鱼 防止AI爬取你的博客 「monthly 」博客重构 游戏全成就 好看的小说 人生的每一步都不会是浪费-2024年终 「weekly」星期五综合征 「weekly」听播客 杂谈 读《美丽新世界》 「weekly」社交媒体 恶性事件 新世界 「weekly」认知觉醒 美丽新世界 follow 「weekly」独立博客9问题 双十一买书 读《惊呆了!原来这就是社会学》 一次聊天与自我建设 2024-09月记 补番《relife》 树状数组简单理解 lvm简单使用 博客自动发布方案 某公众号废案 fail2ban基本使用 Podman 环境使用 Nginx Proxy Manager 最佳实践 2024-07同学聚会 vps使用podman部署freshrss Gitlab CICD 实践,思考与记录 装备升级:新的PC gitlab局域网搭建流程 FastMail迁移至GMail+Cloudflare Email Routing 读《黑客与画家》 leetcode 第 391 场周赛 nuxt3使用echarts5渲染中国地图 人生刻度 又到凤凰花朵开放的时候 2023-05-10 实习结束 2023-04-26 广州博物馆游记 YOASOBI「たぶん」官方音乐视频 风灵玉秀-发如雪 带我的老哥离职了 从p10k转向Starship lunarvim国内安装踩坑记录 什么是对称加密非对称加密、密钥交换、数字签名、证书 强调体验的一种思考时间的方式 ArchLinux使用vscode编写latex报错The font "FontAwesome" cannot be found ArchLinux安装(移动硬盘)流程 yay一个或多个文件没有通过有效性检查! 2022年终 hugo建站伊始 clash设置relay前置代理(校园网破解改进记录) 群青 · YOASOBI · Ayase 华为 matepad pro 11使用体验 Nginx使用acme.sh免费安装ssl证书 永远属于你的安娜 在线 童年趣事 童心,是比野心更难得的梦想 2021年终总结 基于jeeSite的软件测试课程作业 流程记录 Linux不同用户安装不同版本jdk Wifi4更换Wifi6路由器的使用体验 Flask 上传图片并灰度显示 玩客云刷机debain个人记录 jython 简单入门 Jenkins 个人搭建流程记录 集成邮件系统(qq邮箱),gitlab服务器,freestyle风格 在Web项目中配置Log4j --指南-- Codeforces Round 753 (Div. 3) ABCDE 浅谈fork函数 JavaWeb servlet 使用Cookie记录用户访问次数 Codeforces Round Educational Codeforces Round 114 (Rated for Div. 2) ABC 2021牛客多校7 I xay loves or 2021牛客多校9 H Happy Number 2021牛客多校8 D OR 2021牛客多校5 B Boxes 2021牛客多校5 H Holding Two 2021牛客多校2 D Er Ba Game 2021牛客多校1 F Find 3-friendly Integers 2021牛客多校1 B Ball Dropping 记人生第一次投简历和笔试 原型模式 2021-2-20 雨时随记 关于一维差分数组的例子 halo个人建站 关于手机ping电脑和电脑ping手机 泛型类简单理解 CodeForces - 849C From Y to Y 2020牛客国庆集训派对day3 Leftbest 约瑟夫环问题---2020牛客国庆集训派对day2 AKU NEGARAKU mysql 5.7 忘记密码如何更改 CF 1099B Squares and Segments CF 998A Balloons CF 998B Cutting 链表应用之多项式相加 CF 1150A Stock Arbitraging CF 1199A City Day CF 1199B Water Lily CF 982A Row CF 934A A Compatible Pair CF 629B Far Relative’s Problem 651A Joysticks 651B Beautiful Paintings
Astro添加过渡后DarkModeToggle按钮失效
2024-02-27 · via Sehnsucht

我coding博客主题的时候遇上了这样的问题:添加过渡动画后,点击导航栏切换到其他页面,如果当前页面是dark模式,那么切换后就会变成light模式

我的ModeToggle按钮的代码是从这里改的

JS:

<script is:inline>
	// 从localstorage|系统获取主题
	const getThemePreference = () => {
		if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
			return localStorage.getItem('theme');
		}
		return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
	};
	
	// 是dark模式就在dom的class中添加dark
	const isDark = getThemePreference() === 'dark';
	document.documentElement.classList[isDark ? 'add' : 'remove']('dark');
	
	// 监听class,当class变化时触发回调
	if (typeof localStorage !== 'undefined') {
		const observer = new MutationObserver(() => {
			const isDark = document.documentElement.classList.contains('dark');
			localStorage.setItem('theme', isDark ? 'dark' : 'light');
		});
		observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
	}
</script>

ModeToggle:

import * as React from "react"
import { Moon, Sun } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function ModeToggle() {
  // 定义state
  const [theme, setThemeState] = React.useState<
    "theme-light" | "dark" | "system"
  >("theme-light")
  // 定义useEffect,组件渲染时执行
  React.useEffect(() => {
    const isDarkMode = document.documentElement.classList.contains("dark")
    setThemeState(isDarkMode ? "dark" : "theme-light")
  }, [])
  // 定义useEffect,状态theme变化时执行
  React.useEffect(() => {
    const isDark =
      theme === "dark" ||
      (theme === "system" &&
        window.matchMedia("(prefers-color-scheme: dark)").matches)
    document.documentElement.classList[isDark ? "add" : "remove"]("dark")
  }, [theme])

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Toggle theme</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setThemeState("theme-light")}>
          Light
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setThemeState("dark")}>
          Dark
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setThemeState("system")}>
          System
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

添加过渡后代码为何失效

在未加入ViewTransition时,网站是全页面导航,即切换页面后,页面重新加载 加入ViewTransition后,网站变成了SPA模式,页面将不会全部刷新,而是通过JS将页面元素一个个替换 具体可参考:全页面导航 vs 客户端路由

Astro 的视图过渡动画支持由新的 View Transitions 浏览器 API 提供

如果是加入ViewTransition之前,切换页面全部刷新,那么JS就会执行,但是添加了过渡动画之后页面部分切换,JS就不一定执行了,所以需要侦听生命周期事件,当页面刷新时就执行代码

ViewTransition之后,ModeToggle组件也会重新渲染,造成组件的状态丢失,(const [theme, setThemeState] = React.useState<"theme-light" | "dark" | "system">("theme-light"))被重新设置为theme-light

如何解决

ModeToggle组件状态丢失

Astro中提供了transition:persist指令,用于在切换页面时组件保留状态

<Navigation client:idle transition:persist />

这样组件就可以在切换页面后保留状态了

addEventListener监听生命周期事件

视图切换生命周期共五种:

astro:before-swap是能看得到切换前页面的最后一个周期,而astro:after-swap是切换完成页面后的第一个周期

上文创建了observer用来监听全局的class是否变化,变化了就设置localStorage一经创建,只要不刷新页面就一直存在

当页面改变后会默认变成light模式,所以,此时就会触发observer的回调函数,localStorage也就变成了light,所以,由于astro:before-swap是页面变化前最后的一个周期,只有在astro:before-swap以及之前我们检查到的localStorage才是上个页面保存的localStorage

所以,结果出来了!!!我们应该在astro:before-swap或之前的周期检查localStorage,然后在astro:after-swap或者之后的周期添加dark

那么问题又来了,如何在astro:before-swap周期记录isDark值,然后在astro:after-swap获取它呢,答案是用状态变量管理,Astro推荐使用Nano Stores来进行客户端的存储共享,所以后续使用Nano Stores

<script>
    import { isDark } from "@/store.ts";
    document.addEventListener("astro:before-swap", () => {
        // 从localstorage|系统获取主题
        const getThemePreference = () => {
            if (
                typeof localStorage !== "undefined" &&
                localStorage.getItem("theme")
            ) {
                return localStorage.getItem("theme");
            }
            return window.matchMedia("(prefers-color-scheme: dark)")
                .matches
                ? "dark"
                : "light";
        };
        // 是dark模式就在dom中添加dark
        isDark.set(getThemePreference() === "dark");
        console.log("isDark:" + isDark.get());
    });
    document.addEventListener("astro:after-swap", () => {
        document.documentElement.classList[
            isDark.get() ? "add" : "remove"
        ]("dark");
    });
</script>

同时,如果刷新整个页面,页面就会失常,因为我们把页面刷新时的代码都加进生命周期了。当页面整体刷新时,不经过生命周期,所以需要重新加上原来的代码,才能正常执行

代码

前提:Astro已经安装Nano Stores,具体参考本文

除此之外,我还用了TailwindCSS和Shadcn

store.ts

// 设置变量isDark
import { atom } from 'nanostores';
export const isDark = atom<boolean>(false);

ModeToggle.tsx

import * as React from "react"
import { Moon, Sun, SunMoon } from "lucide-react"

import { Button } from "@/components/ui/button"

export function ModeToggle() {
    const [theme, setThemeState] = React.useState<"theme-light" | "dark" | "system">("theme-light")

    React.useEffect(() => {
        const isDarkMode = document.documentElement.classList.contains("dark")
        setThemeState(isDarkMode ? "dark" : "theme-light")
    }, [])

    const toggleTheme = () => {
        setThemeState(prevTheme => {
            if (prevTheme === "theme-light") {
                return "dark"
            } else if (prevTheme === "dark") {
                return "system"
            } else {
                return "theme-light"
            }
        })
    }

    React.useEffect(() => {
        const isDark =
            theme === "dark" ||
            (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches)
        console.log(theme);
        document.documentElement.classList[isDark ? "add" : "remove"]("dark")
    }, [theme])

    return (
        <Button variant="outline" size="icon" onClick={toggleTheme} className="bg-transparent hover:bg-transparent animate-pulse">
            {theme === "theme-light" && (
                <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0 hover:stroke-ring" />
            )}
            {theme === "dark" && (
                <Moon className="h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100 hover:stroke-ring" />
            )}
            {theme === "system" && (
                <SunMoon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:rotate-0 dark:scale-100 hover:stroke-ring" />
            )}
        </Button>
    )
}

省略部分代码

import React, { useState } from 'react';
import { ModeToggle } from "@/components/ModeToggle";

return (
    // ...
    <ModeToggle />
    // ...
);

RootLayout.astro

省略部分代码

// ...
<Navigation client:idle transition:persist />
// ...
// 页面整体刷新时
<script>
    import { isDark } from "@/store.ts";
    // 从localstorage|系统获取主题
    const getThemePreference = () => {
        if (
            typeof localStorage !== "undefined" &&
            localStorage.getItem("theme")
        ) {
            return localStorage.getItem("theme");
        }
        return window.matchMedia("(prefers-color-scheme: dark)").matches
            ? "dark"
            : "light";
    };
    // 是dark模式就在dom中添加dark
    isDark.set(getThemePreference() === "dark");
    console.log("isDark1:" + isDark.get());
    document.documentElement.classList[isDark.get() ? "add" : "remove"](
        "dark",
    );
    // localStorage存在就添加observer,当class变化时触发回调
    if (typeof localStorage !== "undefined") {
        const observer = new MutationObserver(() => {
            const isDark =
                document.documentElement.classList.contains("dark");
            localStorage.setItem("theme", isDark ? "dark" : "light");
        });
        observer.observe(document.documentElement, {
            attributes: true,
            attributeFilter: ["class"],
        });
    }
</script>
// 页面过渡切换时
<script>
    import { isDark } from "@/store.ts";
    document.addEventListener("astro:before-swap", () => {
        // 从localstorage|系统获取主题
        const getThemePreference = () => {
            if (
                typeof localStorage !== "undefined" &&
                localStorage.getItem("theme")
            ) {
                return localStorage.getItem("theme");
            }
            return window.matchMedia("(prefers-color-scheme: dark)")
                .matches
                ? "dark"
                : "light";
        };
        // 是dark模式就在dom中添加dark
        isDark.set(getThemePreference() === "dark");
        console.log("isDark:" + isDark.get());
    });
    document.addEventListener("astro:after-swap", () => {
        document.documentElement.classList[
            isDark.get() ? "add" : "remove"
        ]("dark");
    });
</script>

参考: