likes
comments
collection
share

实用的VUE系列——这次用vueuse 学到了两个有意思的干货!!!声明:本文为稀土掘金技术社区首发签约文章,30天内禁

作者站长头像
站长
· 阅读数 22

前言

vueuse 是近几年非常火的一个工具库,确切的说,他是一款基于Vue组合式API的函数工具集

当然,你也可以叫他vue-hooks 工具集,并且他还人性化的支持了vue2

于是他顺利的勾起了我的好奇心,决心研究一番

让我主要好奇的有两个问题,

  • 1、他到底怎么支持vue2又支持vue3的
  • 2、他到底是怎样实现暗黑模式的

接下来我们一个个来研究

1、他到底怎么支持vue2同时又支持vue3的

这个问题,很有意思,在最开始研究源码的时候,我以为他是写了两套,在不同的项目中导入不同的vue源码

然而细究之后,我发现,有趣,真的是有趣,他用了一个更有意思的库vue-demi

vue-demi

Vue Demi是一款开发工具。允许你为 Vue 2 和 3 编写通用 Vue 库。而无需担心用户安装的版本。

于是我又好奇的研究了一下 vue-demi

vue-demi的实现更有趣,他的核心原理是利用用户安装的版本,在项目install 的时时候始化下载对应的vue版本导出,于是当我们引用 vue-demi 的时候,其实就是引用我们当前项目中的vueapi, 他只是很巧妙的做了一个承上启下

我简单的画个图,来帮助大家更快的理解

实用的VUE系列——这次用vueuse 学到了两个有意思的干货!!!声明:本文为稀土掘金技术社区首发签约文章,30天内禁

上图中,我们可以发现,他的本质,就是动态的更改package.json的入口文件内容来解决通用的问题

不信?,我们可以在源码中来验证

在源码中的package.json中发现脚本 执行了postinstall 函数

  "scripts": {
    "postinstall": "node -e \"try{require('./scripts/postinstall.js')}catch(e){}\"",
  },

在当前函数中拿到vue的版本开始更改package.json中的入口文件内容

const { switchVersion, loadModule } = require('./utils')
// 拿到node_modules中的vue内容
const Vue = loadModule('vue')

if (!Vue || typeof Vue.version !== 'string') {
  console.warn('[vue-demi] Vue is not found. Please run "npm install vue" to install.')
}
// 不同版本执行不同的逻辑
else if (Vue.version.startsWith('2.7.')) {
  switchVersion(2.7)
}
else if (Vue.version.startsWith('2.')) {
  switchVersion(2)
}
else if (Vue.version.startsWith('3.')) {
  switchVersion(3)
}
else {
  console.warn(`[vue-demi] Vue version v${Vue.version} is not supported.`)
}

接下来更改的实际方法如下

实用的VUE系列——这次用vueuse 学到了两个有意思的干货!!!声明:本文为稀土掘金技术社区首发签约文章,30天内禁

const fs = require('fs')
const path = require('path')

const dir = path.resolve(__dirname, '..', 'lib')

function loadModule(name) {
  try {
    return require(name)
  } catch (e) {
    return undefined
  }
}

function copy(name, version, vue) {
  vue = vue || 'vue'
  // 当前是vue3 的版本,拿到vue3 的目录,
  const src = path.join(dir, `v${version}`, name)
  const dest = path.join(dir, name)
  // 读取内容
  let content = fs.readFileSync(src, 'utf-8')
  content = content.replace(/'vue'/g, `'${vue}'`)
  
  try {
    fs.unlinkSync(dest)
  } catch (error) { }
  // 写入内容
  fs.writeFileSync(dest, content, 'utf-8')
}

function updateVue2API() {
  const ignoreList = ['version', 'default']
  const VCA = loadModule('@vue/composition-api')
  if (!VCA) {
    console.warn('[vue-demi] Composition API plugin is not found. Please run "npm install @vue/composition-api" to install.')
    return
  }

  const exports = Object.keys(VCA).filter(i => !ignoreList.includes(i))

  const esmPath = path.join(dir, 'index.mjs')
  let content = fs.readFileSync(esmPath, 'utf-8')

  content = content.replace(
    /\/\*\*VCA-EXPORTS\*\*\/[\s\S]+\/\*\*VCA-EXPORTS\*\*\//m,
`/**VCA-EXPORTS**/
export { ${exports.join(', ')} } from '@vue/composition-api/dist/vue-composition-api.mjs'
/**VCA-EXPORTS**/`
    )

  fs.writeFileSync(esmPath, content, 'utf-8')
  
}

function switchVersion(version, vue) {
  // 更改文件的函数
  copy('index.cjs', version, vue)
  copy('index.mjs', version, vue)
  copy('index.d.ts', version, vue)
  // 如果是非2.7的内容,因为要引入composition-api 所以要做个兼容
  if (version === 2)
    updateVue2API()
}


module.exports.loadModule = loadModule
module.exports.switchVersion = switchVersion

实用的VUE系列——这次用vueuse 学到了两个有意思的干货!!!声明:本文为稀土掘金技术社区首发签约文章,30天内禁

上述方法,就是简单将页面中的v3源码 直接挪到了最外部

我们看下我刚断点时候的v3源码

// 引用全部的vue3源码
var Vue = require('vue')
// 全部导出
Object.keys(Vue).forEach(function(key) {
  exports[key] = Vue[key]
})
// 添加几个方法
exports.set = function(target, key, val) {
  if (Array.isArray(target)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  target[key] = val
  return val
}

exports.del = function(target, key) {
  if (Array.isArray(target)) {
    target.splice(key, 1)
    return
  }
  delete target[key]
}

// 导出
exports.Vue = Vue
exports.Vue2 = undefined
exports.isVue2 = false
// 由于是直接替换的,直接写死
exports.isVue3 = true
exports.install = function(){}

我们发现,所谓的vue-demi添加了几个简单的变量之外,几乎全部用的是vue3的原生api

而到了vue2 的时候,直接如法炮制,将 vue2的代码放入最外层

于是我们只需要在vueuse 中如此引用即可

import {
  watch,
  computed,
  Ref,
  ref,
  set,
  del,
  nextTick,
  isVue2,  
} from 'vue-demi'

上述代码就能无缝的抹平vue2和vue3的所有差异,因为你引用这个库的本质,就是引用你代码中的vue库

vueuse的基本使用方式

vueuse 采用monorepo 方式构建,他包含很多插件,分别散落在@vueuse/head@vueuse/motion,@vueuse/gesture@vueuse/sound 等内容中,本质上来说,这些插件就是在别的库的基础上包裹一层而已

这不是我们这次要研究的内容,我们要研究的内容是@vueuse/core 也就是hooks 函数

首先我们还是要国际惯例,看看怎样使用

// 安装
npm i @vueuse/core

由于都是函数我们只需要引入执行即可,基本不需要做多余配置

<script setup>
import { useLocalStorage, useMouse, usePreferredDark } from '@vueuse/core'

// 跟踪鼠标位置
const { x, y } = useMouse()




// 用户是否喜欢暗黑主题
const isDark = usePreferredDark()

// 在本地存储中持久化状态  
const store = useLocalStorage(
  'my-storage',
  {
    name: 'Apple',
    color: 'red',
  },
)
</script>

好了,使用方式讲完了,我们可以来研究另一个干货,细细研究之下,我瞬间感觉我发现了宝藏

2、 怎样实现暗黑模式的useDark原理

vueuse 中有一个暗黑模式useDark,相信我们在业务中会经常使用

实用的VUE系列——这次用vueuse 学到了两个有意思的干货!!!声明:本文为稀土掘金技术社区首发签约文章,30天内禁

那么他是怎么实现的呢?

在最开始我以为所谓的 useDark 应该是一肩挑,我只需要简单的使用,就能自动的实现暗黑模式

import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark() 
const toggleDark = useToggle(isDark)

然后我发现,他竟然......

只是在跟标签中添加了类似于黑白主题的样式而已

实用的VUE系列——这次用vueuse 学到了两个有意思的干货!!!声明:本文为稀土掘金技术社区首发签约文章,30天内禁

于是细细研究他的源码发现,朴实无华,但又颇有深意

我们来看源码

export function useDark(options: UseDarkOptions = {}) {
  // 传入默认值
  const {
    valueDark = 'dark',
    valueLight = '',
    window = defaultWindow,
  } = options
  debugger
  //创建并管理当前页面的颜色模式,适用于不同类型的主题切换需求。
  const mode = useColorMode({
    ...options,
    onChanged: (mode, defaultHandler) => {
      if (options.onChanged)
        options.onChanged?.(mode === 'dark', defaultHandler, mode)
      else
        defaultHandler(mode)
    },
    modes: {
      dark: valueDark,
      light: valueLight,
    },
  })
  //获取系统主题
  const system = computed(() => {
    if (mode.system) {
      return mode.system.value
    }
    else {
      // 兼容 vue2 问题
      const preferredDark = usePreferredDark({ window })
      return preferredDark.value ? 'dark' : 'light'
    }
  })
  // 返回最终的计算属性的主题模式
  const isDark = computed<boolean>({
    get() {
      return mode.value === 'dark'
    },
    set(v) {
      const modeVal = v ? 'dark' : 'light'
      if (system.value === modeVal)
        mode.value = 'auto'
      else
        mode.value = modeVal
    },
  })

  return isDark
}

上述问题,主要就解决了一个问题,获取页面的是否是暗黑模式,而获取的方式为usePreferredDark

usePreferredDark 其实是一个特别简单的函数

export function usePreferredDark(options?: ConfigurableWindow) {
  // 主要用了媒体查询函数
  return useMediaQuery('(prefers-color-scheme: dark)', options)
}

于是我又转而研究useMediaQuery函数

/**
 * 使用给定的媒体查询字符串来检查它是否与当前窗口的尺寸匹配
 * 请注意,此函数依赖于浏览器的 window 对象和 matchMedia API,如果在没有 window 对象或 matchMedia API 的环境中使用,它将无法正常工作。
 */
export function useMediaQuery(query: MaybeRefOrGetter<string>, options: ConfigurableWindow = {}) {
  const { window = defaultWindow } = options
  // 检查当前环境是否支持 matchMedia API
  const isSupported = useSupported(() => window && 'matchMedia' in window && typeof window.matchMedia === 'function')

  let mediaQuery: MediaQueryList | undefined
  const matches = ref(false)
  // 主题变化回调函数
  const handler = (event: MediaQueryListEvent) => {
    debugger
    matches.value = event.matches
  }
  // 卸载事件监听
  const cleanup = () => {
    if (!mediaQuery)
      return
    if ('removeEventListener' in mediaQuery)
      mediaQuery.removeEventListener('change', handler)
    else
      // @ts-expect-error deprecated API
      mediaQuery.removeListener(handler)
  }

  const stopWatch = watchEffect(() => {
    if (!isSupported.value)
      return

    cleanup()
    // matchMedia 函数
    mediaQuery = window!.matchMedia(toValue(query))

    if ('addEventListener' in mediaQuery)
      mediaQuery.addEventListener('change', handler)
    else
      // @ts-expect-error deprecated API
      mediaQuery.addListener(handler)

    matches.value = mediaQuery.matches
  })
  //在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。
  tryOnScopeDispose(() => {
    stopWatch()
    cleanup()
    mediaQuery = undefined
  })
  // 返回最后的结果
  return matches
}

翻山越岭之后懂了,看似上述代码搞了一大堆,本质上就是利用matchMedia 来查询页面是不是暗黑模式

顺着这个知识点,我又去了解了一下matchMedia

matchMedia

Window 的 matchMedia() 方法返回一个新的 MediaQueryList 对象,表示指定的媒体查询字符串解析后的结果。返回的 MediaQueryList 可被用于判定 Document 是否匹配媒体查询,或者监控一个 document 来判定它匹配了或者停止匹配了此媒体查询。

这是 mdn 的官话,可能官话各位 jym 都听不懂,我来简单的翻译一下

他就是能查询当前是否命中了当前这个媒体查询内容 比如

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

等等等等,有的 jym有问了,这个prefers-color-scheme 啥意思呢?

prefers-color-scheme

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

其实翻译过来,他就是用媒体查询写一个规则,当是亮色主题的时候我们用什么css 样式,暗色主题的时候用什么样式,仅此而已!

但是,由于他本身的特性,就能被我们利用来判断当前页面的主题是不是夜间模式

当以上媒体查询命中的时候,matchMedia() 就会返回命中的结果我们只需要

window.matchMedia('(prefers-color-scheme: dark)')

实用的VUE系列——这次用vueuse 学到了两个有意思的干货!!!声明:本文为稀土掘金技术社区首发签约文章,30天内禁

当 matches 为 true 的时候,就表示当前为暗黑模式

说白了,这个useDark 其实就是修改和获取页面主题的,对于我们自定义主题还相去甚远

那我们要怎么动态修改页面模式呢?

方案很简单,根据useDark 对于 dom 的更改动态匹配

然后问题又来了,我该怎么匹配呢?

其实方案有很多,我们来看看vueuse 是怎么实现的吧

首先一个color-scheme 映入眼帘

实用的VUE系列——这次用vueuse 学到了两个有意思的干货!!!声明:本文为稀土掘金技术社区首发签约文章,30天内禁

color-scheme

color-scheme CSS 属性允许元素指示它可以舒适地呈现哪些颜色方案。

官方讲了一大堆,我们直接用中文翻译过来,更直观颜色方案 就是一个 css 属性 他具备以上属性值

color-scheme: normal; //没有
color-scheme: light; // 白天模式
color-scheme: dark; // 暗黑模式
color-scheme: light dark; // 跟随系统的模式

于是有的jym就问了,我都在根标签添加dark的 class 了我直接根据class 直接设置不就行了吗?

为什么还要多此一举

原因很简单,局部的滚动条你解决不了,这个属性能解决滚动条的颜色,所以这是要设置暗黑模式必备的属性

接下来就是常规的css 变量方案了,我们声明一个日常 bg 变量,以及一个 dark 变量,当 dark 的是时候覆盖 root

:root {
    --vp-c-bg: #ffffff;
    --vp-c-bg-alt: #f6f6f7;
    --vp-c-bg-elv: #ffffff;
    --vp-c-bg-soft: #f6f6f7
}

.dark {
    --vp-c-bg: #1b1b1f;
    --vp-c-bg-alt: #161618;
    --vp-c-bg-elv: #202127;
    --vp-c-bg-soft: #202127
}

以上示例就完整的介绍了,暗黑模式的的方案,接下来给大家完成的实例演示:

<template>
  <button @click="toggleDark()">
    <i inline-block align-middle i="dark:carbon-moon carbon-sun" />
    <span class="ml-2">{{ isDark ? 'Dark' : 'Light' }}</span>
  </button>
  <h1 class="h1">我要好好学习天天向上
      我要好好学习天天向上
      我要好好学习天天向上
      我要好好学习天天向上
      我要好好学习天天向上
      我要好好学习天天向上
      我要好好学习天天向上
      我要好好学习天天向上
      我要好好学习天天向上
      我要好好学习天天向上
      我要好好学习天天向上
      我要好好学习天天向上
      我要好好学习天天向上

    </h1>
</template>
<script setup lang="ts">
import {useDark, useToggle} from '@vueuse/core'
const isDark = useDark({
  valueDark: 'dark',
  valueLight: 'light',
})
const toggleDark = useToggle(isDark)
</script>
<style>
/* 暗黑模式必备,防止滚动条颜色问题 */
html.dark {
  color-scheme: dark;
}
:root {
    --vp-c-bg: #ffffff;
    --vp-c-text-1: rgba(60, 60, 67);
}
.dark {
    --vp-c-bg: #1b1b1f;
    --vp-c-text-1: rgba(255, 255, 245, .86);
}
/* 设置主题 */
body{
  color: var(--vp-c-text-1);
  background-color: var(--vp-c-bg);
}
.h1{
  height: 150px;
  overflow: auto;
}
</style>

效果如下:

实用的VUE系列——这次用vueuse 学到了两个有意思的干货!!!声明:本文为稀土掘金技术社区首发签约文章,30天内禁

以上代码中,看似复杂,其实很简单,只是根据全局dark 覆盖原有样式,来实现背景以及文字的不同模式

最后

干货研究完了,收获颇丰,希望和各位 jym共同进步!!

转载自:https://juejin.cn/post/7405774566565396543
评论
请登录