前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Nuxt3在使用Tailwindcss情况下,如何优雅实现深色模式切换?

Nuxt3在使用Tailwindcss情况下,如何优雅实现深色模式切换?

原创
作者头像
Mintimate
发布2023-07-31 09:33:00
1.3K0
发布2023-07-31 09:33:00
举报
文章被收录于专栏:Mintimate's BlogMintimate's Blog

博客:https://www.mintimate.cn Mintimate’s Blog,只为与你分享

封面不能少ヾ(≧≦)〃
封面不能少ヾ(≧≦)〃

深色模式

随着前端更新,网站设计中,深色模式也成为了一种备受欢迎的设计趋势。可以帮助用户减少眼睛的负担,同时也更加适合在光线较暗的环境下使用。

打个比方,日常下班坐地铁、公车回家,地铁还好,都有灯,公车…… 有时候在跨区站的时候,司机会关灯,这个时候,深色模式就太刚需了😭。

换一个角度,现在系统都有深色模式,浏览器也有深色模式,那么看着别人的网站也有深色,自己的网站怎么能少?开发网站,这个优先级必须提高呀。(当然,一些网站确实就没必要设计深色,比如图形和图表为主要内容的网站、颜色为品牌标识的网站)。

Github的深色模式
Github的深色模式

比较有趣的是,Github的深色模式,目前要么选择跟随系统,要么在用户设置里进行手动设置;藏的比较隐蔽,似乎是怕打破用户的日常习惯?

再提一下,Gthub使用的Cookies进行存储,加快页面渲染:

Github的深色规则
Github的深色规则

{"color_mode":"auto","light_theme":{"name":"light","color_mode":"light"},"dark_theme":{"name":"dark_colorblind","color_mode":"dark"}

而我们使用Nuxt进行操作。

Nuxt3&Tailwindcss

恩…… 翻看腾讯云开发者社区,似乎做运维和后端的人比较多,鲜有人接受前端的;难道前端真的已死么🤔。

哈哈,不开玩笑~ 为了照顾更多小白用户,这里简单介绍什么是Nuxt3~

简单地说,Nuxt3就是一套SSR的Vue3框架,与之对等的,就是React的Next3。不同于Vue3官方的SSR方案依赖于Vue SSR库,在使用上需要手动编写一些服务器端渲染的代码,比如借助ExpressJS实现;Nuxt3则提供了更加简单、易用的服务器端渲染功能框架,可以轻松地实现服务器端渲染和预渲染,并且支持自动装载和静态生成。此外,Nuxt3还提供了一些额外的特性,比如自动生成路由、模块化开发、静态资源优化等,可以帮助我们更加高效地进行开发和部署。

当然,把Nuxt3直接和Next3画约等于,基本可以,即: Nuxt3 ≈ Next3

有利也有弊,Nuxt3把Vue3的生命周期钩子函数进行扩充。一些组件,在Vue3上可以使用,在Nuxt3上的Server端,可能就会出现问题。比如:目前arco-design: https://github.com/arco-design/arco-design-vue目前就和Nuxt3有严重冲突问题。

目前比较好的组件样式,我个人还是推荐: Tailwindcss: https://tailwindcss.com/

tailwindcss
tailwindcss

哈哈,是不是有小伙伴有疑问,这个只是一个CSS组件库,和ElementUI那样的组件,不是一个概念?

Tailwindcss好在,就是有大量给予它开发的组件,比如我用的: NuxtLabs UI: https://ui.nuxtlabs.com/getting-started

深色模式实现

现在,我们确定了使用的技术框架和使用的样式,再来分析一下深色模式的实现思路,并且对比Tailwindcss是如何操作。

思考思路
思考思路

样式叠加

老生常谈的方法,深色模式使用样式叠加来实现。举个例子,我们当前有一个DOM结构:

代码语言:html
复制
<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8">
    <title>Dark Mode Example</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <header>
      <h1>My Website</h1>
      <nav>
        <ul>
          <li><a href="#">Home</a></li>
          <li><a href="#">About</a></li>
          <li><a href="#">Contact</a></li>
        </ul>
      </nav>
    </header>
    <main>
      <h2>Welcome to my website</h2>
      <p>This is a paragraph of text.</p>
      <button>Click me</button>
    </main>
  </body>
</html>

那么,如何做到深色模式呢?

很简单,利用CSS的样式叠加:

代码语言:css
复制
# 限定含有dark类时候的main
.dark main{
    background-color: #1a1a1a;
    color: #ffffff;
}

并且,在展示页面时候;在<html>上,加上class="dark"

而Tailwindcss,官方实现的方法,就是我们这样:

代码语言:html
复制
<!-- 未激活Dark模式 -->
<html>
<body>
  <!-- 这里显示白色 -->
  <div class="bg-white dark:bg-black">
    <!-- ... -->
  </div>
</body>
</html>

<!-- 激活Dark模式 -->
<html class="dark">
<body>
  <!-- 这里将是黑色 -->
  <div class="bg-white dark:bg-black">
    <!-- ... -->
  </div>
</body>
</html>

不同的是,官方使用dark:来控制深色模式特定显示的样式,这样更有益于原子级操作,实现的效果:

亮色模式下
亮色模式下
深色模式下
深色模式下

CSS变量

与此同时,如果页面上有很多的元素,一个一个设置颜色数值也不是办法,过多的颜色,也容易让人冲昏头脑。

我们使用CSS变量定义颜色:

代码语言:css
复制
:root {
  --primary-color: #1a1a1a; /* 定义一个名为primary-color的自定义属性 */
}

.dark main {
  background-color: var(--primary-color); /* 使用名为primary-color的自定义属性 */
  color: white;
  padding: 8px 16px;
}

再来看看Tailwindcss,其实它的方法就在上文已经明示,使用bg:进行亮色模式的区分。

切换模式

上述的思路已经完成,我们切换亮色和深色的方法,就是在<html>标签上,加上class="dark"即可。

使用JavaScript实现很简单:

代码语言:javascript
复制
// 使用localstorge存储深色和亮色模式
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { // 媒体查询系统模式
  document.documentElement.classList.add('dark')
} else {
  document.documentElement.classList.remove('dark')
}

切换按钮,在Vue3内也很简单实现:

代码语言:javascript
复制
<script setup>
import { ref, onMounted } from 'vue';

const dark = ref(false);

// 设置初始主题
onMounted(() => {
  const localStorageTheme = localStorage.getItem('tool-theme-mode');
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

  if (localStorageTheme === 'dark' || (!localStorageTheme && prefersDark)) {
    document.documentElement.classList.add('dark');
    dark.value = true;
  }
});

// 切换主题
const handleToggleTheme = () => {
  dark.value = !dark.value;

  if (dark.value) {
    localStorage.setItem('tool-theme-mode', 'dark');
    document.documentElement.classList.add('dark');
  } else {
    localStorage.removeItem('tool-theme-mode');
    document.documentElement.classList.remove('dark');
  }
};
</script>

存在问题

好的,我们看起来都已经完成了一切操作。

完成啦?
完成啦?

但是实际上,有一个问题: 刷新加载闪烁问题。

刷新时候,加载闪烁
刷新时候,加载闪烁

造成这个原因,主要有:

  • 因为Nuxt3存在一个服务器Server端;所以,在深色模式渲染时候,存在重复渲染问题。
  • 既是使用<ClientOnly>进行限制,页面加载是自上而下,但是onMounted的生命周期,发生在DOM元素加载完毕;所以也会造成闪烁问题。
  • localstorge的加载存在滞后问题,本身就有延时;使用Cookie就不存在这个问题;但是这不是主要原因,因为我Hexo博客也是用localstorge存储~

解决上述问题,最直接的方法就是把主题的判断提前。

如何提前,最好把主题模式的判断,提升到<head>里呢?

其实Nuxt3官方就有保留扩展入口:Nuxt head

Nuxt head
Nuxt head

这个配置其实是用来辅助SEO的,我们这里来穿插一个深色模式判断:

代码语言:javascript
复制
app:{
    // 生成的静态资源根目录
    buildAssetsDir:"/_toolStatic/",
    rootId:"contentId",
    head: {
        // 深色模式判断
        script: ["/darkVerify.js"],
    },
},

添加暗色模式判断:

代码语言:javascript
复制
// darkVerify.js
if (
    localStorage.getItem('tool-theme-mode') === "dark" ||
    (!localStorage.getItem('tool-theme-mode') &&
        window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
    document.querySelector('html').classList.add('dark');
    document.querySelector('html').classList.remove('light');
} else {
    document.querySelector('html').classList.add('light');
    document.querySelector('html').classList.remove('dark');
}

当然,刚刚的onMounted也需要改一下:

代码语言:javascript
复制
<script setup>
import { ref, onMounted } from 'vue';

const dark = ref(false);

// 设置初始主题
onMounted(() => {
  const localStorageTheme = localStorage.getItem('tool-theme-mode');
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  // 监听
  prefersDark.addEventListener('change', handleToggleTheme);
});

// 切换主题
const handleToggleTheme = () => {
  dark.value = !dark.value;

  if (dark.value) {
    localStorage.setItem('tool-theme-mode', 'dark');
    document.documentElement.classList.add('dark');
  } else {
    localStorage.removeItem('tool-theme-mode');
    document.documentElement.classList.remove('dark');
  }
};
</script>
穿插深色模式判断
穿插深色模式判断

之后,网页刷新,就可以看到效果:

页面刷新效果
页面刷新效果

后来又发现,怎么会存在两个key?

两个key
两个key

这个时候,才发现,我使用的NuxtLabs UI存在Nuxt Color Mode,这个好用而优雅的插件。

接下来,我们就使用Nuxt Color Mode来进一步优雅。

Nuxt Color Mode

注意⚠️,接下来的内容,需要对Nuxt3有一定了解。

其实原理和我们的head: {script: ["/darkVerify.js"]}是一样的。

我们进行简单的源码解析。

源码解析

观察客户端的插件:https://github.com/nuxt-modules/color-mode/blob/master/src/runtime/plugin.client.ts

我们从后往前看,先是默认情况下的模式判断,并创建媒体监听:

代码语言:javascript
复制
// 监听系统主题变化  
let darkWatcher: MediaQueryList

function watchMedia() {
  // 已经监听或不支持则返回
  if (darkWatcher || !window.matchMedia) { return } 

  darkWatcher = window.matchMedia('(prefers-color-scheme: dark)')
  darkWatcher.addEventListener('change', () => {
    // 如果没强制指定模式并且默认是系统模式,设置系统模式 
    if (!colorMode.forced && colorMode.preference === 'system') {
      colorMode.value = helper.getColorScheme()
    }
  })
}

// 首选项变化时处理 
watch(() => colorMode.preference, (preference) => {

  // 强制指定模式时返回
  if (colorMode.forced) {
    return
  }

  // 设置对应的值
  if (preference === 'system') {
    colorMode.value = helper.getColorScheme()
    watchMedia()
  } else {
    colorMode.value = preference
  }

  // 保存在localStorage中
  window.localStorage?.setItem(storageKey, preference)

}, { immediate: true })

// 值变化时添加删除类
watch(() => colorMode.value, (newValue, oldValue) => {
  helper.removeColorScheme(oldValue)
  helper.addColorScheme(newValue)
})

// 如果是系统模式,开始监听
if (colorMode.preference === 'system') {
  watchMedia()
}

// mounted时初始化
nuxtApp.hook('app:mounted', () => {
  if (colorMode.unknown) {
    colorMode.preference = helper.preference
    colorMode.value = helper.value
    colorMode.unknown = false 
  }
})

// 提供colorMode
nuxtApp.provide('colorMode', colorMode)

那么代码中的强制指定模式,是怎么判断的呢? 其实在上面的路由判断里:

代码语言:javascript
复制
useRouter().afterEach((to) => {
    const forcedColorMode = isVue2
      ? (to.matched[0]?.components.default as any)?.options.colorMode
      : to.meta.colorMode

    if (forcedColorMode && forcedColorMode !== 'system') {
      colorMode.value = forcedColorMode
      colorMode.forced = true
    } else {
      if (forcedColorMode === 'system') {
        // eslint-disable-next-line no-console
        console.warn('You cannot force the colorMode to system at the page level.')
      }
      colorMode.forced = false
      colorMode.value = colorMode.preference === 'system'
        ? helper.getColorScheme()
        : colorMode.preference
    }
  })

通过上述的源码判断,我们就可以知道;它会在路由的访问过程中,读取Meta信息,进行强制模式切换。

所以,我们在定义路由或者页面时候,就可以添加强制选项:

代码语言:javascript
复制
# 使用路由配置的话
{
    // 简体字、繁体字 互相转换
    path: '/zhConvertTradSimp',
    name: 'zhConvertTradSimp',
    meta: {
        colorMode: 'light',
    },
    component: () => import('@/pages/characterTool/zhConvertTradSimp.vue'),
},

# 使用Nuxt3 Page自动装载
<script setup>
definePageMeta({
  colorMode: 'light',
})
</script>
强制页面使用亮色模式
强制页面使用亮色模式

这个时候,进入这个路由或者在这个页面进行刷新,就会发现默认会强制使用亮色模式:

强制效果
强制效果

实际上,上述代码就是实现官网的这个功能:

对应功能
对应功能

再往上看,为什么会有这段代码呢?

代码语言:javascript
复制
import { globalName, storageKey, dataValue } from '#color-mode-options'
if (dataValue) {
    if (isVue3) {
        useHead({
        htmlAttrs: { [`data-${dataValue}`]: computed(() => colorMode.value) }
        })
    } else {
        const app = nuxtApp.nuxt2Context.app
        const originalHead = app.head
        app.head = function () {
        const head = (typeof originalHead === 'function' ? originalHead.call(this) : originalHead) || {}
        head.htmlAttrs = head.htmlAttrs || {}
        head.htmlAttrs[`data-${dataValue}`] = colorMode.value
        return head
        }
    }
}

很明显,首先要弄清dataValue是什么?

有趣😯
有趣😯

在检查了其他地方源码和官方文档,可以知道nuxt.config.ts内可以配置的内容:

代码语言:typescript
复制
{
  // 首选颜色模式,可以是 'light'、'dark' 或 'system'
  // 如果设置为 'system',则会根据用户的系统设置自动选择颜色模式
  // 默认值为 'system'
  preference: 'system',

  // 回退颜色模式,可以是 'light' 或 'dark'
  // 如果首选颜色模式无法使用,则会使用回退颜色模式
  // 默认值为 'light'
  fallback: 'light',

  // 存储颜色模式的键名,用于在本地存储中存储颜色模式的值
  // 默认值为 'nuxt-color-mode'
  storageKey: 'nuxt-color-mode',

  // 自定义数据属性的名称,用于在 HTML 标签上添加颜色模式的值
  // 如果设置为 undefined,则不会添加自定义数据属性
  // 默认值为 undefined
  dataValue: undefined
}

而我们的dataValue就是配置文件中的dataValue,默认为underfined所以默认是不会执行的。

再之后,我们就可以看看服务端代码了,服务端代码相对更简单,精减一下贴源码了:

代码语言:javascript
复制
import { reactive } from 'vue'

import type { ColorModeInstance } from './types'
import { defineNuxtPlugin, isVue2, isVue3, useHead, useState, useRouter } from '#imports'
import { preference, hid, script, dataValue } from '#color-mode-options'

// 重点ヾ(≧≦)〃 添加脚本到 head 中
const addScript = (head) => {
  head.script = head.script || []
  head.script.push({
    hid,
    innerHTML: script
  })
  const serializeProp = '__dangerouslyDisableSanitizersByTagID'
  head[serializeProp] = head[serializeProp] || {}
  head[serializeProp][hid] = ['innerHTML']
}

  // 在路由切换后处理颜色模式的变化
  useRouter().afterEach((to) => {
    // 获取强制的颜色模式
    const forcedColorMode = isVue2
      ? (to.matched[0]?.components.default as any)?.options?.colorMode
      : to.meta.colorMode

    // 如果存在强制的颜色模式,则更新颜色模式状态,并添加对应的自定义属性到 htmlAttrs 中
    if (forcedColorMode && forcedColorMode !== 'system') {
      colorMode.value = htmlAttrs['data-color-mode-forced'] = forcedColorMode
      if (dataValue) {
        htmlAttrs[`data-${dataValue}`] = colorMode.value
      }
      colorMode.forced = true
    } else if (forcedColorMode === 'system') {
      // 如果强制的颜色模式是 'system',则输出警告信息
      // eslint-disable-next-line no-console
      console.warn('You cannot force the colorMode to system at the page level.')
    }
  })

  // 将颜色模式状态对象作为 provide 提供给子组件
  nuxtApp.provide('colorMode', colorMode)
})

没错,大部分和服务端的效果差不多,主要是这段,很重要:

代码语言:javascript
复制
const addScript = (head) => {
  head.script = head.script || []
  head.script.push({
    hid,
    innerHTML: script
  })
  const serializeProp = '__dangerouslyDisableSanitizersByTagID'
  head[serializeProp] = head[serializeProp] || {}
  head[serializeProp][hid] = ['innerHTML']
}

在服务器响应给客户端的数据中,在头部插入script代码,也就是基于浏览器存储的深色模式判断,我们追溯import { preference, hid, script, dataValue } from '#color-mode-options',紧接着,查看项目的module.ts,便可以找到script的来源:

根据module.ts往上找到script来源
根据module.ts往上找到script来源

最后,我们可以知道:它通过直接在<head>中内联一个脚本,这个脚本会在页面其他元素渲染前执行:

  1. 该脚本会立即读取本地存储和系统偏好的值
  2. 然后直接操作 document.documentElement 加入主题类名
  3. 这个时机早于页面元素的渲染
head内联脚本
head内联脚本

所以页面渲染时已经应用了正确的主题类名,避免了主题延迟导致的闪屏。同时配合前文说的客户端插件,实现本地的系统深色模式切换监听和更改的接口方法。

真不错😁
真不错😁

接下来就看看怎么使用吧。

使用演示

现在,我们就来看看如何使用。

首先是安装:

代码语言:shell
复制
yarn add --dev @nuxtjs/color-mode

我使用的是NuxtLabs UI,在查看NuxtLabs UI的依赖包发现,它已经自带了@nuxtjs/color-mode

已经自带
已经自带

因为使用了tailwindcss,所以,我们在tailwind.config.js上,添加:

代码语言:javascript
复制
module.exports = {
    // 使用class进行暗色模式判断,而非媒体查询自动判断
  darkMode: 'class'
}

然后呢? 我们还需要在项目nuxt.config.ts配置文件内激活配置:

代码语言:javascript
复制
colorMode: {
    classSuffix: '', // 在 dark 或 light 类名后面添加 -mode 后缀
    storageKey: 'tool-theme-mode' // 存储颜色模式的键名,用于在本地存储中存储颜色模式的值
},

最后,我们定义一个组件按钮,用于切换深色模式:

代码语言:javascript
复制
// components/ColorModeButtom.vue
<script setup>
let colorMode;
colorMode = useColorMode();

const isDark = computed({
    get() {
        return colorMode.value === 'dark';
    },
    set() {
        colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark';
    },
});
</script>

<template>
    <ClientOnly>
        <UButton
            :icon="
                isDark
                    ? 'i-heroicons-moon-20-solid'
                    : 'i-heroicons-sun-20-solid'
            "
            color="gray"
            variant="ghost"
            aria-label="Theme"
            @click="isDark = !isDark"
        />
        <template #fallback>
            <div class="w-8 h-8 focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75 flex-shrink-0 font-medium rounded-md text-sm gap-x-1.5 p-1.5 text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-800 focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 inline-flex items-center">
              <span class="i-heroicons-cog-20-solid h-5 w-5"></span>
            </div>
        </template>
    </ClientOnly>
</template>

效果还不错:

最终效果
最终效果

是不是很优雅呢?

写在最后

好啦,本次“如何优雅实现深色模式切换?”的分享,就到这里啦。其实现在细想,还是存在优化的地方,比如: 如果想提高效率,localstorge的渲染还是存在延时读取问题,相对的Cookie就不存在这个问题。

至于,后续有优化,就等待各位吴彦祖们啦。

嘿嘿
嘿嘿

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 深色模式
  • Nuxt3&Tailwindcss
  • 深色模式实现
    • 样式叠加
      • CSS变量
        • 切换模式
          • 存在问题
          • Nuxt Color Mode
            • 源码解析
              • 使用演示
              • 写在最后
              相关产品与服务
              云开发 CloudBase
              云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档