「本文已参与低调务实优秀中国好青年前端社群的写作活动」
背景
偶然的一天,我想做一个vue3的工程,图方便(我以前都是创建空目录然后一个个加内容的),使用了pnpm快速创建vue工程,使用以下命令
pnpm create vue
经过一系列选择后工程就创建好了,执行pnpm install和pnpm dev即可开启工程
大概通读过新工程代码后,发现这个工程内置了几个比较有意思的点,比如媒体查询
正常展示
缩小展示
比如深色模式
白天模式
深色模式
那么,我们就来深扒一下,vue是怎么完成这些功能的,并且我们用一个空工程来自己实现这些功能
创建空工程
因为vite提供的模板会比较简洁,我们使用vite的模板来开始我们的功能搭建,使用以下命令创建工程
pnpm create vite
两个工程对比
这是vue模板的目录
这是vite模板的目录,由此可见vite模板会更简洁,侵入性会更低
执行pnpm install和pnpm dev后,就能运行vite工程了
深色模式
我们先从深色模式开始入手
额外知识
- win11开启深色模式的方式:在桌面右键,选择
个性化,个性化里选择颜色,颜色里有个选择模式,点开选择深色,即可打开深色模式 - win10操作方法类似,也是
个性化-颜色-选择模式 - mac我没有你们可以试下(羡慕的眼光
先来看调成深色模式后两个系统界面的区别,以下我会对两个基础工程分别称为vite/vue
- vite版本
- vue版本
可以看到,在深色模式下,vue版本的界面是有处理的,而vite版本的没有
样式文件
vue版本的样式文件放在src/assets/bass.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 - 添加需要的背景颜色
: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中引入样式文件
<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>
这样背景的深色模式就做好啦!
通过更改系统颜色,背景色也会跟着变(这里就不贴图了)
但这样还是不够完善的,我们需要对字体也进行深色模式适配
: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了,这样能在一个文件就管理
<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>
这样,根据系统色进行模式切换已经完成了
核心内容
注意看base.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
<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>
下面是官方介绍中提供的基本用法
import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark()
const toggleDark = useToggle(isDark)
使用vueuse
下面我们来改造页面,在App.vue中加个按钮进行主题切换
<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标志位控制
可以看到,点击按钮后,在html中添加了dark这个class,这时候我们只需要添加.dark样式就可以了,修改base.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
: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的依赖,点击这里看官方说明
pnpm add less -D
修改App.vue中对样式文件的引入,注意,这里一定要加上lang,不然会报错
<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>
这样在点击按钮的时候,就能看到深色效果啦
bug发现
当系统是亮色模式的时候,应用点击按钮切换主题是正常的,但当系统是深色模式的时候,点击切换按钮无法切换应用样式,一直是深色模式
思路排查
应该是@media (prefers-color-scheme: dark)这块在系统为深色模式的时候,一直占据着应用样式,所以不论html中是否有.dark,都一直展示深色模式
解决方法
把base.less文件中的@media (prefers-color-scheme: dark)这段深色模式样式去掉即可
也就是从
.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();
}
}
变成
// .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源码开始入手
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,是通过useColorMode和usePreferredDark两个hook实现的
- 通过
useColorModehook控制当前的主题类型,在useDark中我们只需要用到dark主题类型,如果还需要别的主题类型,可以直接使用useColorModehook,然后传入自己需要的主题类型,比如dark,light,coffee,green等等。- 切换主题时,
useColorMode会将当前主题类型,存放到你指定的标签,默认是根元素html,所以在切换的时候能看到在html标签中多了个.dark。 useColorModehook也会将当前主题类型,存放到localStorage中,默认键名是vueuse-color-scheme,可以通过storageKey选项修改键名
- 切换主题时,
- 通过
usePreferredDarkhook查询当前系统主题类型,也就是以下伪代码
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版本工程的响应式布局
正常展示
缩小展示
具体步骤
我们先创建一个子组件MediaItem.vue,内容不多,就一个红色块
<script setup lang="ts"></script>
<template>
<div class="media-item"></div>
</template>
<style lang="less">
.media-item {
height: 100px;
width: 100%;
background-color: red;
}
</style>
在App.vue中使用组件
<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
在base.less中添加媒体查询控制
@media (min-width: 1024px) {
#app {
display: flex;
}
.main-wrap {
flex: 1;
}
.media-item {
flex: 1;
}
}
意思是,在宽度大于1024px时,#app就使用flex布局,并且main-wrap和media-item均等分布
当页面宽度小于1024px时,就恢复原来的正常布局流