戏里戏外

使用 Laravel、Tailwind 和 Alpine.js 实现主题切换

2024-11-27#Laravel

在现代网页设计中,深色主题不仅仅是一种潮流,更是一种突破。

它不仅为夜间编程爱好者提供了视觉舒适感,还为用户界面增添了一丝优雅。

Toggle theme using tailwind and alpine.js

基础设置

修改 tailwind.config.js 文件中的 darkMode 配置使用 class 模式:

export default {
  darkMode: 'class', // 深色模式的关键配置!
  content: [
    "./resources/**/*.blade.php",
    "./resources/**/*.js",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

welcome.blade.php 文件中添加 Alpine.js 指令:

<body	class="antialiased"
			x-data="{darkMode: false}"
			:class="{'dark': darkMode === true }">
  • x-data 指令用于设置组件的初始状态。
  • :class="{'dark': darkMode === true }" 指令用于根据 darkMode 的值动态添加或移除 dark 类。

并且引入 app.cssapp.js 文件:

@vite(['resources/css/app.css', 'resources/js/app.js'])

创建手动深色主题切换组件

创建一个主题切换按钮组件 resources/views/components/switch-theme.blade.php

<button @click="darkMode=!darkMode" type="button" class="relative inline-flex flex-shrink-0 h-6 mr-5 transition-colors duration-200 ease-in-out border-2 border-transparent rounded-full cursor-pointer bg-zinc-200 dark:bg-zinc-700 w-11 focus:outline-none focus:ring-2 focus:ring-neutral-700 focus:ring-offset-2" role="switch" aria-checked="false">
  <span class="sr-only">Switch Theme</span>
  <span class="relative inline-block w-5 h-5 transition duration-500 ease-in-out transform translate-x-0 bg-white rounded-full shadow pointer-events-none dark:translate-x-5 ring-0">
    <!-- 太阳图标 -->
    <span class="absolute inset-0 flex items-center justify-center w-full h-full transition-opacity duration-500 ease-in opacity-100 dark:opacity-0 dark:duration-100 dark:ease-out" aria-hidden="true">
      <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-sun w-4 h-4 text-neutral-700" width="24" height="24" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
        <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
        <path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path>
        <path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7"></path>
      </svg>
    </span>
    <!-- 月亮图标 -->
    <span class="absolute inset-0 flex items-center justify-center w-full h-full transition-opacity duration-100 ease-out opacity-0 dark:opacity-100 dark:duration-200 dark:ease-in" aria-hidden="true">
      <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-moon w-4 h-4 text-neutral-700" width="24" height="24" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
        <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
        <path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z"></path>
      </svg>
    </span>
  </span>
</button>

解决页面刷新问题

使用 Alpine.jspersist 插件来保持主题状态:

yarn add -D @alpinejs/persist

更新 resources/js/app.js

import './bootstrap';
import Alpine from 'alpinejs'
import persist from '@alpinejs/persist'

window.Alpine = Alpine
Alpine.plugin(persist)
Alpine.start()

解决闪烁问题

resources/css/app.css 中添加:

@tailwind base;
@tailwind components;
@tailwind utilities;

[x-cloak] { display: none !important; }

更新 body 标签,添加 x-clock 属性:

<body class="antialiased"
			x-cloak
			x-data="{darkMode: $persist(false)}"
			:class="{'dark': darkMode === true }">
  • x-cloak 指令用于在 Alpine.js 初始化完成之前隐藏元素。
  • x-data="{darkMode: $persist(false)}" 指令用于设置 darkMode 的初始状态,并使用 persist 插件来保持其值。该值被存储在浏览器的 localStorage 中的 _x_darkMode 键中。
  • :class="{'dark': darkMode === true }" 指令用于根据 darkMode 的值动态添加或移除 dark 类。

Dusk 测试

如果还没有安装 Laravel Dusk,可以通过以下命令安装:

composer require --dev laravel/dusk
php artisan dusk:install

创建一个新的 Dusk 测试文件 tests/Browser/ThemeSwitchTest.php

<?php

use Laravel\Dusk\Browser;

it('can switch between light and dark theme', function () {
	$this->browse(function (Browser $browser) {

		$browser->visit('/')
			// 确认初始状态是亮色主题
			->assertMissing('.dark')

			// 点击主题切换按钮
			->click('@theme-switch')
			->pause(500) // 等待动画完成

			// 确认主题设置被保持
			->assertPresent('.dark')

			// 再次点击切换回亮色主题
			->click('@theme-switch')
			->pause(500)

			// 确认回到亮色主题
			->assertMissing('.dark');
    });
});

it('theme preference persists after page reload', function () {
	$this->browse(function (Browser $browser) {
			$browser->visit('/')
				// 切换到深色主题
				->click('@theme-switch')
				->pause(500)

				// 刷新页面
				->refresh()

				// 确认主题设置被保持
				->assertPresent('.dark');
    });
});

为了让测试能够正常运行,需要在主题切换按钮上添加 dusk 属性:

<button dusk="theme-switch" @click="darkMode=!darkMode" type="button" class="relative inline-flex flex-shrink-0 h-6 mr-5 transition-colors duration-200 ease-in-out border-2 border-transparent rounded-full cursor-pointer bg-zinc-200 dark:bg-zinc-700 w-11 focus:outline-none focus:ring-2 focus:ring-neutral-700 focus:ring-offset-2" role="switch" aria-checked="false">
    <!-- ... 按钮内容保持不变 ... -->
</button>

这些测试用例会验证:

  1. 主题切换按钮能够正常工作
  2. 主题设置在页面刷新后能够保持
  3. 深色主题类名正确添加和移除

最后,需要在 DuskTestCase 中重写 newBrowser 方法:

protected function newBrowser($driver): Browser
{
  return new Browser($driver, new ElementResolver($driver, ''));
}

运行测试:

php artisan dusk tests/Browser/ThemeSwitchTest.php
运行Dusk测试之前,请确保:
  1. 已经安装并配置好 Laravel Dusk
  2. Chrome 驱动已更新到最新版本
  3. 开发服务器正在运行