简介
Vitepress基于Vue3用到了 <slot>
插槽,在 <Layout/>
布局组件中预留了一些插槽,可以对页面布局进行自定义修改
由于也是使用组件,请了解过 组件的使用 了再来看
信息
布局插槽就好比一个插线板,将电器的插头插入对应的插线孔就可以工作了
示例
开始前,请确保你安装了 vue
,已安装的无视
npm i vue
pnpm add -D vue
yarn add -D vue
bun add -D vue
在 .vitepress/theme/components
目录新建一个 MyLayout.vue
组件
docs
├─ .vitepress
│ └─ config.mts
│ └─ theme
│ │ ├─ components
│ │ │ └─ MyLayout.vue <-- 插槽组件
│ │ └─ index.ts
└─ index.md
示例1:Layout
在 MyLayout.vue
中粘贴如下代码
<script setup>
import DefaultTheme from 'vitepress/theme'
const { Layout } = DefaultTheme
</script>
<template>
<Layout>
<template #aside-outline-before>
<div class="title">aside-outline-before</div>
</template>
<template #doc-before>
<div class="title">doc-before</div>
</template>
</Layout>
</template>
<style scoped>
.title {
color: red;
}
</style>
然后在 .vitepress/theme/index.mts
中引入
// .vitepress/theme/index.mts
import DefaultTheme from 'vitepress/theme'
import MyLayout from './components/MyLayout.vue'
export default {
extends: DefaultTheme,
Layout: MyLayout,
}
示例2:h函数
在 MyLayout.vue
中粘贴如下代码
<script setup>
</script>
<template>
<div class="title">aside-outline-before</div>
</template>
<style scoped>
.title {
color: red;
}
</style>
// .vitepress/theme/index.mts
import DefaultTheme from 'vitepress/theme'
import { h } from 'vue'
import MyLayout from './components/MyLayout.vue'
// import MyLayout2 from './components/MyLayout2.vue' // 第2个组件
export default {
extends: DefaultTheme,
Layout() {
return h(DefaultTheme.Layout, null, {
'aside-outline-before': () => h(MyLayout),
//'doc-before': () => h(MyLayout2), // 第2个组件使用doc-before插槽
})
}
}
示例3:使用首页预留插槽
现在我们已经能做到在Features
下Footer
上添加自定义内容了,但是我有办法将自定义内容加到Header
下Hero
上吗
答案是可以的,vitepress首页给我们预留了很多插槽,通过插槽我们可以将自定义组件渲染到想要的位置
我们尝试将一个组件放到Hero
上方
在components
目录下新建HeroBefore.vue
<script setup lang="ts"></script>
<template>
<div class="before">HeroBefore</div>
</template>
<style>
.before {
color: green;
font-size: 24px;
font-weight: 700;
text-align: center;
}
</style>
安装vue,因为需要使用vue提供的h
方法
pnpm add -D vue
在theme/index.ts
中使用插槽
import Theme from 'vitepress/theme'
import './style/var.css'
import FreeStyle from '../components/FreeStyle.vue'
import { h } from 'vue'
import HeroBefore from '../components/HeroBefore.vue'
export default {
...Theme,
Layout() {
return h(Theme.Layout, null, {
'home-hero-before': () => h(HeroBefore)
})
},
enhanceApp({ app }) {
app.component('FreeStyle', FreeStyle)
}
}
效果如下:
可以看到组件已经渲染到Header下Hero上方了
关于Layout方法中的h函数的使用
/vp/docs/.vitepress/theme/index.ts
单个 retrun h
export default {
extends: DefaultTheme,
// 以下添加一个自定义layoutClass
Layout: () => {
const props: Record<string, any> = {}
// 获取 frontmatter
const { frontmatter } = useData()
/* 添加自定义 class */
if (frontmatter.value?.layoutClass) {
props.class = frontmatter.value.layoutClass
}
return h(DefaultTheme.Layout, props)
// 与上面的return重复,不能生效
return h(Theme.Layout, null, {
"home-hero-info": () => h(AnimateTitle),
});
},
}
如上这种情况,第二个return h
将不会生效
多个return h
export default {
extends: DefaultTheme,
// 以下添加一个自定义layoutClass
Layout: () => {
const props: Record<string, any> = {}
// 获取 frontmatter
const { frontmatter } = useData()
/* 添加自定义 class */
if (frontmatter.value?.layoutClass) {
props.class = frontmatter.value.layoutClass
}
return h(DefaultTheme.Layout, props,{
"home-hero-info": () => h(AnimateTitle), //多个插槽的配置
'nav-bar-title-after': () => h(MNavVisitor),
'doc-after': () => h(MDocFooter),
'aside-bottom': () => h(MAsideSponsors)
})
},
}
如上,使用这种写法就可以了。
查看插槽位置
vitepress文档并没有详细说明,我们可以通过查阅vitepress源码来知道预留的插槽位置,文件在src/client/theme-default/components/Layout.vue
<template>
<div class="Layout">
<slot name="layout-top" />
<VPSkipLink />
<VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" />
<VPNav>
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
<template #nav-bar-content-before><slot name="nav-bar-content-before" /></template>
<template #nav-bar-content-after><slot name="nav-bar-content-after" /></template>
<template #nav-screen-content-before><slot name="nav-screen-content-before" /></template>
<template #nav-screen-content-after><slot name="nav-screen-content-after" /></template>
</VPNav>
<VPLocalNav :open="isSidebarOpen" @open-menu="openSidebar" />
<VPSidebar :open="isSidebarOpen" />
<VPContent>
<template #home-hero-before><slot name="home-hero-before" /></template>
<template #home-hero-after><slot name="home-hero-after" /></template>
<template #home-features-before><slot name="home-features-before" /></template>
<template #home-features-after><slot name="home-features-after" /></template>
<template #doc-footer-before><slot name="doc-footer-before" /></template>
<template #doc-before><slot name="doc-before" /></template>
<template #doc-after><slot name="doc-after" /></template>
<template #aside-top><slot name="aside-top" /></template>
<template #aside-bottom><slot name="aside-bottom" /></template>
<template #aside-outline-before><slot name="aside-outline-before" /></template>
<template #aside-outline-after><slot name="aside-outline-after" /></template>
<template #aside-ads-before><slot name="aside-ads-before" /></template>
<template #aside-ads-after><slot name="aside-ads-after" /></template>
</VPContent>
<VPFooter />
<slot name="layout-bottom" />
</div>
</template>
通过插槽名能大概猜到位置在哪,当然也能一个个试知道具体位置,结合这些插槽就能自定义出更个性化的vitepress首页了
插槽表
不同的页面类型,可使用的插槽不同
doc
当 Frontmatter 配置 layout: doc
(默认)时插槽及位置
- doc-top
- doc-bottom
- doc-footer-before
- doc-before
- doc-after
- sidebar-nav-before
- sidebar-nav-after
- aside-top
- aside-bottom
- aside-outline-before
- aside-outline-after
- aside-ads-before
- aside-ads-after
home
当 Frontmatter 配置 layout: home
(默认)时插槽及位置
- home-hero-before
- home-hero-info
- home-hero-image
- home-hero-after
- home-features-before
- home-features-after
page
当 Frontmatter 配置 layout: page
(默认)时插槽及位置
- page-top
- page-bottom
404
在未找到 (404) 页面上
- not-found
Always
所有布局模式下均可使用的插槽
- layout-top
- layout-bottom
- nav-bar-title-before
- nav-bar-title-after
- nav-bar-content-before
- nav-bar-content-after
- nav-screen-content-before
- nav-screen-content-after
使用插槽原理
重点是vue3提供的h
函数
在官方文档的这篇文章中讲到,第三个参数传入一个对象,就表示给第一个参数,也就是render
的组件的插槽传入内容
这个是vitepress默认使用的theme配置,当用户自定义了theme/index.ts
的时候,就会优先使用用户配置,所以当我们传入Layout属性的时候,最终渲染的是我们传入的Layout,所以用h函数改写Layout并使用插槽,就能实现个性化首页
文档页面主题色
使用演示
分别演示两种使用情况,Frontmatter使用 和 常规使用
Frontmatter使用
本方法参考 掘金 @Younglina的文章
通过VitePress官网给出的 useDate 返回页面数据,可以看到返回对象的类型
interface VitePressData {
site: Ref<SiteData>
page: Ref<PageData>
theme: Ref<any> // themeConfig from .vitepress/config.js
frontmatter: Ref<PageData['frontmatter']>
lang: Ref<string>
title: Ref<string>
description: Ref<string>
localePath: Ref<string>
}
我这里仅演示 frontmatter
使用,其他的同理
在 .vitepress/theme/components
目录新建一个 tags.vue
组件
docs
├─ .vitepress
│ └─ config.mts
│ └─ theme
│ │ ├─ components
│ │ │ └─ tags.vue
│ │ └─ index.ts
└─ index.md
粘贴如下代码,此处的插槽使用的是 doc-before
<script setup>
import DefaultTheme from 'vitepress/theme'
import { useData } from 'vitepress'
const { Layout } = DefaultTheme
const { frontmatter } = useData()
</script>
<template>
<Layout>
<template #doc-before>
<span class="date">🔥 更新时间:{{ frontmatter.date }}</span>
</template>
</Layout>
</template>
<style>
.date{
font-size: 15px;
color: #7f7f7f;
margin-right: 10px;
}
</style>
然后在引入
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import { h } from 'vue'
import tags from './tags.vue'
export default {
extends: DefaultTheme,
Layout: tags,
}
然后在任意 *.md
文章顶部使用 Frontmatter
---
date: 2023-12-19 08:09
---
查看效果
常规使用
在 untils
目录新建一个 sponsors.ts
文件
docs
├─ .vitepress
│ └─ config.mts
│ └─ theme
│ │ ├─ components
│ │ ├─ untils
│ │ │ └─ sponsors.ts <-- ts文件
│ │ └─ index.ts
└─ index.md
粘贴如下代码,保存
import { ref, onMounted } from 'vue'
interface Sponsors {
special: Sponsor[]
platinum: Sponsor[]
platinum_china: Sponsor[]
gold: Sponsor[]
silver: Sponsor[]
bronze: Sponsor[]
}
interface Sponsor {
name: string
img: string
url: string
}
// shared data across instances so we load only once.
const data = ref()
const dataHost = 'https://sponsors.vuejs.org'
const dataUrl = `${dataHost}/vite.json`
const viteSponsors: Pick<Sponsors, 'special' | 'gold'> = {
special: [
// sponsors patak-dev
{
name: 'StackBlitz',
url: 'https://stackblitz.com',
img: '/svg/stackblitz.svg',
},
// sponsors antfu
{
name: 'NuxtLabs',
url: 'https://nuxtlabs.com',
img: '/svg/nuxtlabs.svg',
},
// sponsors bluwy
{
name: 'Astro',
url: 'https://astro.build',
img: '/svg/astro.svg',
},
],
gold: [
// through GitHub -> OpenCollective
{
name: 'Remix',
url: 'https://remix.run/',
img: '/svg/remix.svg',
},
],
}
export function useSponsor() {
onMounted(async () => {
if (data.value) {
return
}
const result = await fetch(dataUrl)
const json = await result.json()
data.value = mapSponsors(json)
})
return {
data,
}
}
function mapSponsors(sponsors: Sponsors) {
return [
{
tier: 'Special Sponsors',
size: 'big',
items: viteSponsors['special'],
},
{
tier: 'Platinum Sponsors',
size: 'big',
items: mapImgPath(sponsors['platinum']),
},
{
tier: 'Gold Sponsors',
size: 'medium',
items: viteSponsors['gold'].concat(mapImgPath(sponsors['gold'])),
},
]
}
const viteSponsorNames = new Set(
Object.values(viteSponsors).flatMap((sponsors) =>
sponsors.map((s) => s.name),
),
)
/**
* Map Vue/Vite sponsors data to objects and filter out Vite-specific sponsors
*/
function mapImgPath(sponsors: Sponsor[]) {
return sponsors
.filter((sponsor) => !viteSponsorNames.has(sponsor.name))
.map((sponsor) => ({
...sponsor,
img: `${dataHost}/images/${sponsor.img}`,
}))
}
然后我们将赞助商的图片放入 public - svg
文件夹
docs
├─ .vitepress
│ └─ config.mts
│ └─ theme
├─ public
│ └─ svg <-- 赞助商svg文件
└─ index.md
在 components
目录新建 HomeSponsors.vue
组件
docs
├─ .vitepress
│ └─ config.mts
│ └─ theme
│ │ ├─ components
│ │ │ └─ HomeSponsors.vue <-- 插槽组件
│ │ └─ index.ts
└─ index.md
粘贴如下代码,保存
<script setup lang="ts">
import { VPHomeSponsors } from 'vitepress/theme'
import { useSponsor } from '../untils/sponsor'
const { data } = useSponsor()
</script>
<template>
<VPHomeSponsors
v-if="data"
message="Vite is free and open source, made possible by wonderful sponsors."
:data="data"
/>
<div class="action">
<a
class="sponsor"
href="https://github.com/sponsors/vitejs"
target="_blank"
rel="noreferrer"
>
Sponsor Vite
</a>
<a
class="sponsor"
href="https://github.com/sponsors/yyx990803"
target="_blank"
rel="noreferrer"
>
Sponsor Evan You
</a>
</div>
</template>
<style scoped>
.action {
display: flex;
justify-content: center;
gap: 1rem;
padding-top: 4rem;
}
.sponsor {
/* .VPButton */
display: inline-block;
border: 1px solid transparent;
text-align: center;
font-weight: 600;
white-space: nowrap;
transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
/* .VPButton.medium */
border-radius: 20px;
padding: 0 20px;
line-height: 38px;
font-size: 14px;
/* .VPButton.sponsor */
border-color: var(--vp-button-sponsor-border);
color: var(--vp-button-sponsor-text);
background-color: var(--vp-button-sponsor-bg);
}
.sponsor:hover {
/* .VPButton.sponsor:hover */
border-color: var(--vp-button-sponsor-hover-border);
color: var(--vp-button-sponsor-hover-text);
background-color: var(--vp-button-sponsor-hover-bg);
}
</style>
最后我们使用 home-features-after
插槽并引入配置文件index.ts
// .vitepress/theme/index.ts
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import HomeSponsors from './components/HomeSponsors.vue'
export default {
extends: DefaultTheme,
Layout() {
return h(DefaultTheme.Layout, null, {
'home-features-after': () => h(HomeSponsors),
})
},
}