实用的VUE系列——这次用vueuse 学到了两个有意思的干货!!!声明:本文为稀土掘金技术社区首发签约文章,30天内禁
前言
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
, 他只是很巧妙的做了一个承上启下
我简单的画个图,来帮助大家更快的理解
上图中,我们可以发现,他的本质,就是动态的更改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.`)
}
接下来更改的实际方法如下
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
上述方法,就是简单将页面中的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
,相信我们在业务中会经常使用
那么他是怎么实现的呢?
在最开始我以为所谓的 useDark
应该是一肩挑,我只需要简单的使用,就能自动的实现暗黑模式
import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark()
const toggleDark = useToggle(isDark)
然后我发现,他竟然......
只是在跟标签中添加了类似于黑白主题的样式而已
于是细细研究他的源码发现,朴实无华
,但又颇有深意
我们来看源码
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)')
当 matches 为 true
的时候,就表示当前为暗黑模式
说白了,这个useDark
其实就是修改和获取页面主题的,对于我们自定义主题还相去甚远
那我们要怎么动态修改页面模式呢?
方案很简单,根据useDark
对于 dom
的更改动态匹配
然后问题又来了,我该怎么匹配呢?
其实方案有很多,我们来看看vueuse
是怎么实现的吧
首先一个color-scheme
映入眼帘
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>
效果如下:
以上代码中,看似复杂,其实很简单,只是根据全局dark
覆盖原有样式,来实现背景以及文字的不同模式
最后
干货研究完了,收获颇丰,希望和各位 jym
共同进步!!
转载自:https://juejin.cn/post/7405774566565396543