在查阅 VitePress 具体实践时,我被 UnoCSS 文档中的彩虹动画效果深深吸引。在查看其实现原理之后,本文也将探索如何通过自定义组件和样式增强 VitePress 站点,并实现一个炫酷的彩虹动画效果。
自定义主题
VitePress 允许你通过自定义 Layout 来改变页面的结构和样式。自定义 Layout 可以帮助你更好地控制页面的外观和行为,尤其是在复杂的站点中。
项目初始化
在终端中运行以下命令,初始化一个新的 VitePress 项目:
npx vitepress init
然后根据提示,这次选择自定义主题(Default Theme + Customization
):
┌ Welcome to VitePress!
│
◇ Where should VitePress initialize the config?
│ ./docs
│
◇ Site title:
│ My Awesome Project
│
◇ Site description:
│ A VitePress Site
│
◇ Theme:
│ Default Theme + Customization
│
◇ Use TypeScript for config and theme files?
│ Yes
│
◇ Add VitePress npm scripts to package.json?
│ Yes
│
└ Done! Now run npm run docs:dev and start writing.
Tips:
- Make sure to add docs/.vitepress/dist and docs/.vitepress/cache to your .gitignore file.
- Since you've chosen to customize the theme, you should also explicitly install vue as a dev dependency.
注意提示,这里需要额外手动安装 vue 库:
pnpm add vue
自定义入口文件
找到 .vitepress/theme/index.ts
入口文件:
// <https://vitepress.dev/guide/custom-theme>
import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import './style.css'
export default {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
// <https://vitepress.dev/guide/extending-default-theme#layout-slots>
})
},
enhanceApp({ app, router, siteData }) {
// ...
}
} satisfies Theme
里面暴露了一个 Layout 组件,这里是通过 h 函数实现的,我们将其抽离成 Layout.vue
组件。
创建自定义 Layout
VitePress 的 Layout 组件是整个网站的骨架,控制了页面的基本结构和布局。通过自定义 Layout,我们可以完全掌控网站的外观和行为。
为什么要自定义 Layout?
- 增加特定的布局元素
- 修改默认主题的行为
- 添加全局组件或功能
- 实现特殊的视觉效果(如我们的彩虹动画)
我们在 .vitepress/theme
文件夹中创建 Layout.vue
组件,并将之前的内容转换成 vue 代码:
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
</script>
<template>
<DefaultTheme.Layout />
</template>
接下来,在 .vitepress/theme/index.ts
中注册自定义 Layout:
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import CustomLayout from './Layout.vue'
export default {
extends: DefaultTheme,
Layout: CustomLayout,
}
这将会覆盖默认的 Layout,应用你自定义的布局结构。
覆盖原本样式
VitePress 提供了 css 变量来动态修改自带的样式,可以看到项目初始化后在 .vitepress/theme
中有一个 style.css
。里面提供了案例,告诉如何去修改这些变量。
同时可以通过该链接查看全部的 VitePress 变量:VitePress 默认主题变量。
VitePress 允许我们通过多种方式覆盖默认样式。最常用的方法是创建一个 CSS 文件,并在主题配置中导入。
比如想设置 name
的颜色,就可以通过:
:root {
--vp-home-hero-name-color: blue;
}
引入 UnoCSS
UnoCSS 是一个按需生成 CSS 的工具,可以极大简化 CSS 管理,帮助快速生成高效样式。
在项目中安装 UnoCSS 插件:
pnpm add -D unocss
然后,在 vite.config.ts
中配置 UnoCSS 插件:
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [UnoCSS()],
}
通过 UnoCSS,可以轻松应用样式而无需写冗余 CSS。例如,使用以下类名快速创建按钮样式:
<button class="bg-blue-500 text-white p-4 rounded-lg hover:bg-blue-600">
按钮
</button>
实现彩虹动画
彩虹动画是本文的主角,主要通过动态改变 CSS 变量值来实现色彩的平滑过渡。
定义彩虹动画关键帧
通过 @keyframes
,在不同颜色之间平滑过渡,形成彩虹动画效果。创建 rainbow.css
文件:
@keyframes rainbow {
0% {
--vp-c-brand-1: #00a98e;
--vp-c-brand-light: #4ad1b4;
--vp-c-brand-lighter: #78fadc;
--vp-c-brand-dark: #008269;
--vp-c-brand-darker: #005d47;
--vp-c-brand-next: #009ff7;
}
25% {
--vp-c-brand-1: #00a6e2;
--vp-c-brand-light: #56cdff;
--vp-c-brand-lighter: #87f6ff;
--vp-c-brand-dark: #0080b9;
--vp-c-brand-darker: #005c93;
--vp-c-brand-next: #9280ed;
}
50% {
--vp-c-brand-1: #c76dd1;
--vp-c-brand-light: #f194fa;
--vp-c-brand-lighter: #ffbcff;
--vp-c-brand-dark: #9e47a9;
--vp-c-brand-darker: #772082;
--vp-c-brand-next: #eb6552;
}
75% {
--vp-c-brand-1: #e95ca2;
--vp-c-brand-light: #ff84ca;
--vp-c-brand-lighter: #ffadf2;
--vp-c-brand-dark: #be317d;
--vp-c-brand-darker: #940059;
--vp-c-brand-next: #d17a2a;
}
100% {
--vp-c-brand-1: #00a98e;
--vp-c-brand-light: #4ad1b4;
--vp-c-brand-lighter: #78fadc;
--vp-c-brand-dark: #008269;
--vp-c-brand-darker: #005d47;
--vp-c-brand-next: #009ff7;
}
}
:root {
--vp-c-brand-1: #00a8cf;
--vp-c-brand-light: #52cff7;
--vp-c-brand-lighter: #82f8ff;
--vp-c-brand-dark: #0082a7;
--vp-c-brand-darker: #005e81;
--vp-c-brand-next: #638af8;
animation: rainbow 40s linear infinite;
}
html:not(.rainbow) {
--vp-c-brand-1: #00a8cf;
--vp-c-brand-light: #52cff7;
--vp-c-brand-lighter: #82f8ff;
--vp-c-brand-dark: #0082a7;
--vp-c-brand-darker: #005e81;
--vp-c-brand-next: #638af8;
animation: none !important;
}
这段代码定义了彩虹动画的五个关键帧,并将动画应用到根元素上。注意,我们还定义了不带动画的默认状态,这样就可以通过 CSS 类切换动画的启用/禁用。
实现彩虹动画控制组件
接下来,实现名为 RainbowAnimationSwitcher
的组件,其主要逻辑是通过添加或移除 HTML 根元素上的 rainbow
类来控制动画的启用状态,从而实现页面的彩虹渐变效果。
这个组件使用了 @vueuse/core 的两个工具函数:
useLocalStorage
用于在浏览器本地存储用户的偏好设置useMediaQuery
用于检测用户系统是否设置了减少动画
<script lang="ts" setup>
import { useLocalStorage, useMediaQuery } from '@vueuse/core'
import { inBrowser } from 'vitepress'
import { computed, watch } from 'vue'
import RainbowSwitcher from './RainbowSwitcher.vue'
defineProps<{ text?: string; screenMenu?: boolean }>()
const reduceMotion = useMediaQuery('(prefers-reduced-motion: reduce)').value
const animated = useLocalStorage('animate-rainbow', inBrowser ? !reduceMotion : true)
function toggleRainbow() {
animated.value = !animated.value
}
// 在这里对动画做处理
watch(
animated,
anim => {
document.documentElement.classList.remove('rainbow')
if (anim) {
document.documentElement.classList.add('rainbow')
}
},
{ immediate: inBrowser, flush: 'post' },
)
const switchTitle = computed(() => {
return animated.value ? 'Disable rainbow animation' : 'Enable rainbow animation'
})
</script>
<template>
<ClientOnly>
<div class="group" :class="{ mobile: screenMenu }">
<div class="NavScreenRainbowAnimation">
<p class="text">
{{ text ?? 'Rainbow Animation' }}
</p>
<RainbowSwitcher
:title="switchTitle"
class="RainbowAnimationSwitcher"
:aria-checked="animated ? 'true' : 'false'"
@click="toggleRainbow"
>
<span class="i-tabler:rainbow animated" />
<span class="i-tabler:rainbow-off non-animated" />
</RainbowSwitcher>
</div>
</div>
</ClientOnly>
</template>
<style scoped>
.group {
border-top: 1px solid var(--vp-c-divider);
padding-top: 10px;
margin-top: 1rem !important;
}
.NavScreenRainbowAnimation {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 8px;
padding: 12px;
background-color: var(--vp-c-bg-elv);
max-width: 220px;
}
.text {
line-height: 24px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.animated {
opacity: 1;
}
.non-animated {
opacity: 0;
}
.RainbowAnimationSwitcher[aria-checked='false'] .non-animated {
opacity: 1;
}
.RainbowAnimationSwitcher[aria-checked='true'] .animated {
opacity: 1;
}
</style>
其中 RainbowSwitcher
组件是一个简单的开关按钮。以下是其实现:
<template>
<button class="VPSwitch" type="button" role="switch">
<span class="check">
<span v-if="$slots.default" class="icon">
<slot />
</span>
</span>
</button>
</template>
<style scoped>
.VPSwitch {
position: relative;
border-radius: 11px;
display: block;
width: 40px;
height: 22px;
flex-shrink: 0;
border: 1px solid var(--vp-input-border-color);
background-color: var(--vp-input-switch-bg-color);
transition: border-color 0.25s !important;
}
.check {
position: absolute;
top: 1px;
left: 1px;
width: 18px;
height: 18px;
border-radius: 50%;
background-color: var(--vp-c-neutral-inverse);
box-shadow: var(--vp-shadow-1);
transition: transform 0.25s !important;
}
.icon {
position: relative;
display: block;
width: 18px;
height: 18px;
border-radius: 50%;
overflow: hidden;
}
</style>
挂载组件
在 .vitepress/theme/index.ts
中,在 enhanceApp
中挂载组件:
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import CustomLayout from './Layout.vue'
export default {
extends: DefaultTheme,
Layout: CustomLayout,
enhanceApp({ app, router }) {
app.component('RainbowAnimationSwitcher', RainbowAnimationSwitcher)
if (typeof window === 'undefined') return
watch(
() => router.route.data.relativePath,
() => updateHomePageStyle(location.pathname === '/'),
{ immediate: true },
)
},
}
// Speed up the rainbow animation on home page
function updateHomePageStyle(value: boolean) {
if (value) {
if (homePageStyle) return
homePageStyle = document.createElement('style')
homePageStyle.innerHTML = `
:root {
animation: rainbow 12s linear infinite;
}`
document.body.appendChild(homePageStyle)
} else {
if (!homePageStyle) return
homePageStyle.remove()
homePageStyle = undefined
}
}
在导航栏中使用彩虹动画开关
在 .vitepress/config/index.ts
的配置文件中添加彩虹动画开关按钮:
export default defineConfig({
themeConfig: {
nav: [
// 其他导航项...
{
text: `v${version}`,
items: [
{
text: '发布日志',
link: '<https://github.com/yourusername/repo/releases>',
},
{
text: '提交 Issue',
link: '<https://github.com/yourusername/repo/issues>',
},
{
component: 'RainbowAnimationSwitcher',
props: {
text: '彩虹动画',
},
},
],
},
],
// 其他配置...
},
})
这样,彩虹动画开关就成功加载到导航栏的下拉菜单中。
如果想查看具体效果,可查看 EasyEditor 的文档。其中关于彩虹动画效果的详细实现看,可以查看内部对应的代码:EasyEditor/docs/.vitepress/theme at main · Easy-Editor/EasyEditor。