Skip to content

简介

Vitepress基于Vue3用到了 <slot> 插槽,在 <Layout/> 布局组件中预留了一些插槽,可以对页面布局进行自定义修改

由于也是使用组件,请了解过 组件的使用 了再来看

信息

布局插槽就好比一个插线板,将电器的插头插入对应的插线孔就可以工作了

示例

开始前,请确保你安装了 vue ,已安装的无视

sh
npm i vue
sh
pnpm add -D vue
sh
yarn add -D vue
sh
bun add -D vue

.vitepress/theme/components 目录新建一个 MyLayout.vue组件

markdown
docs
├─ .vitepress
│  └─ config.mts
│  └─ theme
│  │   ├─ components
│  │   │   └─ MyLayout.vue    <-- 插槽组件
│  │   └─ index.ts
└─ index.md

使用上,有两种方案,按使用习惯选择 示例1示例2

示例1:Layout

MyLayout.vue 中粘贴如下代码

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>

提示

这里的 aside-outline-beforedoc-before插槽

如果只改一个布局插槽,自行增减一个 <template> 即可

然后在 .vitepress/theme/index.mts 中引入

ts
// .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 中粘贴如下代码

vue
<script setup>
</script>

<template>
  <div class="title">aside-outline-before</div>
</template>

<style scoped>
.title {
  color: red;
}
</style>

提示

这里的 aside-outline-before插槽

如果想使用多个插槽,再新建一个vue组件即可

ts
// .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:使用首页预留插槽

现在我们已经能做到在FeaturesFooter上添加自定义内容了,但是我有办法将自定义内容加到HeaderHero上吗

答案是可以的,vitepress首页给我们预留了很多插槽,通过插槽我们可以将自定义组件渲染到想要的位置

我们尝试将一个组件放到Hero上方

components目录下新建HeroBefore.vue

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方法

sh
pnpm add -D vue

theme/index.ts中使用插槽

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)
  }
}

效果如下: img

可以看到组件已经渲染到Header下Hero上方了

关于Layout方法中的h函数的使用

/vp/docs/.vitepress/theme/index.ts

单个 retrun h

ts
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

ts
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

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

img

home

Frontmatter 配置 layout: home (默认)时插槽及位置

  • home-hero-before
  • home-hero-info
  • home-hero-image
  • home-hero-after
  • home-features-before
  • home-features-after

img

page

Frontmatter 配置 layout: page (默认)时插槽及位置

  • page-top
  • page-bottom

img

404

在未找到 (404) 页面上

  • not-found

img

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

img

使用插槽原理

重点是vue3提供的h函数

在官方文档的这篇文章中讲到,第三个参数传入一个对象,就表示给第一个参数,也就是render的组件的插槽传入内容

img

这个是vitepress默认使用的theme配置,当用户自定义了theme/index.ts的时候,就会优先使用用户配置,所以当我们传入Layout属性的时候,最终渲染的是我们传入的Layout,所以用h函数改写Layout并使用插槽,就能实现个性化首页

文档页面主题色

使用演示

分别演示两种使用情况,Frontmatter使用常规使用

Frontmatter使用

本方法参考 掘金 @Younglina的文章

通过VitePress官网给出的 useDate 返回页面数据,可以看到返回对象的类型

ts
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组件

markdown
docs
├─ .vitepress
│  └─ config.mts
│  └─ theme
│  │   ├─ components
│  │   │   └─ tags.vue
│  │   └─ index.ts
└─ index.md

粘贴如下代码,此处的插槽使用的是 doc-before

vue
<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">🔥&nbsp;更新时间:{{ frontmatter.date }}</span>
    </template>
  </Layout>
</template>

<style>
  .date{
    font-size: 15px;
    color: #7f7f7f;
    margin-right: 10px;
  }
</style>

然后在引入

ts
// .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

yaml
---
date: 2023-12-19 08:09
---

查看效果

img

常规使用

这里我们参考 Vite官网 下的赞助,代码在 仓库 查找

img

untils 目录新建一个 sponsors.ts 文件

markdown
docs
├─ .vitepress
│  └─ config.mts
│  └─ theme
│  │   ├─ components
│  │   ├─ untils
│  │   │   └─ sponsors.ts    <-- ts文件
│  │   └─ index.ts
└─ index.md

粘贴如下代码,保存

ts
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文件夹

markdown
docs
├─ .vitepress
│  └─ config.mts
│  └─ theme
├─ public
│  └─ svg      <-- 赞助商svg文件
└─ index.md

components 目录新建 HomeSponsors.vue 组件

markdown
docs
├─ .vitepress
│  └─ config.mts
│  └─ theme
│  │   ├─ components
│  │   │   └─ HomeSponsors.vue    <-- 插槽组件
│  │   └─ index.ts
└─ index.md

粘贴如下代码,保存

ts
<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

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),
    })
  },
}