保姆级Vue3+Vite项目实战黑白模式切换

news2024/11/23 13:14:25

写在前面

注:本文首发掘金签约专栏,此为文章同步!

本文为 Vue3+Vite 项目实战系列教程文章第四篇,系列文章建议从头观看效果更佳,大家可关注专栏防走失!点个赞再看有助于全文完整阅读!

此系列文章主要使用到的主要技术站栈为 Vue3+Vite,那既然是 Vue3,状态库我们使用的是 Pinia 而不是 Vuex,在写法上也肯定是以 CompositionAPI 为主而不是 OptionsAPI,组件库方面我们使用的是 ArcoDesign (赶紧丢掉 ElementUI 吧!)。

  • 这是一份保姆级Vue3+Vite实战教程  - 介绍 Vue3+Vite 项目的搭建以及项目上的一些配置。

  • 保姆级Vue3+Vite项目实战多布局(上) - 介绍多布局实现思路以及默认布局搭建。

  • 保姆级Vue3+Vite项目实战多布局(下) - 介绍边栏布局实现以及动态布局切换。

IT 行业,特别是程序员,经常会加班,虽然加班是不对的,但是大部分人迫于生计还是选择去加班,也就经常在深夜工作,这样的话网站太亮的话会很刺眼,其实我们平常浏览的网站(难道是为了应对社畜🤨)很多都支持切换黑暗模式,那我们做的项目本身就是一个程序员使用较多的工具网站,本着程序员何必为难程序员的原则,还是得加上模式切换,本文的核心就是介绍黑暗模式的实现思路以及在项目中实现一下。

👉🏻 项目 GitHub 地址[1]

如果大家不想从头来过可以直接下载截止到上文内容的代码,👉🏻 toolsdog tag v0.0.2-dev[2]

代码拉下来之后,npm install || pnpm install 下载依赖,然后 npm run serve || pnpm serve 启动,如果一切没问题的话,当前项目运行起来是这样的:

a82c770f937880970f6a0a0b64efd437.png

主题和模式

本文讲的是模式切换,很多人把主题和模式搞混,认为黑白模式就是 2 套主题色,但其实还是不一样的,每一套主题色,一般都有 2 个基础模式,即在白天和晚上的配色方式,也可能会有色弱、色差等模式,但是主题就是主题,模式就是模式,不一样的哈,因为经常有人把黑白两套颜色直接做成2套主题色,所以很多人会搞混。

主题通常会有一系列的配色,这些配色在白天和晚上展示的应该是 2 套配色(比如晚上的背景色应该是暗色而白天是亮色,还有晚上的主色值应该要比白天的偏暗一些),这才是正常的。。。但其实落实到代码中写起来,主题和模式的套路都一致。

很正经的说,如果要区分模式,每个主题色,都应该按照模式分成几套颜色,所以你也可以理解为是几套不同的主题色。

如果把你绕迷了,没关系,不用关心这件事,接着看下文吧!

模式切换思路

一般比较基础的情况都会去做两个模式白色和黑色,因为这两种基础色可以和系统配色模式匹配,本文的重点就是怎么在使用中切换模式,接下来我们简单说一下常用的几种方案。

类名切换

这种方式其实就是在 body 元素上通过不同的 class 去控制不同主题,切换时我们切换 body 元素类名就可以了,如下:

<body class="dark || light">

假如我们有一断代码如下:

<body>
  <button class="button-toggle">点击切换主题模式</button>
  <p>hello world!</p>
  <a href="#">isboyjc</a>
</body>

按照这种方式的思路,接下来我们在不同的主体类中定义不同的样式:

/* 基础(白色)模式样式 light */
body {
  color: #333;
  background: #fff;
}
a {
  color: #666;
}

/* 黑色模式样式 dark */
body.dark {
  color: #eee;
  background: #111;
}
body.dark a {
  color: #ccc;
}

OK,我们使用 JS 来切换一下类名如下:

const btn = document.querySelector(".button-toggle");

btn.addEventListener("click", function () {
  document.body.classList.toggle("dark");
});

如上代码,由于 light 是基础默认色,所以我们直接忽略掉就可以,因为上面 CSS 写的时候就没有写这个类。使用 JS 获取按钮元素之后监听一下点击事件,在用户点击切换按钮时判断 body 元素中有没有 dark 类,有就删除,没有就增加。dark 类存在时,由于 CSS 级联的特性,就会覆盖掉默认的 light 样式,以此来实现主题模式切换。

码上掘金在线预览:https://code.juejin.cn/pen/7167678380098191390[3]

但是大家有没有想过,如果我们把几种主题样式文件都放在一个文件中,使用类名去切换,虽然没问题,但是初始化时,我们必须要加载所有主题的样式,样式非常多的话首次加载其实会很浪费时间从而影响体验。

样式文件切换

我们也可以将各个主题的样式放到单独的文件中,然后使用 link 标签 href 属性引入一个默认的主题样式文件,再使用 JS 去切换 href 属性地址以此来实现主题模式切换,和上面不一样的是,这里我们直接切换整个样式文件而不是类,也就是说初始化时只需要加载一份样式就可以了。

来写个例子:

创建 light.css 文件代表基础白色模式:

body {
  color: #333;
  background: #fff;
}
a {
  color: #666;
}

再创建一个 dark.css 文件代表黑色模式:

body {
  color: #eee;
  background: #111;
}
body a {
  color: #ccc;
}

再创建一个 HTML 文件,我们在 HTML <head> 标签中通过 link href 引入一个默认主题样式文件,如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <!-- 默认样式 -->
  <link href="light.css" rel="stylesheet" id="theme-mode-link">
</head>

<body>
  <button class="button-toggle">点击切换主题模式</button>
  <p>hello world!</p>
  <a href="#">isboyjc</a>
</body>

</html>

上面其实可以看到,我们给 link 标签加上了一个 ID,这是为了我们在做切换时能够获取到这个元素,接下来我们写一下切换的逻辑:

const btn = document.querySelector(".button-toggle");
const theme = document.querySelector("#theme-mode-link");

btn.addEventListener("click", function() {
  if (theme.getAttribute("href") == "light.css") {
    theme.href = "dark.css";
  } else {
    theme.href = "light.css";
  }
});

上面切换的逻辑很简单,就是获取一下触发按钮元素和引入样式文件的 link 元素,监听按钮点击事件,使用 getAttribute API 获取并判断 link 标签中的 href 属性值来区分当前是哪个主题模式并切换即可。

码上掘金在线预览:https://code.juejin.cn/pen/7167685422686928936[4]

直接切换样式文件其实也会有些问题,其实大家应该可以发现,每次切换文件时都要先请求到资源,加载资源文件是需要一点点时间的,如果样式文件过大的话,这个时间给用户带来的体验很不好,点击切换系统主题模式,过了一会才切换成功,哪怕是短暂的空档,也会给用户造成系统操作不够圆滑的印象。

自定义属性切换

自定义属性又叫 CSS 变量,随着浏览器兼容性的不断提升,目前此方案已经是主流。

先来看看怎么用吧,还是那个 HTML

<body>
  <button class="button-toggle">点击切换主题模式</button>
  <p>hello world!</p>
  <a href="#">isboyjc</a>
</body>

接着写 CSS,注意,我们要把需要切换的动态值抽离成 CSS 变量:

/* 基础(白色)模式变量 light */
body {
  --text-color: #333;
  --bkg-color: #fff;
  --anchor-color: #666;
}

/* 黑色模式变量 dark */
body.dark {
  --text-color: #eee;
  --bkg-color: #111;
  --anchor-color: #ccc;
}

/* 样式代码 */
body {
  color: var(--text-color);
  background: var(--bkg-color);
}
a {
  color: var(--anchor-color);
}

如上,我们抽离了 CSS 变量,var() 函数可以代替元素中任何属性中的值的任何部分。该函数不能作为属性名、选择器或其他除了属性值之外的值(这样做通常会产生无效的语法或者一个没有关联到变量的值)。关于 CSS 变量语法如果有不了解的同学,建议百度、谷歌刷下文档哈。

JS 切换主题模式和第一种类名切换一致:

const btn = document.querySelector(".button-toggle");

btn.addEventListener("click", function () {
  document.body.classList.toggle("dark");
});

码上掘金在线预览:https://code.juejin.cn/pen/7167707327213076516[5]

虽然看着和第一种方案差不多,但是如果只把需要切换的值抽离成变量的话,其实由于系统风格统一,这些变量并不会有太多,哪怕是一次加载完所有主题模式的变量,也不会给首次加载带来太大影响,而且后期在配置其他主题色的时候也会大大提高效率,当然如果你的项目非常大并且变量超级多的话,也可以尝试把不同的主题变量抽离成单个文件,并且使用预加载的方式来优化切换加载这块儿的时间。

通常情况下我们会使用 HTML 的自定义属性加上属性选择器而不是使用 calss 来做切换,因为主题模式标识并不与 DOM 元素关联,我们只是通过一个标识去让浏览器选择不同的变量样式,使用 calss 一眼看上去让人觉得它是一个样式,语义会很不明确,当然你要是硬要用 calss 那也不是不可以。

接下来我们使用 HTML 的自定义属性加上属性选择器修改一下上面代码,HTML 方面没有改动,还是原来的样子:

<body>
  <button class="button-toggle">点击切换主题模式</button>
  <p>hello world!</p>
  <a href="#">isboyjc</a>
</body>

JS 切换时,我们使用 getAttribute & setAttribute 方法获取 body 标签的自定义属性 theme 并设置主题模式:

const btn = document.querySelector(".button-toggle");

btn.addEventListener("click", function () {
 if(document.body.getAttribute("theme") === 'dark'){
  document.body.setAttribute('theme', '')
 }else{
  document.body.setAttribute('theme', 'dark')
 }
});

CSS 也许要换成属性选择器语法 body[theme=dark]

/* 基础(白色)模式变量 light */
body {
  --text-color: #333;
  --bkg-color: #fff;
  --anchor-color: #666;
}

/* 黑色模式变量 dark */
body[theme=dark] {
  --text-color: #eee;
  --bkg-color: #111;
  --anchor-color: #ccc;
}

body {
  color: var(--text-color);
  background: var(--bkg-color);
}
a {
  color: var(--anchor-color);
}

关于 CSS 属性选择器,不了解的同学可以看看 菜鸟教程-属性选择器[6] 哈!

当选择 dark 模式时,HTML 如下:

<body theme="dark">
  <button class="button-toggle">点击切换主题模式</button>
  <p>hello world!</p>
  <a href="#">isboyjc</a>
</body>

码上掘金在线预览:https://code.juejin.cn/pen/7167719012283973669[7]

当然我们不一定非要把属性挂在 body 上,挂在 html 上也可以,写 CSS 变量时使用 html{…} 或者 :root{…} 都可以,:root 是一个伪类,表示文档根元素。

思路大概就是这样,当然具体使用那种还是要看需求决定,一般情况下我们会使用第三种,接下来我们项目实践中其实也是基于这种方式。

黑白模式切换实战

之前写项目时我们说使用 ArcoDesignCSS 变量后面我们做黑白模式切换时会很方便想必大家现在理解什么意思了吧,因为如果我们不使用 ArcoDesignCSS 变量的话,想要做模式切换,就得自己写一套 CSS 变量,当然这也不是想写就写的,它需要专业的设计师设计一套合适的主题色,我们一切从简,所以就直接使用 UI 库定义的 CSS 变量了。

而 ArcoDesign 暗黑模式文档[8] 上其实写的很清楚,主题的模式切换我们只需要在 body 元素中设置 自定义属性 arco-theme 即可,如下:

// 设置为暗黑主题
document.body.setAttribute('arco-theme', 'dark')

// 恢复亮色主题
document.body.removeAttribute('arco-theme');

所以,我们不需要考虑 CSS 变量的问题,只需要做切换就 OK 了!除此之外,我们还需要做一个跟随系统的选项,因为目前用户的操作系统中都允许用户直接在系统中设置深色和浅色主题模式,我们可以使用 JS 浏览器 APIJS Bom ) 的 matchedMedia 方法来检测用户的系统配色偏好,以此来展示默认主题模式。

你可以尝试在浏览器控制台输出如下代码:

const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");

if(prefersDarkScheme.matches){
 console.log("dark")
}else{
 console.log("light")
}

OK,接下来我们来写项目。

处理基础数据

和可切换布局一样,还是要先处理数据,我们需要先写一下模式列表,因为后面还要做持久化,也为了这些全局系统配置信息的统一,我们还是写在 pinia system 模块中。

修改 stores/system.js 文件如下:

import { getConfig } from '@/config/index'
import IconMaterialSymbolsWbSunnyRounded from '~icons/material-symbols/wb-sunny-rounded'
import IconMaterialSymbolsDarkModeRounded from '~icons/material-symbols/dark-mode-rounded'
import IconMaterialSymbolsComputer from '~icons/material-symbols/computer'

export const useSystemStore = defineStore(
  'system',
  () => {
    // ...

    // 模式列表
    const modeList = ref([
      {
        name: 'auto',
        icon: markRaw(IconMaterialSymbolsComputer),
        title: '自动模式'
      },
      {
        name: 'light',
        icon: markRaw(IconMaterialSymbolsWbSunnyRounded),
        title: '亮色模式'
      },
      {
        name: 'dark',
        icon: markRaw(IconMaterialSymbolsDarkModeRounded),
        title: '暗色模式'
      }
    ])
    // 当前模式
    const currentMode = ref(null)

    // 初始化模式
    const initMode = () => {
      if (!currentMode.value) {
        currentMode.value = modeList.value[0]
      } else {
        currentMode.value = modeList.value.find(
          item => item.name === currentMode.value.name
        )
      }
    }

    return {
      currentMode,
      modeList,
      initMode,

      // ...
    }
  },
  {
    persist: {
      key: `${getConfig('appCode')}-pinia-system`,
      enabled: true,
      storage: window.localStorage,
   // 新增 currentMode.name 属性持久化
      paths: ['currentSwitchlayout.name', 'currentMode.name']
    }
  }
)

如上,我们在 system 模块中新增了当前模式对象 currentMode 和模式列表数组 modeList,模式列表数组我们直接写死了,除了 lightdark 之外,我们还写了一个 auto,它代表使用操作系统偏好主题模式,同时在 iconify 图标库找了几个对应的图标引入(图标库配置请看第一篇文章),由于直接在 JS 中引入的图标组件,所以还是得手动引入不能自动引入,我们还是使用 markRaw 方法标记一下该组件不被做响应式处理来避免不必要的开销,OK,模式列表就写好了。

我们写了一个初始化模式的方法 initMode,内部逻辑其实和上文布局初始化方法一样,同时我们也给当前模式对象 currentModename 属性做了持久化处理。

最后把这几个主题模式相关的属性和方法 return 出去即可,接下来我们写模式切换组件 SwitchMode 会用到。

模式切换组件 SwitchMode

写下模式切换组件,在 src/layout/components 文件夹下新增 SwitchMode.vue 文件,写入下面内容:

<script setup>
import { useSystemStore } from '@/stores/system.js'
const systemStore = useSystemStore()
const { currentMode, modeList } = storeToRefs(systemStore)

// 初始化模式
systemStore.initMode()

// 下拉菜单选中事件
const handleSelect = val => (currentMode.value = val)

const { next } = useCycleList(modeList.value, {
  initialValue: currentMode
})
</script>

<template>
  <a-dropdown @select="handleSelect" trigger="hover" class="mode-dropdown">
    <a-button type="text" @click="next()">
      <template #icon>
        <component
          :is="currentMode.icon"
          class="text-[var(--color-text-1)] text-16px"
        ></component>
      </template>
    </a-button>
    <template #content>
      <a-doption v-for="item of modeList" :key="item.name" :value="item">
        <template #icon v-if="currentMode.name === item.name">
          <icon-material-symbols-check-small
            class="text-[var(--color-text-1)] text-14px"
          />
        </template>
        <template #default>{{ item.title }}</template>
      </a-doption>
    </template>
  </a-dropdown>
</template>

<style scoped>
.mode-dropdown .arco-dropdown-option {
  @apply flex justify-end items-center;
}
</style>

和切换布局组件 SwitchLayout 很像哈,其实就是一个模式图标,悬浮展示下拉菜单( ArcoDesign 组件库 a-dropdown 组件)点击可选模式,另外点击当前模式图标可以按照顺序切换下一个模式这样子。

VuestoreToRefs 方法以及 VueUseuseCycleList 方法我们上文都有介绍,这里不多做解释了,不了解可以回顾下上文,唯一不同的是模式切换组件中写了模式初始化方法,而之前布局初始化我们实在 SwitchIndex 可切换布局入口文件中写的,这是因为布局未渲染之前不会加载布局切换组件,所以我们必须得在入口处先调用布局初始化方法,而模式初始化不存在这个问题,因为只要布局加载了,就会渲染模式切换组件,在模式切换组件中初始化模式也就没问题。

接下来我们使用一下 SwitchMode 组件,因为配置了自动引入所以无需引入组件直接使用即可,两个布局组件里都需要使用,放在 Navbar 组件右侧插槽中。

修改 DefaultLayout 组件(只展示了修改处代码):

<a-layout-header>
  <Navbar>
    <template #left> <Logo /> </template>
    <template #center> <Menu /> </template>

    <template #right>
   <!-- 新增 -->
   <SwitchMode />
      <SwitchLayout />
      <Github />
    </template>
  </Navbar>
</a-layout-header>

修改 SidebarLayout 组件(只展示了修改处代码):

<a-layout-header>
  <Navbar>
    <template #left> <Logo /> </template>

    <template #right>
   <!-- 新增 -->
   <SwitchMode />
      <SwitchLayout />
      <Github />
    </template>
  </Navbar>
</a-layout-header>

OK,保存刷新下页面:

75ad105939992f9acbf44a2b0fa59a38.png

如上图,我们使用之后导航栏有个模式切换菜单,点击切换即可切换模式,切换的当前模式对象 name 已经缓存到浏览器中。但是点击切换页面并没有什么改变,因为我们还没做切换的逻辑处理,接下来我们在 pinia system 模块中做一下修改,为了方便呢,我们这里借助 VueUseuseColorMode 方法去做切换逻辑,后面会给大家介绍方法,如下:

// ...

export const useSystemStore = defineStore(
  'system',
  () => {
    // ...

    const modeList = ref([
      {
        name: 'auto',
        icon: markRaw(IconMaterialSymbolsComputer),
        title: '自动模式'
      },
      {
        name: 'light',
        icon: markRaw(IconMaterialSymbolsWbSunnyRounded),
        title: '亮色模式'
      },
      {
        name: 'dark',
        icon: markRaw(IconMaterialSymbolsDarkModeRounded),
        title: '暗色模式'
      }
    ])
    const currentMode = ref(null)

  // 新增
    const mode = useColorMode({
      attribute: 'arco-theme',
      emitAuto: true,
      selector: 'body',
      initialValue: currentMode.value?.name,
      storageKey: null
    })
    watchEffect(() => (mode.value = currentMode.value?.name))

    const initMode = () => {
      if (!currentMode.value) {
        currentMode.value = modeList.value[0]
      } else {
        currentMode.value = modeList.value.find(
          item => item.name === currentMode.value.name
        )
      }
    }

    return {
      currentMode,
      modeList,
      initMode,

      // ...
    }
  },
  {
    persist: {
      key: `${getConfig('appCode')}-pinia-system`,
      enabled: true,
      storage: window.localStorage,
      paths: ['currentSwitchlayout.name', 'currentMode.name']
    }
  }
)

其实上面代码中我们只增加了下面这段代码:

const mode = useColorMode({
  attribute: 'arco-theme',
  emitAuto: true,
  selector: 'body',
  initialValue: currentMode.value?.name,
  storageKey: null
})
watchEffect(() => (mode.value = currentMode.value?.name))

我们先来看看 useColorMode[9] 的用法,其实它的大致原理我们上面都说过了,核心就是自定义属性切换那一套,该方法接受一个对象参数,对象有下面几个属性:

  • selector - string 类型,应用于目标元素的 CSS 选择器,作用就是将 HTML 自定义的属性添加到对应元素上。

  • attribute - string 类型,应用目标元素的 HTML 属性,其实就是 HTML 自定义属性的 key

  • initialValue - 初始模式值。

  • modes - 向属性添加值时的前缀。

  • storageRef - 自定义存储引用,如果提供,将跳过 useStorageuseStorageVueUse 中一个做持久化的方法。

  • storageKey - 将数据持久化到 localStorage/sessionStorage 的密钥(key),传 null 则禁用持久化。

  • storage - 存储对象,可以是 localStoragesessionStorage,默认 localStorage

  • emitAuto - 从状态切换为 auto ,选项设置为 true 时,首选模式不会转换为 lightdark,当我们需要知道 auto 状态下的模式值时会很有用。

  • onChanged - 用于处理更新的自定义处理程序,指定后,将覆盖默认行为。

useColorMode 方法内也可以做数据持久化,但是我们将 storageKey 属性设置为 null,统一在 pinia 中做持久化,由于我们使用 ArcoDesignCSS 变量,所以遵循 UI 库的方式将目标元素即 selector 设置为 body,目标元素的自定义属性 attribute 设置为 arco-theme

useColorMode 返回的 mode 默认值为 light、dark,切换模式如下:

const mode = useColorMode({ 
 // ...
})

mode.value = 'light' // 切换基础白色模式
mode.value = 'dark' // 切换黑色模式
mode.value = 'auto' // 跟随系统偏好设置切换模式

默认情况下,当我们把 mode 设置为 auto 时,useColorMode 方法内部会检查系统偏好设置,将项目模式目标元素属性值设置为系统偏好然后将 mode 修改为系统偏好的 dark 或者 light,但当我们将 emitAuto 属性设置为 true 后,当切换为 auto 时,会将项目模式目标元素属性值设置为系统偏好,但是 mode 值还是 auto,这么说大家应该对 emitAuto 属性更容易理解些,也可以自己将 emitAuto 属性分别设置 true/false 然后监听 mode 变化输出看看就知道了。

初始值 initialValue 我们直接设置成当前选中模式对象的 name 属性即可。

我们目前只有黑、白、自动三种模式,这几种也是最普遍的,useColorMode 方法中默认的也是这几种,如果我们想再自定义一个模式(比如色弱模式),我们就需要去配置 modes 属性了,大家可以翻翻文档都尝试下,这里不多赘述了。

那如果我们想在模式改变前做一些事情,可以使用 onChanged 属性,onChanged 属性值是一个回调,回调中有两个参数,第一个参数 mode 指的是要改变的模式标识,第二个参数 defaultOnChanged 是一个方法,调用该方法传入模式标识符即切换模式,如下(只是例子,和项目无关):

const mode = useColorMode({
    attribute: 'arco-theme',
    emitAuto: true,
    selector: 'body',
    onChanged: (mode, defaultOnChanged) => {
   // 再设置一个自定义属性 theme
      document.body.setAttribute('theme', mode)

   // 修改模式
      defaultOnChanged(mode)
    }
  })

目前我们也不需要用到,回到代码逻辑,我们调用 useColorMode 方法返回了一个响应式属性 mode,接着使用 watchEffect (上文有介绍)方法监听了其回调中的响应式属性,也就是当我们的当前模式对象中 name 属性 (currentMode.value?.name) 值发生改变时,会重新赋值给 mode ,而 mode 改变则会触发 useColorMode 方法逻辑以此来切换模式。

OK,看下完整的 pinia system 模块代码:

import { getConfig } from '@/config/index'
import IconMaterialSymbolsWbSunnyRounded from '~icons/material-symbols/wb-sunny-rounded'
import IconMaterialSymbolsDarkModeRounded from '~icons/material-symbols/dark-mode-rounded'
import IconMaterialSymbolsComputer from '~icons/material-symbols/computer'

export const useSystemStore = defineStore(
  'system',
  () => {
    // 当前可切换布局
    const currentSwitchlayout = shallowRef(null)
    // 可切换布局列表
    const switchLayoutList = shallowRef([])

    // 初始化可切换布局方法
    const initSwitchLayout = list => {
      if (list && list.length > 0) {
        switchLayoutList.value = [...list]
        if (!currentSwitchlayout.value) {
          currentSwitchlayout.value = switchLayoutList.value[0]
        } else {
          currentSwitchlayout.value = switchLayoutList.value.find(
            item => item.name === currentSwitchlayout.value.name
          )
        }
      }
    }

    // 模式列表
    const modeList = ref([
      {
        name: 'auto',
        icon: markRaw(IconMaterialSymbolsComputer),
        title: '自动模式'
      },
      {
        name: 'light',
        icon: markRaw(IconMaterialSymbolsWbSunnyRounded),
        title: '亮色模式'
      },
      {
        name: 'dark',
        icon: markRaw(IconMaterialSymbolsDarkModeRounded),
        title: '暗色模式'
      }
    ])
    // 当前模式
    const currentMode = ref(null)
    const mode = useColorMode({
      attribute: 'arco-theme',
      emitAuto: true,
      selector: 'body',
      initialValue: currentMode.value?.name,
      storageKey: null
    })
    watchEffect(() => (mode.value = currentMode.value?.name))

    // 初始化模式
    const initMode = () => {
      if (!currentMode.value) {
        currentMode.value = modeList.value[0]
      } else {
        currentMode.value = modeList.value.find(
          item => item.name === currentMode.value.name
        )
      }
    }

    return {
      currentMode,
      modeList,
      initMode,

      currentSwitchlayout,
      switchLayoutList,
      initSwitchLayout
    }
  },
  {
    persist: {
      key: `${getConfig('appCode')}-pinia-system`,
      enabled: true,
      storage: window.localStorage,
      paths: ['currentSwitchlayout.name', 'currentMode.name']
    }
  }
)

保存刷新页面看看效果吧!

light 模式:

03eb48882517542944e0164d5925ab65.png

我这边系统设置的黑色主题,所以 auto 模式同 dark 模式:

9b1e6e7b0e9aebfd6a576abb29d2c0dc.png

OK,到此我们的模式切换就搞定了!

如上,我们搞了黑白两个模式,其实都是围绕着一个主题色来的,由于我们直接使用了组件库颜色变量,所以这个主题色就是组件库的主题色。如果我们要再写一套主题色做主题色切换,其实和模式切换差不多,但是一套主题色我们需要设计两套颜色来适配黑白模式,这样说大家能够理解主题和模式的区别吗🙄?

其他小 Tips

多用 Ref 少用 Reactive

大家可能也发现了,虽然写的代码还不多,但到目前为止不论是基础数据类型还是复杂数据类型我们都在使用 ref 还没有用过 reactive ,因为实在找不到理由用它,建议大家能使用 ref 还是使用 ref ,在 ref 的源码实现中,如果传入的是对象等复杂类型,其实内部还会使用 reactive 实现响应式,使用 ref 传入一个基础数据类型,返回的是一个 RefImpl 类型对象,而传入复杂类型返回的则是和 reactive 一致都是 Proxy 类型对象。至于为何推荐 ref,原因有下:

ref 创建的数据是显示调用,因为要使用就必须加 .value,可以让我们一眼就能知道它是响应式数据,代码写多了不会和普通 JS 变量混淆,相当于一个响应式数据的类型检查,所以不要觉得这是个缺点,相反它是优点,代码写多了就知道多香了。

相比 reactive 局限更少,因为 reactive 创建的对象使用 ES6 解构会使响应性丢失。

const obj = reactive({ 
 aaa: { a1: 1, a2: 2 }, 
 bbb: 'bbb' 
})

let { aaa, bbb } = obj
aaa = { a1: 11111 }
bbb = 'bbbbbb'

console.log(obj) // { aaa: { a1: 1, a2: 2 }, bbb: 'bbb'  }

reactive 类型上和对象没有区别,不容易观察是否为响应式对象,包括修改响应式对象中属性值时都和普通对象一致,再一个大型项目里,时刻要观察一个对象是响应式对象还是普通对象很痛苦。

const obj1 = reactive({ 
 aaa: { a1: 1, a2: 2 }, 
 bbb: 'bbb' 
})

const obj2 = { 
 aaa: { a1: 1, a2: 2 }, 
 bbb: 'bbb' 
}

// obj1.aaa 
// obj2.aaa

在使用 watch 监听响应式数据时 reactive 创建的数据需要有箭头函数包裹,而 ref 则不需要。

const counter = ref(0)

watch(counter, (count) => {  
 console.log(count) // == count.value
})

更好的开发体验

开发时,比如我们定义了一个 ref

const count = ref(0)

console.log(count) // RefImpl {_rawValue: 0, _shallow: false, _v_isRef: true, _value: 0}
console.log(count.value) // 0

可以看到,直接输出 count 控制台的打印是 Vue 内部处理之后的对象,这个对象塞进去了很多开发时用不到的数据,我们必须加上 .value ,才会像普通 JS 一样打印值,很不直观。

如果你想让它的输出信息直观,Vue3 源码中有一个名为 initCustomFormatter 的函数,用来在开发环境下初始化自定义 formatter

开发时打开 Chrome DevrTools ,勾选 Console -> Enable custom formatters 选项,如下图:

77de969203c17bf4e1ca243544f18e0b.png

接下来,我们再打印上面的 count ,就会变得非常直观了,如下:

console.log(count) // Ref<0>

赶紧自己设置设置尝试下吧!!!

写在最后

OK,就到这,下文开始写项目中的功能模块了!

截止本文的代码已经打了 Tag 发布,可下载查看:

👉🏻 toolsdog tag v0.0.3-dev[10]

👉🏻 项目 GitHub 地址[11]

谢阅,如有错误请评论纠正,有什么疑问或者不理解的地方都可以私信咨询我,由于不经常写实战文章,也为了不同程度同学都可以看下去,文章可能稍微有些啰嗦,见谅,再次欢迎关注专栏!

如果您觉得文章不错,记得点赞,欢迎关注公众号👇

Reference

[1]

项目 GitHub 地址: https://github.com/isboyjc/toolsdog

[2]

toolsdog tag v0.0.2-dev: https://github.com/isboyjc/toolsdog/releases/tag/v0.0.2-dev

[3]

https://code.juejin.cn/pen/7167678380098191390: https://code.juejin.cn/pen/7167678380098191390

[4]

https://code.juejin.cn/pen/7167685422686928936: https://code.juejin.cn/pen/7167685422686928936

[5]

https://code.juejin.cn/pen/7167707327213076516: https://code.juejin.cn/pen/7167707327213076516

[6]

菜鸟教程-属性选择器: https://www.runoob.com/css/css-attribute-selectors.html

[7]

https://code.juejin.cn/pen/7167719012283973669: https://code.juejin.cn/pen/7167719012283973669

[8]

ArcoDesign 暗黑模式文档: https://arco.design/vue/docs/dark

[9]

useColorMode: https://vueuse.org/core/usecolormode/#usecolormode

[10]

toolsdog tag v0.0.3-dev: https://github.com/isboyjc/toolsdog/releases/tag/v0.0.3-dev

[11]

项目 GitHub 地址: https://github.com/isboyjc/toolsdog

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/346616.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C/C++【内存管理】

✨个人主页&#xff1a; Yohifo &#x1f389;所属专栏&#xff1a; C修行之路 &#x1f38a;每篇一句&#xff1a; 图片来源 Love is a choice. It is a conscious commitment. It is something you choose to make work every day with a person who has chosen the same thi…

商城系统必备营销工具(五)——积分商城

做商城&#xff0c;流量必不可少&#xff0c;日活跃度也很重要。现在各大APP、网站、小程序和微商城&#xff0c;基本都在为了巩固流量做积分商城&#xff0c;虽然已经随处可见&#xff0c;但很多企业商家却并没有将积分商城运作起来&#xff0c;积分商城也没有人浏览兑换商品。…

跟ChatGPT,聊聊ChatGPT

不仅“上知天文、下知地理”&#xff0c;似乎还能对答如流、出口成诗&#xff0c;甚至还能写剧本、编音乐、写代码——最近&#xff0c;一款名叫ChatGPT的人工智能聊天机器人火爆全球。由此&#xff0c;一系列关于新一代技术变革、人工智能替代人力、巨头企业扎堆入局AI的讨论在…

Multi Paxos

basic paxos 是用于确定且只能确定一个值&#xff0c;“只确定一个值有什么用&#xff1f;这可解决不了我面临的问题,例如每个用户都要多次保存数据.” 你心中可能有这样的疑问。 原simple paxos论文里有提到一连串个instance of paxos [4] 但没有提出 multi paxos的概念&…

ChatGPT国内镜像站试用,聊天、Python代码生成。

ChatGPT国内镜像站试用&#xff0c;聊天、Python代码生成。 (本文获得CSDN质量评分【91】)【学习的细节是欢悦的历程】Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”教程《 python 完全自学教程》&#xff0c;不仅仅是基础那么简单…… …

前端开发:关于diff算法详解

前言 前端开发中&#xff0c;关于JS原生的内容和前端算法相关的内容一直都是前端工作中的核心&#xff0c;不管是在实际的前端业务开发还是前端求职面试&#xff0c;都是非常重要且必备的内容。那么本篇博文来分享一个关于前端开发中必备内容&#xff1a;diff算法&#xff0c;d…

ChatGPT背后的技术可以给数据治理带来哪些神奇的效果?_光点科技

最近&#xff0c;由美国人工智能研究室OpenAI开发的全新“聊天机器人”ChatGPT火了。作为一款人工智能语言模型&#xff0c;它不仅能和人展开互动&#xff0c;还可以写文章、制定方案、创作诗歌&#xff0c;甚至编写代码、检查漏洞样样精通&#xff0c;上线仅两个月全球活跃用户…

Python雪花代码

前言 用python画个雪花玩玩&#xff0c;源码在文末公众号哈。 雪花类 class Snow(): #雪花类 def __init__(self): self.r 6 #雪花的半径 self.x ra.randint(-1000,1000) #雪花的横坐标 self.y ra.randint(-500,5…

剑指 Offer 10- I. 斐波那契数列[c语言]

目录题目思路代码结果该文章只是用于记录考研复试刷题题目 力扣斐波那契数列 写一个函数&#xff0c;输入 n &#xff0c;求斐波那契&#xff08;Fibonacci&#xff09;数列的第 n 项&#xff08;即 F(N)&#xff09;。斐波那契数列的定义如下&#xff1a; F(0) 0, F(1) 1 …

卷积神经网络-D2L

从全连接层到卷积 企业级理解卷积 不稳定输入 稳定输出 求系统存量 - 信号系统周围像素点如何产生影响 - 图像处理一个像素点如何试探 - 图像识别 好处&#xff1a; 平移不变性和局部性 图像卷积 import torch from torch import nn from d2l import torch as d2ldef try_…

Java高频面试题,ReentrantLock 是如何实现锁公平和非公平性的?

我先解释一下个公平和非公平的概念。 公平&#xff0c;指的是竞争锁资源的线程&#xff0c;严格按照请求顺序来分配锁。 非公平&#xff0c;表示竞争锁资源的线程&#xff0c;允许插队来抢占锁资源。 ReentrantLock 默认采用了非公平锁的策略来实现锁的竞争逻辑。 其次&…

SqlServer的LDF文件丢失, 如何仅用MDF文件恢复数据库呢?(已解决)

笔者的一个大小为2 TB的SQL Server的database的LDF文件在玩存储盘映射的过程中莫名其妙的丢失了. 好在MDF文件还在. 笔者慌了, Bruce Ye告诉笔者, 不用着急, 光用MDF也可以把数据库弄回来的. 笔者就问Bruce, 假设我可以容忍LDF中信息的丢失的话, 那么该如何恢复这个数据库呢?我…

快速搭建个人在线书库,随时随地畅享阅读!

前边我们利用NAS部署了个人的导航页、小说站、云笔记&#xff0c;今天&#xff0c;我们再看看怎么部署一个个人的在线书库。 相信很多朋友都在自己的电脑中收藏了大量的PDF、MOBI等格式的电子书籍&#xff0c;但是一旦换了一台设备&#xff0c;要么是无法翻阅&#xff0c;要么…

如何为报表开发工具 FastReport .NET 设置 Apache 2 Web 服务器?

FastReport .NET是一款全功能的Windows Forms、ASP.NET和MVC报表分析解决方案&#xff0c;使用FastReport .NET可以创建独立于应用程序的.NET报表&#xff0c;同时FastReport .Net支持中文、英语等14种语言&#xff0c;可以让你的产品保证真正的国际性。专业版和企业版包括Fast…

FortiTalk | “三英论安全”之OT安全热门话题解读

OT安全热门话题解读 在数字化转型时代&#xff0c;OT/IT融合已经成为主旋律&#xff0c;可能很多人还没有意识到“工厂”已经不是以前的“工厂”。从封闭走向互联、从现场走向远程、从手动走向自动&#xff0c;这种变革带来的不仅是便捷和效率&#xff0c;更潜藏着巨大的网络安…

【数据结构】基础:图的最短路径问题(附C++源码)

【数据结构】基础&#xff1a;图的最短路径问题&#xff08;附C源码&#xff09; 摘要&#xff1a;将会在数据结构专题中开展关于图论的内容介绍&#xff0c;其中包括四部分&#xff0c;分别为图的概念与实现、图的遍历、图的最小生成树以及图的最短路径问题。本文介绍图的最短…

LeetCode 105. 从前序与中序遍历序列构造二叉树 -- 数据结构基础

从前序与中序遍历序列构造二叉树 中等 1.9K 相关企业 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树并返回其根节点。 示例 1: 输入: preorder [3,9,20,15,7], i…

基于MATLAB的MIMO信道估计(附完整代码与分析)

目录 一. 介绍 二. MATLAB代码 三. 运行结果与分析 一. 介绍 本篇将在MATLAB的仿真环境中对比MIMO几种常见的信道估计方法的性能。 有关MIMO的介绍可看转至此篇博客&#xff1a; MIMO系统模型构建_唠嗑&#xff01;的博客-CSDN博客 在所有无线通信中&#xff0c;信号通过…

05- 线性回归算法 (LinearRegression) (算法)

线性回归算法(LinearRegression)就是假定一个数据集合预测值与实际值存在一定的误差, 然后假定所有的这些误差值符合正太分布, 通过方程求这个正太分布的最小均值和方差来还原原数据集合的斜率和截距。当误差值无限接近于0时, 预测值与实际值一致, 就变成了求误差的极小值。 fr…

【Calcite源码学习】ImmutableBitSet介绍

Calcite中实现了一个ImmutableBitSet类&#xff0c;用于保存bit集合。在很多优化规则和物化视图相关的类中都使用了ImmutableBitSet来保存group by字段或者聚合函数参数字段对应的index&#xff0c;例如&#xff1a; //MaterializedViewAggregateRule#compensateViewPartial()…