Blazor Web Assembly (WASM) 主题切换





5.00/5 (5投票s)
用户偏好主题支持 - 支持操作系统/浏览器偏好设置到自定义用户选择
目录
引言
本文从需求到实现进行探讨,以帮助读者理解主题支持的实现方式和使用方法。文中穿插了代码片段,而非完整的代码转储。所有代码和示例项目都包含在下载文件中,供进一步学习和尝试。
本文假定您对 Blazor 有基本了解,并链接到各种外部资源,您可能需要在这些资源中寻求进一步的信息和/或解释。
灵感来源
我想要一个带有动画的现代主题切换按钮。我喜欢 Google Fonts 网站上的按钮,但我是一名后端开发人员,不是 UI 网页设计师。这是他们按钮的演示。
(点击上面的图片查看主题切换效果)
幸运的是,Kevin Powell 接受了他一个观众的挑战,并创建了一个类似的按钮。您可以在他的 YouTube 频道上 观看它是如何制作的。
下面是将 Kevin 的按钮集成到这个 Blazor 解决方案中的效果。
(点击上面的图片查看主题切换效果)
设计理念
核心理念是:
- 代码极简 - 完全封装或开放以供自定义实现
- 设计开放,可与任何定制代码或 CSS 框架(如 Bootstrap、Tailwind 等)配合使用
- 可在多个项目中重复使用
- 尽量减少 JavaScript 的使用(如果不可避免)
- 采用最新的 Blazor 和 CSS3 编码技术
实现
本文中使用了两种不同的主题切换方法
1. 交换样式表
允许将不同的主题分离到单独的文件中。这允许从第三方网站(如 Bootswatch)下载主题。我在本文中使用了他们的 Darkly(暗色主题)和 Flatly(亮色主题)。这是一种较旧的主题切换技术。
实现主题支持的最佳位置是尽可能靠近 DOM 的顶部。这在 `MainLayout.razor` 中完成。
对于样式表切换,我们需要修改页面的 `head` 部分。为此,我们在 `MainLayout.razor` 中使用 `HeadContent` 组件。
<HeadContent>
/* elements go here */
</HeadContent>
使用 `HeadContent` 组件时,顺序很重要。该组件会将内容添加到页面 `head` 的底部。
CSS 样式表通常添加到 `index.html` 文件中。但是,为了切换样式表,我们从 `index.html` 中移除颜色样式,并将文件放在 `MainLayout.razor` 文件中的 `
<HeadContent>
<Themes>
<DarkMode>
<link href="css/bootstrap/darkly.min.css" rel="stylesheet" />
</DarkMode>
<LightMode>
<link href="css/bootstrap/flatly.min.css" rel="stylesheet" />
</LightMode>
</Themes>
<link href="css/app.css" rel="stylesheet" />
<link href="ThemeByStylesheetDemo.styles.css" rel="stylesheet" />
</HeadContent>
应用程序 CSS 不需要更改,因为每个主题文件都应用相同的 CSS 规则。这种方法侵入性最小,但切换主题时,页面刷新可能会出现轻微的闪烁。
2. CSS 变量
CSS 变量,也称为自定义属性,是当今网站使用的现代且推荐的技术。像 Open Props 这样的 CSS 框架广泛使用CSS 变量。
主题切换是通过CSS 类完成的。在我们的例子中,我们默认使用浅色模式,并通过添加一个 CSS 类名(例如 `dark`)来切换模式。CSS 标记看起来大致如下:
:root {
/* Light theme */
--background: #fff;
--font-color: #000;
--font-color-2: #fff;
--highlight: #f7f7f7;
--highlight-2: #95a6a6;
--link: #0366d6;
}
.dark {
/* dark theme */
--background: #222;
--font-color: #fff;
--font-color-2: #fff;
--highlight: #393939;
--highlight-2: #444444;
--link: #3ca4ff;
}
使用 CSS 变量
.page {
background-color: var(--background);
color: var(--font-color);
}
此方法确实需要使用 CSS 变量,因此需要更改现有的样式表代码和内联样式规则。但好处是浏览器更新时不会出现闪烁。另一个好处是您现在正在使用共享变量,并且 CSS 更易于维护。
3. 主题切换
上面动画示例(例如 Google Fonts 网站)有一个手动切换按钮。还有一个媒体查询用于检测操作系统或 Web 浏览器的用户更改。
因此,通常在 CSS 中,我们会使用 `prefers-color-scheme` 媒体查询。
为了使此功能正常工作,我们需要侦听更改事件。Blazor 目前无法直接侦听媒体查询,因此我们需要使用一些 JavaScript 并带有回调到 Blazor。
这是 JavaScript
function createThemeListener(dotNetRef) {
window.matchMedia("(prefers-color-scheme: dark)").addListener(
e => dotNetRef.invokeMethodAsync("DarkModeStateChanged", e.matches)
);
Blazor 中的初始化器,我们将类引用传递给回调
_jsRuntime = jsRuntime;
_moduleTask = new(() => jsRuntime.ModuleFactory(ScriptFile));
IJSObjectReference module = await _moduleTask!.Value;
DotNetInstance = DotNetObjectReference.Create(this);
JavaScript 事件回调到 Blazor
[JSInvokable]
public async Task DarkModeStateChanged(bool state)
=> await SetDarkModeAsync(state).ConfigureAwait(false);
JavaScript 代码位于库中。使用最新版本的 Blazor,无需手动将其添加到 `index.html` 文件即可包含 JavaScript 文件。为此,我们 `export` JavaScript 函数。编译器会识别这一点并为我们包含 JavaScript。因此,更新后的 JavaScript 如下所示:
export function isDarkTheme() {
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
export function createThemeListener(dotNetRef) {
window.matchMedia("(prefers-color-scheme: dark)").addListener(
e => dotNetRef.invokeMethodAsync("DarkModeStateChanged", e.matches)
);
}
export function getLocalStorage(key) {
return localStorage[key];
}
export function setLocalStorage(key, value) {
localStorage[key] = value;
}
您可以在 Microsoft 的文档中了解更多关于此工作原理的信息。
4. 将按钮链接到切换
启用主题切换有三个部分:
- 用户选择 - 在这种情况下,是一个切换按钮。您也可以使用下拉列表或更定制化的选择。
- 在 `MainLayout.razor` 文件中切换主题。
- 将选择链接到切换。我们将使用一个名为 `ThemeService` 的服务来实现这一点。
Dot Net Core 使用 IOC 容器来实现 依赖注入,以自动连接类与其依赖项。
`ThemeService` 类负责共享主题状态和用户更改的通知,这些更改可能来自 `ThemeToggle` 按钮组件,也可能来自操作系统或浏览器更改。任何更改都在 `MainLayout.razor` 组件中处理。
代码
包含示例项目以演示每种主题切换模式的工作原理。两个示例项目都使用了包含的 `ThemeToggle` 组件,但您可以将其替换为您自己的组件。
主题库
该库封装了所有核心功能,便于重复使用
- 自动包含所有库 CSS 和 JavaScript 在主项目中
1. ThemeToggle 组件
- 符合 ARIA 标准
- BEM CSS 类命名约定
- 动画效果,动画效果极少
- 浅色或深色状态
- 可选的 `ShowTooltip` 属性
- 自定义 `DarkTipMessage` 和 `LightTipMessage` 属性
- 支持 16、24、43、48 像素 `ButtonSize`
- 自定义 `Style`、`class` 和 `attribute`
- `OnDarkModeStateChanged` 事件
@inject IThemeService themeService
<button @attributes="@Attributes"
style="@Style"
class="@GetComponentCssClass()"
aria-label="@GetToolTip()"
@onclick="_ => ToggleTheme()">
<svg xmlns="http://www.w3.org/2000/svg"
max-width="24px" max-height="24px"
viewBox="0 0 472.39 472.39">
<g class="theme-toggle__sun">
<path d="M403.21,167V69.18H305.38L236.2,0,167,69.18H69.18V167L0,236.2l69.18,
69.18v97.83H167l69.18,69.18,69.18-69.18h97.83V305.38l69.18-69.18Zm-167,
198.17a129,129,0,1,1,129-129A129,129,0,0,1,236.2,365.19Z"/>
</g>
<g class="theme-toggle__circle">
<circle cx="236.2" cy="236.2" r="103.78"/>
</g>
</svg>
</button>
按下按钮时,它会通知 `ThemeService`
private void ToggleTheme()
=> themeService.DarkMode = !themeService.DarkMode;
2. ThemeService (核心)
-
`DarkMode` 属性用于主题状态 - 浅色或深色
-
将状态更改存储到浏览器的 `localstorage` 中,以便在页面重新加载、更改和以后再次访问网站时记住用户的选择。
-
侦听 `prefers-color-scheme` 媒体查询更改事件
-
`OnDarkModeStateChanged` 事件用于通知更改
当状态发生更改时,将执行以下代码:
private async Task SetDarkModeAsync(bool value)
{
_darkMode = value;
// store user's currently selected color scheme for the app
await (await GetModuleInstance())
.SetLocalStorageThemeAsync(_darkMode);
OnDarkModeStateChanged?.Invoke(DarkMode);
}
`SetLocalStorageThemeAsync` 是一个扩展方法,它包装了用于存储的 JavaScript 调用
internal static class IJSObjectReferenceExtensions
{
private static string JSSetLocalStorage = "setLocalStorage";
public static async Task SetLocalStorageAsync(
this IJSObjectReference? jsObjRef, string key, string value)
=> await jsObjRef!.InvokeVoidAsync(JSSetLocalStorage, key, value)
.ConfigureAwait(false);
public static async Task SetLocalStorageThemeAsync(
this IJSObjectReference? jsObjRef, bool IsDarkTheme)
=> await jsObjRef!.SetLocalStorageAsync(ThemeKey,IsDarkTheme
? DarkThemeValue : LightThemeValue).ConfigureAwait(false);
}
以及 JavaScript 代码
function setLocalStorage(key, value) {
localStorage[key] = value;
}
3a. Themes 组件方法
此组件用于样式表切换
- 自动选择浅色或深色主题
- 初始化 `ThemeService` 并侦听 `OnDarkModeStateChanged` 事件以获取更改并触发渲染更新。
这是主题切换的标记
@if (ThemeService is not null && ThemeService.DarkMode)
{
@if (DarkMode is not null)
{
@DarkMode
}
}
else
{
@if (LightMode is not null)
{
@LightMode
}
}
以及监听并触发渲染更新的代码
protected override async Task OnInitializedAsync()
{
if (ThemeService is not null)
{
await ThemeService.InitializeAsync()!;
ThemeService.OnDarkModeStateChanged+= OnDarkModeChanged;
}
await base.OnInitializedAsync();
}
private void OnDarkModeChanged(bool State) => StateHasChanged();
在您的应用中,组件在 `MainLayout.razor` 中的用法将是:
<HeadContent>
<Theming.Themes>
<DarkMode>
<link href="css/bootstrap/darkly.min.css" rel="stylesheet" />
</DarkMode>
<LightMode>
<link href="css/bootstrap/flatly.min.css" rel="stylesheet" />
</LightMode>
</Theming.Themes>
<link href="css/app.css" rel="stylesheet" />
<link href="ThemeTest.styles.css" rel="stylesheet" />
</HeadContent>
3b. CSS 类更改方法
手动将 `MainLayout.razor` 连接到 CSS 类选择
@inject IThemeService themeService
<div class="@GetClassCss()">
以及管理类的代码
private bool IsDarkMode;
protected override async void OnInitialized()
{
// uncomment if not using our ThemeToggle component
//await themeService.InitializeAsync();
themeService.OnDarkModeStateChanged += OnDarkModeChanged;
await base.OnInitializedAsync();
}
private void OnDarkModeChanged(bool state)
{
IsDarkMode = state;
StateHasChanged();
}
private string GetClassCss()
=> "page" + (IsDarkMode ? " dark" : "");
测试主题切换
要测试浅色和深色主题之间的切换,您可以设置 Windows 或 Mac OS 中的偏好设置,或使用 Web 浏览器的设置/开发者工具。下面列出了常见浏览器中选项的位置。
Chrome/Edge
- 打开开发者工具
- 点击三个点选择更多选项,然后选择“更多工具”,再选择“渲染”。
- 向下滚动直到看到“prefers-color-scheme”下拉选择。
FireFox
打开开发者工具,选择“页面检查器”,有按钮可以在浅色(太阳)和深色(月亮)模式之间切换。
Opera
选择最右边的“简易设置”按钮,您可以选择浅色、深色和系统 OS 模式。
总结
该库封装了管理主题状态和切换所需的所有功能,支持自动存储,支持多种主题技术,以及现代的 Toggle。只需几行代码即可集成到您自己的项目中。
尽情享用!
历史
- v1.0 - 2022年1月23日 - 初始发布
- v1.01 - 2022年1月31日 - 在“实现”部分添加了更多信息。