Skip to content

主题

更新: 11/16/2024 字数: 0 字 时长: 0 分钟

本节为可选阅读章节,主要针对官方文档中主题的扩展,从源码角度解释方便二次开发。

默认主题是如何实现的

官方文档布局一节告诉我们,通过 frontmatter 中的 layout 控制, 默认主题的情况下会有以下类型的页面:

  • doc:文章页,会渲染md文件,同时会添加样式
  • page:普通页面,只会将md文件编译为html,但是不会有样式,需要自己设计样式
  • home:首页
  • 无布局:没有任何侧边栏、导航栏或页脚,完全自定义
  • 自定义布局:当你注册了全局组件,可以使用注册的组件直接作为1个页面

那这些页面是如何实现的呢?在 Vue.js Devtools 中我们也可以看到整个App包裹在了Layout组件中

翻读Layout源码后, 我们可以发现,整个页面(顶导、侧导等)都定义在这里,读取 config 或者 frontmatter 配置的值来做出对应的改变, 上面说的页面类型也是在这里判断后做出对应的代码判断。

还可以发现无布局的实现就是Content组件,如果再细翻VPContent的实现, 可以发现还是使用了Content组件,但是会包含增加 class 来增加样式。 所以md文件转换为Vue组件后就是Content组件, 整个 VitePress 其实就是围绕Layout来展示的

这也就解释了官方文档 自定义主题一节中的这句话:“最基本的布局组件需要包含一个 <Content /> 组件”

Layout源码
vue

<template>
  <div v-if="frontmatter.layout !== false" class="Layout" :class="frontmatter.pageClass">
    <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">
      <template #sidebar-nav-before>
        <slot name="sidebar-nav-before"/>
      </template>
      <template #sidebar-nav-after>
        <slot name="sidebar-nav-after"/>
      </template>
    </VPSidebar>

    <VPContent>
      <template #page-top>
        <slot name="page-top"/>
      </template>
      <template #page-bottom>
        <slot name="page-bottom"/>
      </template>

      <template #not-found>
        <slot name="not-found"/>
      </template>
      <template #home-hero-before>
        <slot name="home-hero-before"/>
      </template>
      <template #home-hero-info-before>
        <slot name="home-hero-info-before"/>
      </template>
      <template #home-hero-info>
        <slot name="home-hero-info"/>
      </template>
      <template #home-hero-info-after>
        <slot name="home-hero-info-after"/>
      </template>
      <template #home-hero-actions-after>
        <slot name="home-hero-actions-after"/>
      </template>
      <template #home-hero-image>
        <slot name="home-hero-image"/>
      </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 #doc-top>
        <slot name="doc-top"/>
      </template>
      <template #doc-bottom>
        <slot name="doc-bottom"/>
      </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>
  <Content v-else/>
</template>

如何扩展默认主题

上一节中我们已经知道了Layout组件是 VitePress 的核心,所以只要扩展这个Layout就可以。 VitePress 提供了一个接口,在.vitepress/theme目录下导出即可让 VitePress 使用我们定义的主题。

  • 创建新的Layout组件,里面包含默认的Layout组件,可以通过 slot 为默认的Layout注入模块,所有插槽
  • 可以在theme/index.ts中注入一些全局组件,这样我们就可以在Markdown文件中直接使用了
ts
interface Theme {
    /**
     * 每个页面的根布局组件
     * @required
     */
    Layout: Component
    /**
     * 增强 Vue 应用实例
     * @optional
     */
    enhanceApp?: (ctx: EnhanceAppContext) => Awaitable<void>
    /**
     * 扩展另一个主题,在我们的主题之前调用它的 `enhanceApp`
     * @optional
     */
    extends?: Theme
}

interface EnhanceAppContext {
    app: App // Vue 应用实例
    router: Router // VitePress 路由实例
    siteData: Ref<SiteData> // 站点级元数据
}
ts
import DefaultTheme from 'vitepress/theme';
import type {Theme} from 'vitepress';
import Layout from './Layout.vue';

export default {
    extends: DefaultTheme,
    // 扩展原有主题
    Layout: Layout,
    enhanceApp(ctx) {
        // 注册全局组件
        ctx.app.component('XX', XX);
    },
    setup() {
        // 初始化
    },
} satisfies Theme;
vue
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme';
const { Layout } = DefaultTheme;
</script>

<template>
  <Layout>
    <template #layout-bottom>
      <!-- 首页底部插槽,可以添加备案号等 -->
    </template>
  </Layout>
</template>

如何自定义主题

其实和扩展默认主题类似,我们只要将Layout组件替换为自己写的就可以

TIP

Layout组件中要包含Content组件,除非你不想渲染md文件

ts
import Layout from './Layout.vue'

export default {
    Layout,
    enhanceApp({ app, router, siteData }) {
        // ...
    }
} satisfies Theme;
vue
<script setup>
  import { useData } from 'vitepress'
  import NotFound from './NotFound.vue'
  import Home from './Home.vue'
  import Page from './Page.vue'

  const { page, frontmatter } = useData()
</script>

<template>
  <h1>Custom Layout!</h1>

  <NotFound v-if="page.isNotFound" />
  <Home v-if="frontmatter.layout === 'home'" />
  <Page v-else /> <!-- <Page /> renders <Content /> -->
</template>