65.9K
CodeProject 正在变化。 阅读更多。
Home

Blazor Web Assembly (WASM) 主题切换

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2022年1月22日

CPOL

7分钟阅读

viewsIcon

22024

downloadIcon

416

用户偏好主题支持 - 支持操作系统/浏览器偏好设置到自定义用户选择

目录

引言

本文从需求到实现进行探讨,以帮助读者理解主题支持的实现方式和使用方法。文中穿插了代码片段,而非完整的代码转储。所有代码和示例项目都包含在下载文件中,供进一步学习和尝试。

本文假定您对 Blazor 有基本了解,并链接到各种外部资源,您可能需要在这些资源中寻求进一步的信息和/或解释。

灵感来源

我想要一个带有动画的现代主题切换按钮。我喜欢 Google Fonts 网站上的按钮,但我是一名后端开发人员,不是 UI 网页设计师。这是他们按钮的演示。

(点击上面的图片查看主题切换效果)

幸运的是,Kevin Powell 接受了他一个观众的挑战,并创建了一个类似的按钮。您可以在他的 YouTube 频道观看它是如何制作的

下面是将 Kevin 的按钮集成到这个 Blazor 解决方案中的效果。

(点击上面的图片查看主题切换效果)

设计理念

核心理念是:

  1. 代码极简 - 完全封装或开放以供自定义实现
  2. 设计开放,可与任何定制代码或 CSS 框架(如 Bootstrap、Tailwind 等)配合使用
  3. 可在多个项目中重复使用
  4. 尽量减少 JavaScript 的使用(如果不可避免)
  5. 采用最新的 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. 将按钮链接到切换

启用主题切换有三个部分:

  1. 用户选择 - 在这种情况下,是一个切换按钮。您也可以使用下拉列表或更定制化的选择。
  2. 在 `MainLayout.razor` 文件中切换主题。
  3. 将选择链接到切换。我们将使用一个名为 `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

  1. 打开开发者工具
  2. 点击三个点选择更多选项,然后选择“更多工具”,再选择“渲染”。

  3. 向下滚动直到看到“prefers-color-scheme”下拉选择。

FireFox

打开开发者工具,选择“页面检查器”,有按钮可以在浅色(太阳)和深色(月亮)模式之间切换。

Opera

选择最右边的“简易设置”按钮,您可以选择浅色、深色和系统 OS 模式。

总结

该库封装了管理主题状态和切换所需的所有功能,支持自动存储,支持多种主题技术,以及现代的 Toggle。只需几行代码即可集成到您自己的项目中。

尽情享用!

历史

  • v1.0 - 2022年1月23日 - 初始发布
  • v1.01 - 2022年1月31日 - 在“实现”部分添加了更多信息。
© . All rights reserved.