加载中...

跟着vue学习深色模式和媒体查询

跟着vue学习深色模式和媒体查询

「本文已参与低调务实优秀中国好青年前端社群的写作活动」

背景

偶然的一天,我想做一个vue3的工程,图方便(我以前都是创建空目录然后一个个加内容的),使用了pnpm快速创建vue工程,使用以下命令

bash
代码解读
复制代码
pnpm create vue

经过一系列选择后工程就创建好了,执行pnpm installpnpm dev即可开启工程

大概通读过新工程代码后,发现这个工程内置了几个比较有意思的点,比如媒体查询 正常展示 New Image

缩小展示 New Image

比如深色模式 白天模式 New Image

深色模式 New Image

那么,我们就来深扒一下,vue是怎么完成这些功能的,并且我们用一个空工程来自己实现这些功能

创建空工程

因为vite提供的模板会比较简洁,我们使用vite的模板来开始我们的功能搭建,使用以下命令创建工程

bash
代码解读
复制代码
pnpm create vite

New Image

两个工程对比

New Image 这是vue模板的目录

New Image 这是vite模板的目录,由此可见vite模板会更简洁,侵入性会更低

执行pnpm installpnpm dev后,就能运行vite工程了 New Image

深色模式

我们先从深色模式开始入手

额外知识

  • win11开启深色模式的方式:在桌面右键,选择个性化,个性化里选择颜色,颜色里有个选择模式,点开选择深色,即可打开深色模式
  • win10操作方法类似,也是个性化-颜色-选择模式
  • mac我没有你们可以试下(羡慕的眼光

先来看调成深色模式后两个系统界面的区别,以下我会对两个基础工程分别称为vite/vue

  • vite版本 New Image
  • vue版本 New Image 可以看到,在深色模式下,vue版本的界面是有处理的,而vite版本的没有

样式文件

vue版本的样式文件放在src/assets/bass.css

css
代码解读
复制代码
/* color palette from <https://github.com/vuejs/theme> */ :root { --vt-c-white: #ffffff; --vt-c-white-soft: #f8f8f8; --vt-c-white-mute: #f2f2f2; --vt-c-black: #181818; --vt-c-black-soft: #222222; --vt-c-black-mute: #282828; --vt-c-indigo: #2c3e50; --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); --vt-c-text-light-1: var(--vt-c-indigo); --vt-c-text-light-2: rgba(60, 60, 60, 0.66); --vt-c-text-dark-1: var(--vt-c-white); --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); } /* semantic color variables for this project */ :root { --color-background: var(--vt-c-white); --color-background-soft: var(--vt-c-white-soft); --color-background-mute: var(--vt-c-white-mute); --color-border: var(--vt-c-divider-light-2); --color-border-hover: var(--vt-c-divider-light-1); --color-heading: var(--vt-c-text-light-1); --color-text: var(--vt-c-text-light-1); --section-gap: 160px; } @media (prefers-color-scheme: dark) { :root { --color-background: var(--vt-c-black); --color-background-soft: var(--vt-c-black-soft); --color-background-mute: var(--vt-c-black-mute); --color-border: var(--vt-c-divider-dark-2); --color-border-hover: var(--vt-c-divider-dark-1); --color-heading: var(--vt-c-text-dark-1); --color-text: var(--vt-c-text-dark-2); } } *, *::before, *::after { box-sizing: border-box; margin: 0; position: relative; font-weight: normal; } body { min-height: 100vh; color: var(--color-text); background: var(--color-background); transition: color 0.5s, background-color 0.5s; line-height: 1.6; font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; font-size: 15px; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }

可以看到,vue版本将需要全局共享的样式,放在了:root下,下面是MDN:root的解释

:root 这个 CSS 伪类匹配文档树的根元素。对于 HTML 来说,:root 表示 元素,除了优先级更高之外,与 html 选择器相同。

也就是说,将css变量,存放到html中,这样就能在工程的哪个文件都能用到这个css变量

实践

我们先对背景色进行改造

  • assets目录下创建base.css
  • 添加需要的背景颜色
css
代码解读
复制代码
:root { --dmd-white: #ffffff; ---dmd-dark: #181818; } :root { --color-background: var(--dmd-white); } @media (prefers-color-scheme: dark) { :root { --color-background: var(---dmd-dark); } } *, *::before, *::after { box-sizing: border-box; margin: 0; position: relative; font-weight: normal; } body { min-height: 100vh; background: var(--color-background); transition: background-color .5s; }

App.vue中引入样式文件

html
代码解读
复制代码
<script setup lang="ts"> // This starter template is using Vue 3 <script setup> SFCs // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup import HelloWorld from "./components/HelloWorld.vue"; </script> <template> <img alt="Vue logo" src="./assets/logo.png" /> <HelloWorld msg="Hello Vue 3 + TypeScript + Vite" /> </template> <style> /* 这里引入样式 */ @import "./assets/base.css"; #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>

这样背景的深色模式就做好啦! New Image

通过更改系统颜色,背景色也会跟着变(这里就不贴图了)

但这样还是不够完善的,我们需要对字体也进行深色模式适配

css
代码解读
复制代码
:root { --dmd-white: #ffffff; ---dmd-dark: #181818; --dmd-text-white: #2c3e50; --dmd-text-dark: rgba(235, 235, 235, 0.64); } :root { --color-background: var(--dmd-white); --color-text: var(--dmd-text-white); } /* 重点 */ @media (prefers-color-scheme: dark) { :root { --color-background: var(---dmd-dark); --color-text: var(--dmd-text-dark); } } *, *::before, *::after { box-sizing: border-box; margin: 0; position: relative; font-weight: normal; } body { min-height: 100vh; /* 将App.vue中的color移到这里,更好的管理字体和背景色 */ color: var(--color-text); background: var(--color-background); transition: color .5s, background-color .5s; }

我将App.vue中的color样式移到base.css了,这样能在一个文件就管理

html
代码解读
复制代码
<style> /* 这里引入样式 */ @import "./assets/base.css"; #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; /* color: #2c3e50; */ margin-top: 60px; } </style>

这样,根据系统色进行模式切换已经完成了 New Image

核心内容

注意看base.css中的一个媒体查询

css
代码解读
复制代码
/* 重点 */ @media (prefers-color-scheme: dark) { :root { --color-background: var(---dmd-dark); --color-text: var(--dmd-text-dark); } }

我们先来看下MDN对这个用法的解释

prefers-color-scheme CSS 媒体特性用于检测用户是否有将系统的主题色设置为亮色或者暗色。

也就是说,当用户系统主题色为暗色时,就会触发这个media,然后使用里面的css样式,这样就能做到根据系统颜色进行亮暗切换

自定义深色模式事件

以上用法都是根据系统色进行主题切换的,那我办法自定义方法来控制亮暗模式吗?

我们可以使用vueuse中的useDark方法来控制

初探vueuse

vueuse官网有一个useDarkhook,我们可以用这个完成基本的深色模式 点击这里可以看useDark介绍,下面是官方github提供的demo

html
代码解读
复制代码
<script setup lang="ts"> import { useToggle } from ''@vueuse/shared'' import { isDark } from ''../../.vitepress/theme/composables/dark'' // const isDark = useDark() const toggleDark = useToggle(isDark) </script> <template> <button @click="toggleDark()"> <i inline-block align-middle i="dark:carbon-moon carbon-sun" /> <span class="ml-2">{{ isDark ? ''Dark'' : ''Light'' }}</span> </button> </template>

下面是官方介绍中提供的基本用法

js
代码解读
复制代码
import { useDark, useToggle } from ''@vueuse/core'' const isDark = useDark() const toggleDark = useToggle(isDark)

使用vueuse

下面我们来改造页面,在App.vue中加个按钮进行主题切换

html
代码解读
复制代码
<script setup lang="ts"> // This starter template is using Vue 3 <script setup> SFCs // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup import HelloWorld from "./components/HelloWorld.vue"; import { computed } from "vue"; import { useDark, useToggle } from "@vueuse/core"; const isDark = useDark(); const toggleDark = useToggle(isDark); const btnMsg = computed(() => { return isDark.value ? "深色" : "亮色"; }); </script> <template> <img alt="Vue logo" src="./assets/logo.png" /> <HelloWorld msg="Hello Vue 3 + TypeScript + Vite" /> <button @click="toggleDark()">{{ btnMsg }}</button> </template> <style> /* 这里引入样式 */ @import "./assets/base.css"; #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; margin-top: 60px; } </style>

我们添加了一个button,添加click事件,然后按钮文字根据isDark标志位控制

New Image

可以看到,点击按钮后,在html中添加了dark这个class,这时候我们只需要添加.dark样式就可以了,修改base.css

css
代码解读
复制代码
:root { --dmd-white: #ffffff; ---dmd-dark: #181818; --dmd-text-white: #2c3e50; --dmd-text-dark: rgba(235, 235, 235, 0.64); } :root { --color-background: var(--dmd-white); --color-text: var(--dmd-text-white); } /* 按钮控制样式 */ :root.dark { --color-background: var(---dmd-dark); --color-text: var(--dmd-text-dark); color-scheme: dark; } /* 重点 */ @media (prefers-color-scheme: dark) { :root { --color-background: var(---dmd-dark); --color-text: var(--dmd-text-dark); } }

这样写我们就需要维护两份深色模式样式,分别是系统控制的和用户控制的,会比较麻烦,我们可以结合less对这部分进行一个抽取然后混入,将base.css文件名改成base.less

less
代码解读
复制代码
:root { --color-background: var(--dmd-white); --color-text: var(--dmd-text-white); } .darkThemeMixin { --color-background: var(---dmd-dark); --color-text: var(--dmd-text-dark); } :root.dark { .darkThemeMixin(); color-scheme: dark; } /* 重点 */ @media (prefers-color-scheme: dark) { :root { .darkThemeMixin(); } }

vite对.less文件有天然的支持,所以不需要安装诸如webpack中的less-loader之类的插件,但还是需要安装less的依赖,点击这里看官方说明

bash
代码解读
复制代码
pnpm add less -D

修改App.vue中对样式文件的引入,注意,这里一定要加上lang,不然会报错

html
代码解读
复制代码
<style lang="less"> /* 这里引入样式 */ @import "./assets/base.less"; #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; margin-top: 60px; } </style>

New Image 这样在点击按钮的时候,就能看到深色效果啦

bug发现

当系统是亮色模式的时候,应用点击按钮切换主题是正常的,但当系统是深色模式的时候,点击切换按钮无法切换应用样式,一直是深色模式

思路排查

应该是@media (prefers-color-scheme: dark)这块在系统为深色模式的时候,一直占据着应用样式,所以不论html中是否有.dark,都一直展示深色模式

解决方法

base.less文件中的@media (prefers-color-scheme: dark)这段深色模式样式去掉即可 也就是从

less
代码解读
复制代码
.darkThemeMixin { --color-background: var(---dmd-dark); --color-text: var(--dmd-text-dark); } :root.dark { .darkThemeMixin(); color-scheme: dark; } /* 重点 */ @media (prefers-color-scheme: dark) { :root { .darkThemeMixin(); } }

变成

less
代码解读
复制代码
// .darkThemeMixin { // --color-background: var(---dmd-dark); // --color-text: var(--dmd-text-dark); // } :root.dark { --color-background: var(---dmd-dark); --color-text: var(--dmd-text-dark); color-scheme: dark; } /* 重点 */ // @media (prefers-color-scheme: dark) { // :root { // .darkThemeMixin(); // } // }

这样,在系统深色模式下,也能进行主题切换了

原因分析

我们可以从useDark源码开始入手

ts
代码解读
复制代码
export function useDark(options: UseDarkOptions = {}) { const { valueDark = ''dark'', valueLight = '''', window = defaultWindow, } = options const mode = useColorMode({ ...options, onChanged: (mode, defaultHandler) => { if (options.onChanged) options.onChanged?.(mode === ''dark'') else defaultHandler(mode) }, modes: { dark: valueDark, light: valueLight, }, }) const preferredDark = usePreferredDark({ window }) const isDark = computed<boolean>({ get() { return mode.value === ''dark'' }, set(v) { if (v === preferredDark.value) mode.value = ''auto'' else mode.value = v ? ''dark'' : ''light'' }, }) return isDark }

通读代码,得知useDark这个hook,是通过useColorModeusePreferredDark两个hook实现的

  • 通过useColorModehook控制当前的主题类型,在useDark中我们只需要用到dark主题类型,如果还需要别的主题类型,可以直接使用useColorModehook,然后传入自己需要的主题类型,比如dark, light, coffee, green等等。
    • 切换主题时,useColorMode会将当前主题类型,存放到你指定的标签,默认是根元素html,所以在切换的时候能看到在html标签中多了个.dark
    • useColorModehook也会将当前主题类型,存放到localStorage中,默认键名是vueuse-color-scheme,可以通过storageKey选项修改键名
  • 通过usePreferredDarkhook查询当前系统主题类型,也就是以下伪代码
js
代码解读
复制代码
export function usePreferredDark(options?: ConfigurableWindow) { return window.matchMedia(''(prefers-color-scheme: dark)'', options).matches }

因为useDarkhook已经对系统主题切换做了检测了,所以我们自己再加上@media就重复控制了,就导致系统主题为深色的时候我们无法用按钮控制主题,所以把@media那段控制去掉即可

媒体查询

上面深色模式中,用到了一个媒体查询@media (prefers-color-scheme: dark)来控制深色模式的样式,媒体查询其实还有很多适配的场景,摘抄MDN的介绍

@media CSS @规则 可用于基于一个或多个 媒体查询 的结果来应用样式表的一部分。 使用它,您可以指定一个媒体查询和一个CSS块,当且仅当该媒体查询与正在使用其内容的设备匹配时,该CSS块才能应用于该文档

点击这里可以查看媒体查询支持的场景,我们这次使用@media (min-width)来模拟vue版本工程的响应式布局

正常展示 New Image

缩小展示 New Image

具体步骤

我们先创建一个子组件MediaItem.vue,内容不多,就一个红色块

vue
代码解读
复制代码
<script setup lang="ts"></script> <template> <div ></div> </template> <style lang="less"> .media-item { height: 100px; width: 100%; background-color: red; } </style>

App.vue中使用组件

html
代码解读
复制代码
<script setup lang="ts"> import HelloWorld from "./components/HelloWorld.vue"; import MediaItem from "./components/MediaItem.vue"; import { computed } from "vue"; import { useDark, useToggle } from "@vueuse/core"; const isDark = useDark(); const toggleDark = useToggle(isDark); const btnMsg = computed(() => { return isDark.value ? "深色" : "亮色"; }); </script> <template> <div class="main-wrap"> <img alt="Vue logo" src="./assets/logo.png" /> <HelloWorld msg="Hello Vue 3 + TypeScript + Vite" /> <button @click="toggleDark()">{{ btnMsg }}</button> </div> <MediaItem></MediaItem> </template>

注意,我这里把原来的元素,用.main-wrap包裹了起来,这样在#app下就有两个元素,一个是.main-wrap,一个是MediaItem

New Image

base.less中添加媒体查询控制

css
代码解读
复制代码
@media (min-width: 1024px) { #app { display: flex; } .main-wrap { flex: 1; } .media-item { flex: 1; } }

意思是,在宽度大于1024px时,#app就使用flex布局,并且main-wrapmedia-item均等分布

New Image

当页面宽度小于1024px时,就恢复原来的正常布局流

New Image

完整代码

点击这里可以看完整代码