Vue3自定义一个Hooks,实现一键换肤

发布时间 2024-01-04 11:55:24作者: 柯基与佩奇

核心

  • 使用 CSS 变量, 准备两套 CSS 颜色, 一套是在 light 模式下的颜色,一套是在 dark 模式下的颜色
  • dark 模式下的 CSS 权重要比 light 模式下的权重高, 不然当我们给 html 添加自定义属性[data-theme='dark']的时候, dark 模式权重比 light 低,会一直不起效果
  • 当我们点击 dark 模式的时候, 给 html 设置自定义属性[data-theme='dark']
  • 当我们点击 light 模式的时候, 给 html 设置自定义属性[data-theme='light']
  • 在 dark 模式下, 会匹配到html[data-theme='dark']选择器下的样式
  • 在 light 模式下,由于我们没有设置html[data-theme='light']的方案, 那么他就匹配:root(即 html)下的样式

两套样式代码大概如下(列了一部分):

:root {
  --color-body-bg: #ffffff;
  --color-text: #000;
  --color-secondary-bg-for-transparent: rgba(209, 209, 214, 0.28);
}

html[data-theme="dark"] {
  --color-body-bg: #222222;
  --color-text: #ffffff;
  --color-primary-bg-for-transparent: rgba(255, 255, 255, 0.12);
}

然后我们点击的时候,通过

let theme = "light"; // light / dark
document.documentElement.setAttribute("data-theme", theme);

这样就能实现简单的更换肤色功能了

什么? 你以为这就完了?好戏刚开始

跟随系统颜色

首先利用Window  的  'matchMedia()'  方法返回一个新的  MediaQueryList  对象,表示指定的媒体查询  (en-US)字符串解析后的结果。

如运行媒体查询(max-width: 600px)并在<span>;中显示MediaQueryListmatches属性值。如果视口的宽度小于或等于 600 像素,则输出将为 true,而如果窗口的宽度大于此宽度,则将输出 false。

<span class="mq-value"></span>
let mql = window.matchMedia("(max-width: 600px)");
document.querySelector(".mq-value").innerText = mql.matches; //此时小于或等于600像素时span 里面的结果为false

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

.day {
  background: #eee;
  color: black;
}
.night {
  background: #333;
  color: white;
}

@media (prefers-color-scheme: dark) {
  .day.dark-scheme {
    background: #333;
    color: white;
  }
  .night.dark-scheme {
    background: black;
    color: #ddd;
  }
}

@media (prefers-color-scheme: light) {
  .day.light-scheme {
    background: white;
    color: #555;
  }
  .night.light-scheme {
    background: #eee;
    color: black;
  }
}

两者相结合

matchMedia()prefers-color-scheme  结合在一起, 我们就可以通过 js 去给系统颜色为 dark 或 light 的情况下更换对应的 html 自定义属性, 即[data-theme='dark'][data-theme='light']

首先,我们先去获取主题颜色, 我们还没设置的时候,就默认是系统颜色, 设置了就把他存储起来,下次直接获取这个颜色

// 获取主题变量
let appearance = ref < string > (localStorage.getItem("appearance") || "auto");

// 查询当前系统主题颜色
const match = window.matchMedia("(prefers-color-scheme: dark)");

// 如果主题变量为 auto, 则跟随系统主题
if (appearance.value === "auto") {
  followSystem();
} else {
  document.documentElement.setAttribute("data-theme", appearance.value);
}

function followSystem() {
  // 当前系统颜色是亮色还是暗色 , 设置对应的html[data-theme= 'dark' 或者'light']
  const theme = match.matches ? "dark" : "light";
  document.documentElement.setAttribute("data-theme", theme);
}

// 监听系统主题变化,电脑主题发生改变的时候就调用followSystem函数
match.addEventListener("change", followSystem);

封装成一个 hooks

暴露出一个 useThemeColor 函数, 返回一个对象, 对象里面返回我们的主题变量

// 获取主题变量
let appearance = ref < string > (localStorage.getItem("appearance") || "auto");
// 查询当前系统主题颜色
const match: MediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
// 监听系统主题变化
match.addEventListener("change", followSystem);

function followSystem() {
  const theme = match.matches ? "dark" : "light";
  document.documentElement.setAttribute("data-theme", theme);
}

watchEffect(() => {
  // 如果主题变量为 auto, 则跟随系统主题
  if (appearance.value === "auto") {
    followSystem();
  } else {
    document.documentElement.setAttribute("data-theme", appearance.value);
  }
});

export default function useThemeColor() {
  return {
    appearance,
  };
}

使用 hooks

导入我们 export 出来的函数

import useThemeColor from "../hooks/useThemeColor";

使用函数,注意, 这里返回的 apprance 已经是一个响应式数据

const { appearance } = useThemeColor();

使用 v-model 绑定 apprance,直接使用 apprance , 当我们切换颜色的时候, 就会调用 watchEffect 里面的函数, 达到一键换肤效果

<div class="item">
  <div class="left">
    <div class="title">外观</div>
  </div>
  <div class="right">
    <select v-model="appearance">
      <option value="auto">{{ "自动" }}</option>
      <option value="light">? {{ "浅色" }}</option>
      <option value="dark">? {{ "深色" }}</option>
    </select>
  </div>
</div>