当前仓库里,主题切换已经不再和双语路由揉在同一篇实现说明里。
双语属于路径和文案层;主题切换属于组件行为层。现在这部分被明确收敛到 ThemeController.astro 和 LayoutStyles.astro。
目标
主题系统只做下面几件事:
- 默认使用
light - 用户点击按钮后可切到
dark - 用户刷新页面后保留上次选择
- 代码高亮跟着主题变化
- 中英文页面共用同一套逻辑,只替换按钮文案
先把主题状态放到 html 根节点
当前实现不是给每个页面单独加 class,而是直接使用:
<html lang={lang} data-theme="light">
这样做的好处很直接:
- 全局 CSS 变量可以统一挂在
:root - 页面切换时不需要给很多组件分别同步状态
- 代码块、Header、正文、按钮都可以共用同一套主题变量
ThemeController 只管行为,不管视觉
src/components/layout/ThemeController.astro 只有一段内联脚本。它做两件事:
- 页面初始化时,从
localStorage取回上次主题 - 找到
[data-theme-toggle]按钮并绑定切换行为
核心逻辑如下:
<script is:inline>
(() => {
const root = document.documentElement;
const getTheme = () => (root.dataset.theme === "dark" ? "dark" : "light");
const applySavedTheme = () => {
try {
const theme = localStorage.getItem("theme");
root.dataset.theme = theme === "dark" ? "dark" : "light";
} catch {
root.dataset.theme = "light";
}
};
const initThemeToggle = () => {
const button = document.querySelector("[data-theme-toggle]");
if (!(button instanceof HTMLButtonElement) || button.dataset.themeReady === "true") {
return;
}
const syncButton = () => {
button.setAttribute("aria-pressed", String(getTheme() === "dark"));
};
syncButton();
button.addEventListener("click", () => {
const nextTheme = getTheme() === "dark" ? "light" : "dark";
root.dataset.theme = nextTheme;
localStorage.setItem("theme", nextTheme);
syncButton();
});
};
applySavedTheme();
initThemeToggle();
})();
</script>
这里最关键的一点,是 applySavedTheme() 先执行,再去初始化按钮。这样进入页面时,根节点主题已经先落下去了,不会等按钮绑定完才突然变色。
ThemeController 和 Header 通过 data attribute 解耦
按钮本身不在 ThemeController 里,而在 SiteHeader.astro 里:
<button
class="site-button site-theme-toggle"
type="button"
data-theme-toggle
aria-label={themeToggleDarkLabel}
title={themeToggleDarkLabel}
data-theme-label-light={themeToggleLightLabel}
data-theme-label-dark={themeToggleDarkLabel}
>
<span class="site-theme-toggle-icons" aria-hidden="true">...</span>
</button>
这意味着:
- Header 只关心按钮长什么样、放在哪
- ThemeController 只关心找到这个按钮后怎么工作,并同步当前可切换的提示文案
两边通过 data-theme-toggle 这个约定连接,不需要彼此 import。
视觉层统一放在 LayoutStyles
主题变量和代码高亮联动不放在脚本里,而放在 LayoutStyles.astro:
:root {
color-scheme: light;
--page-bg: #fffdf8;
--page-fg: #1f2328;
--surface: #ffffff;
}
:root[data-theme="dark"] {
color-scheme: dark;
--page-bg: #12161c;
--page-fg: #eef2f6;
--surface: #171c23;
}
:root[data-theme="dark"] .astro-code,
:root[data-theme="dark"] .astro-code span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
这样主题切换触发的只是根节点属性变化,具体要变哪些颜色、代码块如何跟随,全部交给 CSS。
多语言页面为什么不需要写两套主题逻辑
中英文页面的差异,在当前仓库里只体现在这些地方:
- 路由前缀不同
- 页面 copy 不同
- Header 中的切换按钮提示文案不同
主题脚本完全不用分语言。中文页和英文页共享同一个 ThemeController,只是在 site.ts 里给出不同的目标态文案:
- 中文:
切换到明亮模式/切换到暗黑模式 - 英文:
Switch to light mode/Switch to dark mode
因此“多语言主题切换”的正确拆法,不是做两套主题组件,而是让一套主题行为吃站点文案。
这次拆分真正得到的结果
现在主题系统的职责边界很清楚:
ThemeController负责状态恢复和点击切换SiteHeader提供按钮入口LayoutStyles负责主题变量和代码高亮视觉site.ts负责中英文提示文案
这样之后,无论是继续调整配色、替换按钮样式,还是把主题开关挪到别的位置,都不需要重新改路由或页面数据层。