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

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

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

写在前面

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

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

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

👉🏻 项目 GitHub 地址

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

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

主题和模式

本文讲的是模式切换,很多人把主题和模式搞混,认为黑白模式就是 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 样式,以此来实现主题模式切换。

码上掘金在线预览:

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

样式文件切换

我们也可以将各个主题的样式放到单独的文件中,然后使用 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 属性值来区分当前是哪个主题模式并切换即可。

码上掘金在线预览:

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

自定义属性切换

自定义属性又叫 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");
});
复制代码

码上掘金在线预览:

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

通常情况下我们会使用 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 属性选择器,不了解的同学可以看看 菜鸟教程-属性选择器 哈!

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

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

码上掘金在线预览:

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

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

黑白模式切换实战

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

ArcoDesign 暗黑模式文档 上其实写的很清楚,主题的模式切换我们只需要在 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,保存刷新下页面:

如上图,我们使用之后导航栏有个模式切换菜单,点击切换即可切换模式,切换的当前模式对象 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 的用法,其实它的大致原理我们上面都说过了,核心就是自定义属性切换那一套,该方法接受一个对象参数,对象有下面几个属性:

  • 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 模式:

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

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 选项,如下图:

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

console.log(count) // Ref<0>
复制代码

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

写在最后

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

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

👉🏻 toolsdog tag v0.0.3-dev

👉🏻 项目 GitHub 地址

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

分类:
前端
收藏成功!
已添加到「」, 点击更改