「原生练手」✨将深浅色模式切换逻辑抽离成hooks,体验hooks的魅力!
最近项目开发的过程中经常有抽离hooks
提高代码复用性的场景,在我个人看来,抽离hooks
主要有以下好处:
- 提高代码复用性,对于存在明显重复代码的场景,这能够很大程度上减少重复代码,解放你的双手,停止重复劳动
- 即便是没有复用性的场景,也可以起到一个语义化的作用,将某一个逻辑抽离成
hooks
,起一个语义化的名字,这样一来别人看到你的代码,就能够见名知意,知道对应的功能是什么,而不用去理解实现细节 - 抽离
hooks
的过程中,我们往往能够发现一些可扩展点,在保证原来功能的基础上进行扩展,让其功能更加丰富,提高可扩展性
今天我们就以深浅色模式切换这个功能为例,体验以下抽离hooks
的整个流程是怎样的
场景搭建
为了方便,这里我直接使用vite
的vanilla
模板,也就是原生开发,
pnpm create vite dark-light-toggler --template vanilla-ts
然后我们在src
目录下创建一个hooks
目录,用于存放我们的hooks
,styles
存放全局的样式
思考深浅色模式切换的原理
首先我们要知道如何去区分深色和浅色模式,我的思路是在html
根结点上添加一个.dark
或.light
的类名去标识当前的模式,然后css
中根据这两个类名显示对应的颜色
由于考虑到其他的功能或许有可能也会在html
根节点上添加类名,所以我们不能够说简单的将根节点的className
设置成light
或者dark
,而应当通过classList
的add
和remove
去控制,不应当影响到别的功能对根节点类名的操作
其次,我们还需要有一个按钮,点击它能够切换深色模式和浅色模式,还需要能够根据设备的深浅色模式自动选择默认的深浅色模式,这个可以通过媒体查询prefers-color-scheme
去实现,稍后讲实现时会说到
编码实践
页面结构
理解了原理后我们可以开始实战了,首先编写以下整个页面结构
<!DOCTYPE html>
<html lang="en" wh-full class="light">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dark Light Toggler</title>
</head>
<body wh-full>
<!-- 右上角深色模式切换 -->
<section fixed top-10 right-20 text-2xl cursor-pointer>
<div i-ic-twotone-wb-sunny></div>
</section>
<div id="app" flex-center wh-full font-mono>
<div w-sm h-sm i-uim:rocket></div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
结构不复杂,就是右上角一个图标按钮,中间一个图标让界面看起来不会那么空荡,根节点上初始化一个类名light
,也就是默认是浅色模式
这里样式的编写我没有使用css
,而是使用了antf
大神的UnoCSS
原子化css
框架,并且结合Attributify
的方式,可以直接以html attribute
的方式编写原子化样式,再结合vscode
的unocss
插件,开发体验简直不要太爽!
有了页面结构后,再编写一下根节点的.dark
和.light
样式,主要就是修改背景色
html {
transition: background-color 1s ease;
}
html.dark {
background-color: #1b2430;
}
html.light {
background-color: #e7f6f2;
}
目前的效果如下:
实现深浅色模式切换逻辑
接下来我们编写深浅色模式切换逻辑,主要就是获取根节点上的类名,点击按钮时切换类名即可,于是我们可以写出第一版实现
import '@unocss/reset/antfu.css'
import 'uno.css'
import '@/styles/index.scss'
const oDarkToggler = document.getElementById('dark-toggler')
const init = () => {
bindEvent()
}
const bindEvent = () => {
oDarkToggler?.addEventListener('click', () => {
// 获取当前主题
const currentTheme = document.documentElement.className
if (currentTheme === 'light') document.documentElement.className = 'dark'
else if (currentTheme === 'dark')
document.documentElement.className = 'light'
})
}
init()
很简单粗暴,就是获取当前主题,然后取反进行切换,于是现在的效果如下
右上角的图标并没有切换,为了方便切换图标,我们给图标加个id
,并且原子化样式由attribute
形式改成类名形式,方便修改
<!-- 右上角深色模式切换 -->
<section fixed top-10 right-20 text-2xl cursor-pointer>
<div id="dark-toggler" class="i-ic-twotone-wb-sunny"></div>
</section>
然后加上图标类名的切换
oDarkToggler?.addEventListener('click', () => {
// 获取当前主题
const currentTheme = document.documentElement.className
// 切换根节点类名和按钮图标
if (currentTheme === 'light') {
document.documentElement.className = 'dark'
oDarkToggler.className = 'i-material-symbols:dark-mode'
} else if (currentTheme === 'dark') {
document.documentElement.className = 'light'
oDarkToggler.className = 'i-ic-twotone-wb-sunny'
}
})
现在效果如下:
可以看到图标的切换也正常了,那么就这样就结束了吗?肯定没有,我们还没进入正题呢!
首先先做一个小的重构优化,现在我们的代码看上去已经有很明显的重复了,那就是对根节点类名的赋值,每次修改都要写上一长串,不优雅,而且不够语义化,这里可以先抽离一下
const bindEvent = () => {
oDarkToggler?.addEventListener('click', () => {
// 获取当前主题
const currentTheme = getRootElClassName()
// 切换根节点类名和按钮图标
if (currentTheme === 'light') {
setRootElClassName('dark')
oDarkToggler.className = 'i-material-symbols:dark-mode'
} else if (currentTheme === 'dark') {
setRootElClassName('light')
oDarkToggler.className = 'i-ic-twotone-wb-sunny'
}
})
}
const getRootElClassName = (): string => {
return document.documentElement.className
}
const setRootElClassName = (className: string): void => {
document.documentElement.className = className
}
抽离成hooks
现在我们的功能算是完成了,但是考虑一下过几个月后再来看这个代码,肯定是需要花点时间看一看代码才知道这部分代码想要做什么事情,而不能够见名知意,不够语义化,可维护性较低,这时候抽离成hooks
就能够帮大忙了
首先创建src/hooks/useToggleDark.ts
,其代码如下
interface Options {
// 切换按钮的 id
togglerId: string
// 深色模式的图标类名
darkIcon: string
// 浅色模式的图标类名
lightIcon: string
}
export default function useToggleDarkLight(options?: Options) {
const togglerId = options?.togglerId
const darkIcon = options?.darkIcon
const lightIcon = options?.lightIcon
let togglerEl: HTMLElement | null = null
// 如果有传入按钮 id 则需要对按钮图标进行修改
togglerId && (togglerEl = document.getElementById(togglerId))
return () => {
// 获取当前主题
const currentTheme = getRootElClassName()
// 切换根节点类名和按钮图标
if (currentTheme === 'light') {
setRootElClassName('dark')
togglerEl && (togglerEl.className = darkIcon!)
} else if (currentTheme === 'dark') {
setRootElClassName('light')
togglerEl && (togglerEl.className = lightIcon!)
}
}
}
const getRootElClassName = (): string => {
return document.documentElement.className
}
const setRootElClassName = (className: string): void => {
document.documentElement.className = className
}
就是将刚刚的代码复制了过来,但是做了一些改动,由于我们现在是抽离hooks
了,就应当尽可能让其和刚才的特定环境解耦,也就是只负责深浅色的切换,而图标的更改其实未必一定会有
比如我希望在setInterval
中每两秒自动切换一次深浅色模式的话,这种时候其实我是不需要切换按钮图标的,不过我们可以将按钮图标的切换作为一个可扩展点,允许用户传入配置,当其传入了按钮图标的相关配置后则会自动将图标也进行切换
配置项中包括按钮图标的id
,用于获取该元素,其次,由于图标的类名也是可能不同的,所以也要作为配置项,于是得到了如上代码
这样一来,当我们只是希望切换深浅色的时候,调用该hooks
返回的函数即可,由于闭包的特性,这些配置项都会被保留下来,这样子可以做什么有趣的事情呢?
我们可以利用其生成一个只切换深浅色的toggleDark
函数,也可以再生成一个既切换深浅色,又切换按钮图标的toggleDarkWithIcon
函数
import '@unocss/reset/antfu.css'
import 'uno.css'
import '@/styles/index.scss'
import { useToggleDarkLight } from './hooks'
const oDarkToggler = document.getElementById('dark-toggler')
// 只切换深浅色
const toggleDark = useToggleDarkLight()
// 切换深浅色的同时还能切换按钮图标
const toggleDarkWithIcon = useToggleDarkLight({
togglerId: 'dark-toggler',
darkIcon: 'i-material-symbols:dark-mode',
lightIcon: 'i-ic-twotone-wb-sunny',
})
const init = () => {
// 自动切换深浅色 但是不希望切换图标
setInterval(toggleDark, 3000)
bindEvent()
}
const bindEvent = () => {
// 点击切换深浅色 同时切换按钮图标
oDarkToggler?.addEventListener('click', toggleDarkWithIcon)
}
init()
抽象再升级 -- 扩展成任意主题的切换
在深浅色切换的基础上,我们可以再扩展,这次我们的目标不局限于深浅色模式的切换了,而是任意主题的切换
再创建一个src/hooks/useThemer.ts
这次我们实现一个themer
,它可以管理主题,包括对主题的初始化、获取当前主题、设置当前主题,以及把刚才的深浅色模式作为单独的功能集成进来,我们可以先定义它的接口
interface Themer {
initTheme: () => void
getCurrentTheme: () => Theme
setCurrentTheme: (theme: Theme) => void
toggleDarkTheme: () => void
}
themer
的实现如下
interface Options {
// 切换按钮的 id
togglerId: string
// 深色模式的图标类名
darkIcon: string
// 浅色模式的图标类名
lightIcon: string
}
export default function useThemer(options?: Options) {
const togglerId = options?.togglerId
const darkIcon = options?.darkIcon
const lightIcon = options?.lightIcon
let togglerEl: HTMLElement | null = null
// 如果有传入按钮 id 则需要对按钮图标进行修改
togglerId && (togglerEl = document.getElementById(togglerId))
const themer: Themer = {
initTheme: () => {
// 使用媒体查询判断当前系统设备是什么模式 自动初始化为对应模式的主题
const systemIsDark = window.matchMedia(
'(prefers-color-scheme: dark)',
).matches
if (systemIsDark) {
themer.setCurrentTheme('dark')
} else {
themer.setCurrentTheme('light')
}
},
// 没有设置主题时默认是 light 主题
getCurrentTheme: () => (getRootElDataTheme() as Theme) ?? 'light',
setCurrentTheme: (theme: Theme) => {
const currentTheme = themer.getCurrentTheme()
// 采用 classList 添加和移除类名的方式 避免影响其他同样会操作根节点类名的操作
// 先将原来的主题移除
document.documentElement.classList.remove(currentTheme)
// 再设置新主题
setRootElDataTheme(theme)
document.documentElement.classList.add(theme)
},
// 在 light 和 dark 主题之间切换
toggleDarkTheme: () => {
const currentTheme = themer.getCurrentTheme()
switch (currentTheme) {
case 'light':
themer.setCurrentTheme('dark')
togglerEl && (togglerEl.className = darkIcon!)
break
case 'dark':
themer.setCurrentTheme('light')
togglerEl && (togglerEl.className = lightIcon!)
break
}
},
}
return themer
}
const getRootElDataTheme = () => {
return document.documentElement.dataset.theme
}
const setRootElDataTheme = (theme: Theme): void => {
document.documentElement.dataset.theme = theme
}
注释已经写的很清楚了,就不赘述了,直接来体验一下吧
import { useThemer } from '@/hooks'
export const useThemerDemo = () => {
const { initTheme, toggleDarkTheme } = useThemer({
togglerId: 'dark-toggler',
darkIcon: 'i-material-symbols:dark-mode',
lightIcon: 'i-ic-twotone-wb-sunny',
})
const oDarkToggler = document.getElementById('dark-toggler')
const init = () => {
initTheme()
bindEvent()
}
const bindEvent = () => {
oDarkToggler?.addEventListener('click', toggleDarkTheme)
}
init()
}
效果和前面的是一样的,但是由于我们扩展了任意主题的特性,怎么能不尝鲜一下呢,在页面中再添加一个按钮,用于切换到自定义主题
<!-- 右上角深色模式切换 -->
<section fixed top-10 right-20 text-2xl cursor-pointer flex gap-3>
<div id="dark-toggler" class="i-ic-twotone-wb-sunny"></div>
<div i-icon-park-twotone:theme></div>
</section>
那么怎么切换主题呢?别忘了我们的themer
中有一个setCurrentTheme
的方法,通过它就可以切换根节点上的主题类名,然后我们再在scss
中编写全局的主题样式即可生效
const { initTheme, toggleDarkTheme, setCurrentTheme } = useThemer({
togglerId: 'dark-toggler',
darkIcon: 'i-material-symbols:dark-mode',
lightIcon: 'i-ic-twotone-wb-sunny',
})
oThemeToggler?.addEventListener('click', () =>
setCurrentTheme('plasticine'),
)
这里我添加了一个名为plasticine
的主题,我们来配置一下该主题
html.plasticine {
background-color: #748da6;
color: #f2d7d9;
}
效果如下:
可以看到,扩展主题成功
总结
怎么样,通过这样一个简单的深浅色切换到主题切换扩展的小案例,你能体会到hooks
的强大之处吗,它能够提高代码复用性,即便是没有提高复用性的场景,也可以提高代码的可读性,屏蔽具体细节,更加语义化,方便多人维护
其次,在抽离hooks
的过程中,我们也能够很自然的发现可扩展点,做进一步优化,总之,hooks yyds!
转载自:https://juejin.cn/post/7132872750804238349