css变量+vue3实现跟随浏览器主题和用户设置切换网页主题

前言

目前的公司只有我一个应届前端,学习起来很慢,所以打算写一些文章来记录一下,通过记录的过程增强一下自己的理解以及方便后期的总结复盘。如果文章哪里写的有问题希望看到的大佬能指点一下。

一. css变量和:root选择器

:root 这个 CSS 伪类匹配文档树的根元素,也就是html元素,但是他的优先级比html选择器更高,在:root下可以声明全局css变量,在每个css样式中都可以使用,。例如:

css 复制代码
:root {
  --main-color: hotpink;
  --pane-padding: 5px 42px;
}

我们这里定义了两个css变量,定义css变量时需要在变量名前添加--,值可以是任何css属性值。在使用时配合var()来使用,例如padding:var(--pane-padding);

二. 设置主题样式

我们可以使用属性选择器来实现明亮和黑暗两套主题的切换,使用时只需要给html元素添加"data-theme"属性来表示当前主题的值。

css 复制代码
:root[data-theme="light"] {
  --backgroundcolor: pink;
  --fontcolor: #726b6b;
}
:root[data-theme="dark"] {
  --my-backgroundcolor: black;
  --my-fontcolor: #fff;
}

可以将通用的样式放在:root选择器下

css 复制代码
:root {
  --my-fontsize: 20px;
}

在vue项目中,创建一个theme.css文件,将上面的css规则放到这个文件中,在main.js中引入css文件

三. 切换主题的javascript代码

在vue项目中创建一个theme.js文件存放我们的js代码

1.matchMedia() 方法

来看一下MDN文档的描述:WindowmatchMedia() 方法返回一个新的 MediaQueryList 对象,表示指定的媒体查询 (en-US)字符串解析后的结果。返回的 MediaQueryList 可被用于判定 Document 是否匹配媒体查询,或者监控一个 document 来判定它匹配了或者停止匹配了此媒体查询。

要实现跟随浏览器当前主题进行主题的变化,我们需要利用这个方法来查询浏览器当前主题,通过查询'(prefers-color-scheme: light)'这个媒体查询字符串来查询当前主题是否是light主题。
let matchMedia = window.matchMedia('(prefers-color-scheme: light)')

返回的matchMedia对象有一个matches属性,值是bool类型,表示查询的匹配结果,同时matchMedia对象可以用addEventListener和removeListener方法来监听和移除媒体查询结果的变化。

基于此我们可以封装一个查询浏览器当前主题的函数

js 复制代码
function queryBroswerTheme() {
  let matchMedia = window.matchMedia('(prefers-color-scheme: light)')//isLight
  return matchMedia.matches ? "light" : "dark"
}

2.跟随浏览器选择主题

首先创建一个查询浏览器主题的媒体查询对象,同时创建一个监听和移除监听的函数来实现与用户浏览器主题的同步改变,在回调函数中判断当前主题来进行切换

js 复制代码
let matchMedia = window.matchMedia('(prefers-color-scheme: light)')
//停止监听
function stopListenBroswerTheme() {
  console.log("停止监听浏览器主题变换");
  matchMedia.removeEventListener("change", themeChange)
}
//开始监听
function beginListenBroswerTheme() {
  console.log("开始监听浏览器主题变换");
  matchMedia.addEventListener("change", themeChange)
}
//监听回调函数
function themeChange(e) {
  if (e.matches) {
    document.documentElement.setAttribute("data-theme", "light")
  } else {
    document.documentElement.setAttribute("data-theme", "dark")
  }
}

这时候我们可以创建一个函数,来实现跟随浏览器主题的切换

js 复制代码
function setWithBroswer() {
  if (matchMedia.matches) {
    document.documentElement.setAttribute("data-theme", "light")
  } else {
    document.documentElement.setAttribute("data-theme", "dark")
  }
  beginListenBroswerTheme()
}

3. 根据用户偏好选择主题

如果用户没有偏好的主题我们让页面的主题跟随浏览器变化,如果用户选择一种自己偏好的主题,那我们需要存储用户的偏好,在下次打开页面时根据偏好来进行主题的显示。

所以我们需要使用localStorage来持久化存储一个字段保存用户的偏好,字段名为"preferTheme",我这里采取的方案是:

  1. 如果用户首次进入,我们根据浏览器主题来显示,"preferTheme"的值为"auto"
  2. 如果用户进行过主题选择,那么"preferTheme"的值为"light"或者"dark"
  3. 如果用户选择的主题和浏览器当前主题相同,"preferTheme"的值也为"auto"

封装俩个存取localStorage的函数

js 复制代码
//设置loaclstorage
export function setPreferTheme(val) {
  localStorage.setItem("preferTheme", JSON.stringify(val))
}
//读取loaclstorage
export function getPreferTheme() {
  const res = JSON.parse(localStorage.getItem("preferTheme"))
  if (!res) return false
  return res
}

4. 初始化主题

我们可以写一个完整的初始化页面主题的函数,在初始化主题函数中做判断,如果不存在"preferTheme"字段,说明用户是首次访问网页,将"preferTheme"值设为"auto",跟随浏览器主题切换,如果存在"preferTheme"字段,那么直接设置主题。

js 复制代码
export function useInitTheme() {
  if (!getPreferTheme()) {
    setWithBroswer()
    setPreferTheme("auto")
  } else {
    const preferTheme = getPreferTheme()
    if (preferTheme == "auto") {
      setWithBroswer()
    } else {
      document.documentElement.setAttribute("data-theme", preferTheme)
    }
  }
}

5. 开关切换处理

新建一个vue文件,在这里编译页面以及业务代码,这里我写了一个简单的页面,进行主题切换。开关直接使用了element-plus组件库中的开关,给按钮绑定v-model="theme",并将按钮激活的和关闭的值设为"dark"和"light"

html 复制代码
<template>
  <div class="common-layout">
    <el-container>
      <el-header>
        <div class="header">
          <span>这是一个标题</span>
          <el-switch v-model="theme" class="switch" active-value="dark" inactive-value="light" />
        </div>
      </el-header>
      <el-container>
        <el-aside width="200px" class="aside">这是侧边栏</el-aside>
        <el-main class="main"><el-button @click="show">显示弹窗</el-button></el-main>
      </el-container>
    </el-container>
  </div>
</template>
<style scoped lang="less">
.common-layout {
  .header {
    position: relative;
    font-size: var(--my-fontsize);
    background-color: var(--my-backgroundcolor);
    color: var(--my-fontcolor);
    .switch {
      position: absolute;
      right: 0;
      top: 50%;
      transform: translateY(-50%);
      --el-switch-on-color: #2f2f2f;
    }
  }
  .aside {
    font-size: var(--my-fontsize);
    background-color: var(--my-backgroundcolor);
    color: var(--my-fontcolor);
  }
}
</style>

具体的业务代码也写在theme.js中,最后通过import导入到所需要的vue组件中使用即可。

js 复制代码
import { ref, watch } from "vue"
let theme=null //为什么将theme定义在全局,一会会解释的
export function handleSwitch() {
  //初始化按钮
  theme = ref("light")
  const broswerTheme = queryBroswerTheme()
  const preferTheme = getPreferTheme()
  if (preferTheme === "auto") {
     theme.value = broswerTheme
   }
  else if (preferTheme === "dark") {
     theme.value = "dark"
  }
  //监听变化
  //xxxxxxxxxxxxx
  return theme
}

在这个函数中我们要初始化theme变量,根据存入localStorage的"preferTheme"值来初始化theme的值

还需要用watch来监听开关的切换,当按钮切换时修改主题和"preferTheme"的值,这里关键是如何让设置"preferTheme"的值,刚才其实已经说过了,如果用户选择主题和浏览器当前主题相同,"preferTheme"的值为"auto",不同则为"light"或者"dark"

js 复制代码
watch(theme, (newval, oldval) => {
    document.documentElement.setAttribute("data-theme", newval)
    newval == queryBroswerTheme() ? setPreferTheme("auto") : setPreferTheme(newval)
})

别忘了,如果"preferTheme"的值是"auto",需要监听浏览器主题的变化,如果不是"auto",需要移除监听, 所以我们需要修改一下watch函数

js 复制代码
//开关改变时
watch(theme, (newval, oldval) => {
  document.documentElement.setAttribute("data-theme", newval)
  newval == queryBroswerTheme() ? isAuto() : notAuto(newval)
})
//是auto自动增加监听器
function isAuto() {
  setPreferTheme("auto")
  beginListenBroswerTheme()
}
//不是auto自动取消监听器
function notAuto(val) {
  setPreferTheme(val)
  stopListenBroswerTheme()
}

现在解释一下theme为什么要定义在全局,因为在监听浏览器主题和移除监听的回调函数中我们还需要修改开关的状态,也就是theme的值,所以我们还需要修改一下回调函数

js 复制代码
function themeChange(e) {
  if (e.matches) {
    document.documentElement.setAttribute("data-theme", "light")
    theme.value = "light"
  } else {
    document.documentElement.setAttribute("data-theme", "dark")
    theme.value = "dark"
  }
}

6.完结

到此所有的代码都写完了,将useInitTheme函数导入到main.js文件中调用,将handleSwitch函数导入到vue组件中使用即可。

js 复制代码
import { useInitTheme } from "@/utils/theme.js"
useInitTheme()
js 复制代码
import { handleSwitch } from "@/utils/theme.js"
let theme = handleSwitch()
相关推荐
恋猫de小郭30 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端